diff --git a/Box3JS-NeoForge-1.21.1/.gitignore b/Box3JS-NeoForge-1.21.1/.gitignore index fee2f7b..89203a2 100644 --- a/Box3JS-NeoForge-1.21.1/.gitignore +++ b/Box3JS-NeoForge-1.21.1/.gitignore @@ -32,6 +32,11 @@ bin/ ### Mac OS ### .DS_Store +### VitePress ### +node_modules/ +docs/.vitepress/dist/ +docs/.vitepress/cache/ + ### Minecraft Modding ### run/ !**/src/**/run/ diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md index 4f41d40..faecf9b 100644 --- a/Box3JS-NeoForge-1.21.1/README.md +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -6,7 +6,9 @@ **无需 Java 知识,用 TypeScript 为你的 Minecraft 服务器创造无限玩法。** -Box3JS 是一个内置于 NeoForge 模组的服务端脚本引擎(Mozilla Rhino),延续了神奇代码岛的 API 风格。告别复杂的 Java 模组开发——写 TypeScript,一键热重载,即时生效。PvP 竞技场、RPG 副本、派对小游戏、世界管理、社交工具,全能用脚本快速实现。 +Box3JS 是一个社区驱动的 Minecraft 模组,在服务端嵌入 Mozilla Rhino JavaScript 引擎。它的 API 设计延续了[神奇代码岛](https://box3.fun)(深圳奇梦岛科技有限公司)的风格——把 Box3 平台简洁高效的开发体验带进 Minecraft。告别复杂的 Java 模组开发:写 TypeScript,一键热重载,即时生效。PvP 竞技场、RPG 副本、派对小游戏、世界管理、社交工具,全能用脚本快速实现。 + +> 了解 Box3JS 与神奇代码岛的关系?→ [Box3JS 与神奇代码岛](docs/guide/about-box3js.md) ## 安装 @@ -24,7 +26,7 @@ Box3JS 是一个内置于 NeoForge 模组的服务端脚本引擎(Mozilla Rhin 这会创建 TypeScript 项目: -``` +```` config/box3/script/mygame/ ├── package.json ← npm 依赖(esbuild、Babel、TypeScript) ├── tsconfig.base.json ← 公共 TS 编译选项 @@ -47,7 +49,7 @@ config/box3/script/mygame/ ```bash cd config/box3/script/mygame npm install && npm run build -``` +```` ``` /box3script sandbox mygame # (推荐) 开启沙盒,放心测试 @@ -58,19 +60,19 @@ npm install && npm run build ## 为什么选择 Box3JS? -| 特性 | 说明 | -| -------------- | -------------------------------------------------------------------------------- | -| **零门槛** | 会 JS/TS 就能写,无需 Gradle、无需 IDE、无需重启 | -| **热重载** | 改代码 → build → reload,秒级生效。开启 `watch` 自动重载 | -| **沙盒保护** | 开启沙盒自动追踪所有脚本修改,关闭时完整回滚,服务器不留痕迹 | -| **TypeScript** | 完整 `.d.ts` 类型声明,esbuild + Babel 编译管线,享受智能提示 | -| **20+ 种事件** | onTick、onPlayerJoin、onChat、onEntityDeath、onBlockActivate、onButtonPressed... | -| **视觉效果** | 13+ 粒子、烟花、闪电、爆炸、音效 | -| **客户端 API** | 键盘输入、屏幕 UI、聊天拦截、音效/音乐控制、客户端存储、SQLite、HTTP、双向事件通道 | -| **游戏系统** | 计分板、BossBar、队伍、世界边界、跨脚本通信 | -| **自定义注册表** | JSON 配置注册方块、物品(食物/工具/盔甲)、音效与创造标签页,编译为独立 JAR | -| **数据持久化** | JSON 存储 + SQLite 数据库(排行榜、经济、玩家数据) | -| **独立打包** | `/box3script compile` 将脚本编译为独立 JAR 模组,便于分发部署 | +| 特性 | 说明 | +| ---------------- | ---------------------------------------------------------------------------------- | +| **零门槛** | 会 JS/TS 就能写,无需 Gradle、无需 IDE、无需重启 | +| **热重载** | 改代码 → build → reload,秒级生效。开启 `watch` 自动重载 | +| **沙盒保护** | 开启沙盒自动追踪所有脚本修改,关闭时完整回滚,服务器不留痕迹 | +| **TypeScript** | 完整 `.d.ts` 类型声明,esbuild + Babel 编译管线,享受智能提示 | +| **20+ 种事件** | onTick、onPlayerJoin、onChat、onEntityDeath、onBlockActivate、onButtonPressed... | +| **视觉效果** | 13+ 粒子、烟花、闪电、爆炸、音效 | +| **客户端 API** | 键盘输入、屏幕 UI、聊天拦截、音效/音乐控制、客户端存储、SQLite、HTTP、双向事件通道 | +| **游戏系统** | 计分板、BossBar、队伍、世界边界、跨脚本通信 | +| **自定义注册表** | JSON 配置注册方块、物品(食物/工具/盔甲)、音效与创造标签页,编译为独立 JAR | +| **数据持久化** | JSON 存储 + SQLite 数据库(排行榜、经济、玩家数据) | +| **独立打包** | `/box3script compile` 将脚本编译为独立 JAR 模组,便于分发部署 | ## 命令 @@ -89,23 +91,23 @@ npm install && npm run build ## API 速览 -| 全局对象 | 用途 | -| -------------------------------- | ----------------------------------------------------------------------------------- | -| `world` | 世界状态、事件回调、粒子、烟花、闪电、音效、计分板、BossBar、队伍、边界 | -| `entity` | 实体属性、AI 寻路、装备、药水效果、标签、导航 | -| `player` | 背包、飞行、游戏模式、传送、消息、经验、音效 | -| `voxels` | 方块读写、区域填充、刷怪笼 | -| `http` | HTTP 网络请求(同步 + 异步,GET/POST/JSON) | -| `remoteChannel` | 服务端 ↔ 客户端双向事件通讯 | -| `registries` | 自定义方块/物品/音效(编译 JAR 模式),见 [registries.md](docs/api/registries.md) | -| `client` · `input` · `ui` · `chat` · `audio` | 客户端脚本:生命周期、键盘、屏幕文字、聊天、音频控制 | -| `storage` | JSON 数据持久化(服务端 & 客户端) | -| `db` | SQLite 数据库(服务端 & 客户端) | -| `console` | 控制台日志输出(`log`/`warn`/`error`/`debug`/`assert`/`clear`) | -| `GameVector3` | 三维向量(坐标运算) | -| `GameBounds3` | 包围盒 | -| `GameRGBColor` / `GameRGBAColor` | RGB/RGBA 颜色 | -| `GameQuaternion` | 四元数(旋转运算) | +| 全局对象 | 用途 | +| -------------------------------------------- | --------------------------------------------------------------------------------- | +| `world` | 世界状态、事件回调、粒子、烟花、闪电、音效、计分板、BossBar、队伍、边界 | +| `entity` | 实体属性、AI 寻路、装备、药水效果、标签、导航 | +| `player` | 背包、飞行、游戏模式、传送、消息、经验、音效 | +| `voxels` | 方块读写、区域填充、刷怪笼 | +| `http` | HTTP 网络请求(同步 + 异步,GET/POST/JSON) | +| `remoteChannel` | 服务端 ↔ 客户端双向事件通讯 | +| `registries` | 自定义方块/物品/音效(编译 JAR 模式),见 [registries.md](docs/api/registries.md) | +| `client` · `input` · `ui` · `chat` · `audio` | 客户端脚本:生命周期、键盘、屏幕文字、聊天、音频控制 | +| `storage` | JSON 数据持久化(服务端 & 客户端) | +| `db` | SQLite 数据库(服务端 & 客户端) | +| `console` | 控制台日志输出(`log`/`warn`/`error`/`debug`/`assert`/`clear`) | +| `GameVector3` | 三维向量(坐标运算) | +| `GameBounds3` | 包围盒 | +| `GameRGBColor` / `GameRGBAColor` | RGB/RGBA 颜色 | +| `GameQuaternion` | 四元数(旋转运算) | [文档首页 →](docs/README.md) · [API 总览 →](docs/api/README.md) · [按任务速查 →](docs/api/README.md#功能速查---我想) @@ -116,7 +118,7 @@ npm install && npm run build | # | 教程 | 时长 | 学什么 | | --- | ----------------------------------------------------- | ------ | ---------------------------------------- | | 1 | [从零开始](docs/tutorial/01-basics.md) | 10 min | 创建项目、第一个脚本、聊天命令、定时任务 | -| 2 | [玩家操控与物品](docs/tutorial/02-player-items.md) | 15 min | 传送、飞行、物品、附魔、药水 | +| 2 | [玩家操控与物品](docs/tutorial/02-player-items.md) | 15 min | 传送、飞行、物品、附魔、药水 | | 3 | [事件系统与实体](docs/tutorial/03-events-entities.md) | 15 min | 事件回调、实体生成、AI、战斗、巡逻 | | 4 | [高级游戏系统](docs/tutorial/04-advanced-systems.md) | 15 min | 计分板、BossBar、队伍、边界、跨脚本通信 | | 5 | [实战小游戏](docs/tutorial/05-examples.md) | 20 min | PvP 竞技场、粒子烟花、波次刷怪、特效大全 | @@ -129,6 +131,7 @@ npm install && npm run build docs/ ├── guide/ ← 入门指南 │ ├── README.md 指南总览 +│ ├── about-box3js.md Box3JS 与神奇代码岛(起源、关系、优势) │ ├── getting-started.md 从零开始(环境、第一个脚本、调试、发布) │ ├── architecture.md 运行原理(Rhino 引擎、作用域、构建管线) │ └── js-vs-java.md JS vs Java 模组开发对比 diff --git a/Box3JS-NeoForge-1.21.1/README_en.md b/Box3JS-NeoForge-1.21.1/README_en.md index d881602..4d91210 100644 --- a/Box3JS-NeoForge-1.21.1/README_en.md +++ b/Box3JS-NeoForge-1.21.1/README_en.md @@ -6,7 +6,9 @@ **No Java knowledge required. Build unlimited Minecraft gameplay with TypeScript.** -Box3JS is a server-side scripting engine (Mozilla Rhino) built into a NeoForge mod. Forget complex Java mod development — write TypeScript, hot-reload instantly, see changes live. PvP arenas, RPG dungeons, party games, world management, social tools — all achievable with scripts. +Box3JS is a community-driven Minecraft mod (NeoForge 1.21.1) that embeds the Mozilla Rhino JavaScript engine in the server. Its API design is inspired by [Box3](https://box3.fun) (Shenzhen Qimengdao Technology Co., Ltd.) — bringing Box3's clean, efficient developer experience into Minecraft. Forget complex Java mod development: write TypeScript, hot-reload instantly, see changes live. PvP arenas, RPG dungeons, party games, world management, social tools — all achievable with scripts. + +> Curious about Box3JS's relationship with the Box3 platform? → [Box3JS & Box3](docs/guide/about-box3js_en.md) ## Installation @@ -129,6 +131,7 @@ From zero to full mini-games. Every example is TypeScript-compiled and ESLint-ve docs/ ├── guide/ ← Getting Started │ ├── README.md Guide overview +│ ├── about-box3js.md Box3JS & Box3 (origin, relationship, advantages) │ ├── getting-started.md From zero (setup, first script, debug, deploy) │ ├── architecture.md Internals (Rhino engine, scopes, build pipeline) │ └── js-vs-java.md JS vs Java modding comparison diff --git a/Box3JS-NeoForge-1.21.1/build.gradle b/Box3JS-NeoForge-1.21.1/build.gradle index e366251..db49f9c 100644 --- a/Box3JS-NeoForge-1.21.1/build.gradle +++ b/Box3JS-NeoForge-1.21.1/build.gradle @@ -209,6 +209,17 @@ tasks.named('jar', Jar).configure { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation + options.compilerArgs.addAll(['-Xlint:deprecation', '-Xlint:unchecked']) +} + +tasks.register('verifyBox3JSProject', Exec) { + group = 'verification' + description = 'Verifies Box3JS template files, runtime DTS split, and event API token consistency.' + commandLine 'node', 'tools/verify-box3js-project.mjs' +} + +tasks.named('check').configure { + dependsOn tasks.named('verifyBox3JSProject') } // IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. diff --git a/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs b/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs new file mode 100644 index 0000000..849e398 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/.vitepress/config.mjs @@ -0,0 +1,233 @@ +import { defineConfig } from 'vitepress' + +const cnNav = [ + { text: '首页', link: '/' }, + { text: '指南', link: '/guide/README' }, + { text: '教程', link: '/tutorial/README' }, + { text: 'API', link: '/api/README' }, +] + +const enNav = [ + { text: 'Home', link: '/en/' }, + { text: 'Guide', link: '/en/guide/README' }, + { text: 'Tutorials', link: '/en/tutorial/README' }, + { text: 'API', link: '/en/api/README' }, +] + +const cnSidebar = [ + { + text: '快速开始', + collapsed: false, + items: [ + { text: '文档索引', link: '/overview' }, + { text: '总览', link: '/guide/README' }, + { text: '快速开始', link: '/guide/getting-started' }, + { text: '常用配方', link: '/guide/recipes' }, + { text: '常见问题', link: '/guide/faq' }, + { text: '运行原理', link: '/guide/architecture' }, + { text: 'JS vs Java', link: '/guide/js-vs-java' }, + { text: '关于 Box3JS', link: '/guide/about-box3js' }, + ], + }, + { + text: '教程', + collapsed: false, + items: [ + { text: '总览', link: '/tutorial/README' }, + { text: '01 — 基础入门', link: '/tutorial/01-basics' }, + { text: '02 — 玩家与物品', link: '/tutorial/02-player-items' }, + { text: '03 — 事件与实体', link: '/tutorial/03-events-entities' }, + { text: '04 — 进阶系统', link: '/tutorial/04-advanced-systems' }, + { text: '05 — 实战案例', link: '/tutorial/05-examples' }, + { text: '06 — 客户端脚本', link: '/tutorial/06-client-scripting' }, + ], + }, + { + text: 'API 参考', + collapsed: false, + items: [ + { text: '总览', link: '/api/README' }, + { + text: '服务端 API', + collapsed: false, + items: [ + { text: 'Server', link: '/api/server' }, + { text: 'World', link: '/api/world' }, + { text: 'Entity', link: '/api/entity' }, + { text: 'Player', link: '/api/player' }, + { text: 'Voxels', link: '/api/voxels' }, + { text: 'Registries', link: '/api/registries' }, + ], + }, + { + text: '客户端 API', + collapsed: false, + items: [ + { text: 'Client', link: '/api/client' }, + ], + }, + { + text: '共用 API', + collapsed: false, + items: [ + { text: 'Storage', link: '/api/storage' }, + { text: 'Database', link: '/api/database' }, + { text: 'HTTP', link: '/api/http' }, + { text: 'Math', link: '/api/math' }, + ], + }, + { + text: '命令参考', + collapsed: false, + items: [ + { text: '/box3script', link: '/api/commands' }, + ], + }, + ], + }, +] + +const enSidebar = [ + { + text: 'Get Started', + collapsed: false, + items: [ + { text: 'Overview', link: '/en/guide/README' }, + { text: 'Getting Started', link: '/en/guide/getting-started' }, + { text: 'Recipes', link: '/en/guide/recipes' }, + { text: 'FAQ', link: '/en/guide/faq' }, + { text: 'Architecture', link: '/en/guide/architecture' }, + { text: 'JS vs Java', link: '/en/guide/js-vs-java' }, + { text: 'About Box3JS', link: '/en/guide/about-box3js' }, + ], + }, + { + text: 'Tutorials', + collapsed: false, + items: [ + { text: 'Overview', link: '/en/tutorial/README' }, + { text: '01 — Basics', link: '/en/tutorial/01-basics' }, + { text: '02 — Player & Items', link: '/en/tutorial/02-player-items' }, + { text: '03 — Events & Entities', link: '/en/tutorial/03-events-entities' }, + { text: '04 — Advanced Systems', link: '/en/tutorial/04-advanced-systems' }, + { text: '05 — Examples', link: '/en/tutorial/05-examples' }, + { text: '06 — Client Scripting', link: '/en/tutorial/06-client-scripting' }, + ], + }, + { + text: 'API Reference', + collapsed: false, + items: [ + { text: 'Overview', link: '/en/api/README' }, + { + text: 'Server-side API', + collapsed: false, + items: [ + { text: 'Server', link: '/en/api/server' }, + { text: 'World', link: '/en/api/world' }, + { text: 'Entity', link: '/en/api/entity' }, + { text: 'Player', link: '/en/api/player' }, + { text: 'Voxels', link: '/en/api/voxels' }, + { text: 'Registries', link: '/en/api/registries' }, + ], + }, + { + text: 'Client-side API', + collapsed: false, + items: [ + { text: 'Client', link: '/en/api/client' }, + ], + }, + { + text: 'Shared API', + collapsed: false, + items: [ + { text: 'Storage', link: '/en/api/storage' }, + { text: 'Database', link: '/en/api/database' }, + { text: 'HTTP', link: '/en/api/http' }, + { text: 'Math', link: '/en/api/math' }, + ], + }, + { + text: 'CLI Reference', + collapsed: false, + items: [ + { text: '/box3script', link: '/en/api/commands' }, + ], + }, + ], + }, +] + +export default defineConfig({ + title: 'Box3JS', + description: 'Minecraft NeoForge 1.21.1 的 JavaScript/TypeScript 脚本引擎', + lastUpdated: true, + cleanUrls: true, + ignoreDeadLinks: true, + + head: [ + ['link', { rel: 'icon', href: '/favicon.ico' }], + ], + + locales: { + root: { + label: '简体中文', + lang: 'zh-CN', + title: 'Box3JS', + description: '基于 Mozilla Rhino 引擎,为 Minecraft NeoForge 1.21.1 提供 JS/TS 双端脚本能力 — 神奇代码岛同款编程体验', + themeConfig: { + nav: cnNav, + sidebar: cnSidebar, + editLink: { + pattern: 'https://github.com/box3lab/Box3JS/edit/main/Box3JS-NeoForge-1.21.1/docs/:path', + }, + lastUpdated: { + text: '最后更新', + }, + docFooter: { + prev: '上一页', + next: '下一页', + }, + footer: { + message: '基于 MIT 许可证发布', + }, + }, + }, + en: { + label: 'English', + lang: 'en-US', + title: 'Box3JS', + description: 'JavaScript/TypeScript dual-side scripting engine for Minecraft NeoForge 1.21.1 — same programming experience as Box3', + themeConfig: { + nav: enNav, + sidebar: enSidebar, + editLink: { + pattern: 'https://github.com/box3lab/Box3JS/edit/main/Box3JS-NeoForge-1.21.1/docs/en/:path', + }, + lastUpdated: { + text: 'Last updated', + }, + docFooter: { + prev: 'Previous page', + next: 'Next page', + }, + footer: { + message: 'Released under the MIT License.', + }, + }, + }, + }, + + themeConfig: { + logo: false, + + socialLinks: [ + { icon: 'github', link: 'https://github.com/box3lab/Box3JS' }, + ], + + search: { + provider: 'local', + }, + }, +}) diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md index 14bfc72..d7f6338 100644 --- a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md @@ -1,135 +1,119 @@ # Box3 API vs Box3JS 实现对比 -本文档详细对比官方 Box3 平台 API 与 Box3JS 模组(NeoForge 1.21.1)的实现差异。仅涉及**服务端 API**,因为客户端 API(ClientUI、ClientAudio、ClientMedia 等)在 Box3JS 中完全不可用——Minecraft 模组运行在服务端,没有 Box3 平台的客户端渲染环境。 +本文档详细对比官方 Box3 平台 API 与 Box3JS 模组的实现差异。 > **图例**: ✅ 已实现 | ⚠️ 部分实现 | ❌ 未实现 | ⬆ 独有扩展 ---- - -## 目录 - -1. [GameWorld (world)](#1-gameworld-world) -2. [GameEntity (entity)](#2-gameentity-entity) -3. [GamePlayerEntity (entity.player)](#3-gameplayerentity-entityplayer) -4. [GameVoxels (voxels)](#4-gamevoxels-voxels) -5. [GameDataStorage (storage)](#5-gamedatastorage-storage) -6. [Math 类型](#6-math-类型) -7. [其他服务端 API](#7-其他服务端-api) -8. [客户端 API(不适用)](#8-客户端-api不适用) -9. [Box3JS 独有 MC 扩展](#9-box3js-独有-mc-扩展) -10. [总结](#10-总结) - ---- - ## 1. GameWorld (world) ### 1.1 基础属性 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.projectName()` (只读方法) | `world.projectName` (属性) | ✅ | 返回当前脚本项目名;服务器 MOTD 使用 `world.serverId` | -| `world.serverId` (属性) | `world.serverId` (读写属性) | ✅ | 一致。映射到服务端 MOTD(get/set) | -| `world.currentTick()` (只读方法) | `world.currentTick` (属性) | ✅ | 返回服务器总 tick 数 | -| `world.url` (只读属性) | — | ❌ | 未实现。MC 无地图 URL 概念 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------- | --------------------------- | ---- | ----------------------------------------------------- | +| `world.projectName()` (只读方法) | `world.projectName` (属性) | ✅ | 返回当前脚本项目名;服务器 MOTD 使用 `world.serverId` | +| `world.serverId` (属性) | `world.serverId` (读写属性) | ✅ | 一致。映射到服务端 MOTD(get/set) | +| `world.currentTick()` (只读方法) | `world.currentTick` (属性) | ✅ | 返回服务器总 tick 数 | +| `world.url` (只读属性) | — | ❌ | 未实现。MC 无地图 URL 概念 | ### 1.2 天气 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.rainDensity` (属性 0-1) | `world.rainDensity` (属性) | ✅ | 一致 | -| — | `world.thunderDensity` (属性) | ⬆ | MC 扩展。Box3 无打雷概念 | -| — | `world.clearWeather()` | ⬆ | MC 扩展。清除雨和雷 | -| `world.maxFog` | — | ❌ | | -| `world.fogColor` | — | ❌ | | -| `world.fogStartDistance` | — | ❌ | | -| `world.fogHeightOffset` | — | ❌ | | -| `world.fogUniformDensity` | — | ❌ | | -| `world.fogHeightFalloff` | — | ❌ | | -| `world.rainSpeed` | — | ❌ | | -| `world.rainColor` | — | ❌ | | -| `world.rainDirection` | — | ❌ | | -| `world.rainInterference` | — | ❌ | | -| `world.rainSizeLo/Hi` | — | ❌ | | -| `world.snowColor` | — | ❌ | Box3 有独立雪花系统,MC 降雪依附于原版天气 | -| `world.snowTexture` | — | ❌ | | -| `world.snowDensity` | — | ❌ | | -| `world.snowFallSpeed` | — | ❌ | | -| `world.snowSpinSpeed` | — | ❌ | | -| `world.snowSizeLo/Hi` | — | ❌ | | -| `world.lightMode` | — | ❌ | Box3 的手动/自然光照模式 | -| `world.sunFrequency/Phase/Direction/Light` | — | ❌ | | -| `world.skyLeftLight/RightLight/...` | — | ❌ | Box3 六个方向环境光 | -| `world.lunarPhase` | — | ❌ | | - -**原因**: Box3 拥有独立的天气/光照渲染引擎,可以精细控制雾、雨、雪、光照参数。MC 的天气和光照系统由原版引擎控制,模组无法在不安装客户端 mod 的情况下改变这些视觉效果。要实现这些需要客户端侧渲染 hook,超出了服务端脚本引擎的范围。 +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------ | ------------------------------- | ---- | ------------------------------------------ | +| `world.rainDensity` (属性 0-1) | `world.rainDensity` (属性) | ✅ | 一致 | +| — | `world.thunderDensity` (属性) | ⬆ | MC 扩展。Box3 无打雷概念 | +| — | `world.clearWeather()` | ⬆ | MC 扩展。清除雨和雷 | +| `world.maxFog` | `client.setFogEndDistance(d)` | ✅ | 客户端 API。设置雾完全遮挡距离(方块) | +| `world.fogColor` | `client.setFogColor(r, g, b)` | ✅ | 客户端 API。RGB 0-255 | +| `world.fogStartDistance` | `client.setFogStartDistance(d)` | ✅ | 客户端 API。雾开始距离(方块) | +| `world.fogHeightOffset` | — | ❌ | | +| `world.fogUniformDensity` | — | ❌ | | +| `world.fogHeightFalloff` | — | ❌ | | +| `world.rainSpeed` | — | ❌ | | +| `world.rainColor` | — | ❌ | | +| `world.rainDirection` | — | ❌ | | +| `world.rainInterference` | — | ❌ | | +| `world.rainSizeLo/Hi` | — | ❌ | | +| `world.snowColor` | — | ❌ | Box3 有独立雪花系统,MC 降雪依附于原版天气 | +| `world.snowTexture` | — | ❌ | | +| `world.snowDensity` | — | ❌ | | +| `world.snowFallSpeed` | — | ❌ | | +| `world.snowSpinSpeed` | — | ❌ | | +| `world.snowSizeLo/Hi` | — | ❌ | | +| `world.lightMode` | — | ❌ | Box3 的手动/自然光照模式 | +| `world.sunFrequency/Phase/Direction/Light` | — | ❌ | | +| `world.skyLeftLight/RightLight/...` | — | ❌ | Box3 六个方向环境光 | +| `world.lunarPhase` | — | ❌ | | + +**原因**: Box3 拥有独立的天气/光照渲染引擎,可以精细控制雾、雨、雪、光照参数。MC 的天气和光照系统由原版引擎控制,这些视觉效果需要客户端侧渲染 hook。Box3JS 客户端脚本引擎**已暴露雾颜色和距离 API**(`client.setFogColor` / `client.setFogStartDistance` / `client.setFogEndDistance` / `client.resetFog`)。雪花/光照等高级渲染参数 API 未来可继续扩展。 ### 1.3 时间 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.time` (属性) | `world.time` (属性, 本质是 getTime/setTime) | ✅ | 一致。Box3 一天 = 24000 tick | -| `world.setTime(tick)` | `world.setTime(tick)` | ✅ | 便捷方法,一致 | -| `world.timeScale` (属性 0-1) | `world.timeScale` (属性) | ✅ | 一致。底层操作 doDaylightCycle 规则 | -| — | `world.setTime(tick)` | ⬆ | 等同于 `world.time = tick` | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------------- | ------------------------------------------- | ---- | ----------------------------------- | +| `world.time` (属性) | `world.time` (属性, 本质是 getTime/setTime) | ✅ | 一致。Box3 一天 = 24000 tick | +| `world.setTime(tick)` | `world.setTime(tick)` | ✅ | 便捷方法,一致 | +| `world.timeScale` (属性 0-1) | `world.timeScale` (属性) | ✅ | 一致。底层操作 doDaylightCycle 规则 | +| — | `world.setTime(tick)` | ⬆ | 等同于 `world.time = tick` | ### 1.4 难度 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.difficulty` (属性) | `world.difficulty` (属性) | ✅ | 一致。get 返回名称字符串,set 接受名称或数字 0-3 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------- | ------------------------- | ---- | ------------------------------------------------ | +| `world.difficulty` (属性) | `world.difficulty` (属性) | ✅ | 一致。get 返回名称字符串,set 接受名称或数字 0-3 | ### 1.5 出生点 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.spawnPoint` (只读属性) | `world.spawnPoint` (只读属性) | ✅ | 一致,返回 GameVector3 | -| `world.setWorldSpawn(pos)` | `world.setWorldSpawn(pos)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ----------------------------- | ---- | ---------------------- | +| `world.spawnPoint` (只读属性) | `world.spawnPoint` (只读属性) | ✅ | 一致,返回 GameVector3 | +| `world.setWorldSpawn(pos)` | `world.setWorldSpawn(pos)` | ✅ | 一致 | ### 1.6 游戏规则 Box3 **没有**游戏规则 API。Box3JS 完全为 MC 扩展: -| Box3JS API | 说明 | -|------------|------| -| `world.getGameRule(name)` | MC 扩展。获取游戏规则布尔值 | +| Box3JS API | 说明 | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `world.getGameRule(name)` | MC 扩展。获取游戏规则布尔值 | | `world.setGameRule(name, value)` | MC 扩展。支持 7 种规则:doDaylightCycle, doWeatherCycle, keepInventory, doMobSpawning, doFireTick, mobGriefing, doImmediateRespawn | ### 1.7 物理系统 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.useOBB` | — | ❌ | OBB 碰撞检测 | -| `world.gravity` | — | ❌ | 世界重力 | -| `world.airFriction` | — | ❌ | 空气阻力 | -| `world.addCollisionFilter(a, b)` | — | ❌ | 碰撞过滤 | -| `world.removeCollisionFilter(a, b)` | — | ❌ | | -| `world.clearCollisionFilters()` | — | ❌ | | -| `world.collisionFilters()` | — | ❌ | | -| `world.testSelector(sel, ent)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | ----------- | ---- | ------------ | +| `world.useOBB` | — | ❌ | OBB 碰撞检测 | +| `world.gravity` | — | ❌ | 世界重力 | +| `world.airFriction` | — | ❌ | 空气阻力 | +| `world.addCollisionFilter(a, b)` | — | ❌ | 碰撞过滤 | +| `world.removeCollisionFilter(a, b)` | — | ❌ | | +| `world.clearCollisionFilters()` | — | ❌ | | +| `world.collisionFilters()` | — | ❌ | | +| `world.testSelector(sel, ent)` | — | ❌ | | **原因**: MC 的物理系统由原版引擎控制,重力/碰撞/摩擦力是全局常量,无法通过脚本动态修改。 ### 1.8 实体生成 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.createEntity(config)` | `world.createEntity(config)` | ✅ | 一致。接受 NativeObject 配置对象,支持 type/position/velocity/fixed/gravity/friction/mass/restitution/collides/meshInvisible/hp/maxHp/tags 字段 | -| `world.spawnEntity(type, pos)` | `world.spawnEntity(type, pos)` | ⬆ | 便捷方法。简化版生成,仅接受类型+位置 | -| `world.entityQuota()` | — | ❌ | 实体配额查询 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `world.createEntity(config)` | `world.createEntity(config)` | ✅ | 一致。接受 NativeObject 配置对象,支持 type/position/velocity/fixed/gravity/friction/mass/restitution/collides/meshInvisible/hp/maxHp/tags 字段 | +| `world.spawnEntity(type, pos)` | `world.spawnEntity(type, pos)` | ⬆ | 便捷方法。简化版生成,仅接受类型+位置 | +| `world.entityQuota()` | — | ❌ | 实体配额查询 | ### 1.9 查询 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.querySelector(selector)` | `world.querySelector(selector)` | ✅ | 一致 | -| `world.querySelectorAll(selector)` | `world.querySelectorAll(selector)` | ✅ | 一致 | -| `world.searchBox(bounds)` | `world.searchBox(bounds)` | ✅ | 一致。接受 GameBounds3,内部委托 entitiesInArea | -| `world.raycast(origin, dir, options?)` | `world.raycast(origin, dir)` / `world.raycast(origin, dir, maxDist)` | ⚠️ | Box3JS 无 options 对象(无 ignoreFluid/ignoreVoxel/ignoreEntities 等),仅支持 maxDistance | -| — | `world.entitiesInArea(pos1, pos2)` | ⬆ | MC 扩展 | -| — | `world.entitiesInRadius(x, y, z, r)` / `world.entitiesInRadius(pos, r)` | ⬆ | MC 扩展 | -| — | `world.getBiome(x, y, z)` / `world.getBiome(pos)` | ⬆ | MC 扩展。返回生物群系 ID(如 "minecraft:plains") | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------- | ----------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------ | +| `world.querySelector(selector)` | `world.querySelector(selector)` | ✅ | 一致 | +| `world.querySelectorAll(selector)` | `world.querySelectorAll(selector)` | ✅ | 一致 | +| `world.searchBox(bounds)` | `world.searchBox(bounds)` | ✅ | 一致。接受 GameBounds3,内部委托 entitiesInArea | +| `world.raycast(origin, dir, options?)` | `world.raycast(origin, dir)` / `world.raycast(origin, dir, maxDist)` | ⚠️ | Box3JS 无 options 对象(无 ignoreFluid/ignoreVoxel/ignoreEntities 等),仅支持 maxDistance | +| — | `world.entitiesInArea(pos1, pos2)` | ⬆ | MC 扩展 | +| — | `world.entitiesInRadius(x, y, z, r)` / `world.entitiesInRadius(pos, r)` | ⬆ | MC 扩展 | +| — | `world.getBiome(x, y, z)` / `world.getBiome(pos)` | ⬆ | MC 扩展。返回生物群系 ID(如 "minecraft:plains") | **Raycast 返回值差异**: + - Box3 返回: `{origin, direction, distance, hit, hitEntity, hitPosition, hitVoxel, voxelIndex, normal}` - Box3JS 返回: `{hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel}` - 差异: 字段命名不同 (`normal` → `normalX/Y/Z`, `hitEntity` → `entity`),无 `origin/direction/hitPosition/voxelIndex` @@ -138,59 +122,60 @@ Box3 **没有**游戏规则 API。Box3JS 完全为 MC 扩展: Box3 的区域系统完全未实现: -| Box3 API | 状态 | -|----------|------| -| `world.addZone(config)` | ❌ | -| `world.removeZone(trigger)` | ❌ | -| `world.zones()` | ❌ | -| `zone.onEnter / zone.onLeave` | ❌ | -| `zone.entities` | ❌ | -| `zone.remove()` | ❌ | +| Box3 API | 状态 | +| ----------------------------- | ---- | +| `world.addZone(config)` | ❌ | +| `world.removeZone(trigger)` | ❌ | +| `world.zones()` | ❌ | +| `zone.onEnter / zone.onLeave` | ❌ | +| `zone.entities` | ❌ | +| `zone.remove()` | ❌ | -Zone 可以设置局部天气/光照/力场参数,这在 MC 服务端无法实现(需要客户端渲染支持)。 +Zone 可以设置局部天气/光照/力场参数,需要客户端渲染支持(Box3JS 客户端引擎已可用,但具体的 Zone 渲染 API 尚未暴露)。 ### 1.11 聊天 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.say(message)` | `world.say(message)` | ✅ | 一致。全服广播 | -| `world.createTempChat(chatId?)` | — | ❌ | 临时聊天频道 | -| `world.destroyTempChat(chatId)` | — | ❌ | | -| `world.addTempChatPlayer(chatId, player)` | — | ❌ | | -| `world.removeTempChatPlayer(chatId, player)` | — | ❌ | | -| `world.getTempChats()` | — | ❌ | | -| `world.getTempChatUsers(chatId)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------------- | -------------------- | ---- | -------------- | +| `world.say(message)` | `world.say(message)` | ✅ | 一致。全服广播 | +| `world.createTempChat(chatId?)` | — | ❌ | 临时聊天频道 | +| `world.destroyTempChat(chatId)` | — | ❌ | | +| `world.addTempChatPlayer(chatId, player)` | — | ❌ | | +| `world.removeTempChatPlayer(chatId, player)` | — | ❌ | | +| `world.getTempChats()` | — | ❌ | | +| `world.getTempChatUsers(chatId)` | — | ❌ | | ### 1.12 事件回调 -| Box3 API | Box3JS 实现 | 状态 | 回调签名差异 | -|----------|-------------|------|-------------| -| `world.onTick(fn)` | `world.onTick(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{tick, prevTick, elapsedTimeMS, skip}`;Box3JS 已对齐传入 NativeObject | -| `world.onPlayerJoin(fn)` | `world.onPlayerJoin(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | -| `world.onPlayerLeave(fn)` | `world.onPlayerLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | -| `world.onChat(fn)` | `world.onChat(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, message, tick}`;Box3JS 传入 `(entity, message, tick)` — 展开参数 | -| `world.onInteract(fn)` | `world.onInteract(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, targetEntity, tick}`;Box3JS 传入 `(entity, target, tick)` — 展开参数 | -| `world.onEntityContact(fn)` | `world.onEntityContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, other, tick}`;Box3JS 传入 `(entity, other, tick)` — 缺少 axis/force | -| `world.onEntitySeparate(fn)` | `world.onEntitySeparate(fn)` → GameEventHandlerToken | ✅ | 同上 | -| `world.onVoxelContact(fn)` | `world.onVoxelContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, voxel, tick, x, y, z}`;Box3JS 传入 `(entity, voxelId, x, y, z, contactType, force, tick)` | -| `world.onFluidEnter(fn)` | `world.onFluidEnter(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, voxel, tick}`;Box3JS 传入 `(entity, fluid, x, y, z, tick)` | -| `world.onFluidLeave(fn)` | `world.onFluidLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | -| `world.onEntityCreate(fn)` | — | ❌ | | -| `world.onEntityDestroy(fn)` | — | ❌ | 实体层面有 `entity.onDestroy` 属性 | -| `world.onClick(fn)` | — | ❌ | Box3 鼠标点击事件,MC 无对应 | -| `world.onPress(fn)` | `world.onButtonPressed(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, button, raycast, tick, position, pressed}`;Box3JS 传入 `(entity, button, tick)` — 简化版 | -| `world.onRelease(fn)` | — | ❌ | 按键释放事件 | -| `world.onTakeDamage(fn)` | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damage, damageType, tick}`;Box3JS 传入 `(entity, amount, source, attacker, tick)` — 展开参数 | -| `world.onDie(fn)` | `world.onEntityDeath(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damageType, tick}`;Box3JS 传入 `(entity, killer, tick)`,killer 可能为 null | -| `world.onRespawn(fn)` | `world.onPlayerRespawn(fn)` → GameEventHandlerToken | ⚠️ | 命名不同(Respawn → PlayerRespawn),Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | -| `world.onPlayerPurchaseSuccess(fn)` | — | ❌ | MC 无商城系统 | -| — | `world.onVoxelDestroy(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | -| — | `world.onBlockPlace(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, voxelId, tick)` | -| — | `world.onBlockActivate(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | -| — | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, amount, source, attacker, tick)` | -| — | `world.onMessage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。跨脚本消息 | +| Box3 API | Box3JS 实现 | 状态 | 回调签名差异 | +| ----------------------------------- | ---------------------------------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------- | +| `world.onTick(fn)` | `world.onTick(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{tick, prevTick, elapsedTimeMS, skip}`;Box3JS 已对齐传入 NativeObject | +| `world.onPlayerJoin(fn)` | `world.onPlayerJoin(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | +| `world.onPlayerLeave(fn)` | `world.onPlayerLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | +| `world.onChat(fn)` | `world.onChat(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, message, tick}`;Box3JS 传入 `(entity, message, tick)` — 展开参数 | +| `world.onInteract(fn)` | `world.onInteract(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, targetEntity, tick}`;Box3JS 传入 `(entity, target, tick)` — 展开参数 | +| `world.onEntityContact(fn)` | `world.onEntityContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, other, tick}`;Box3JS 传入 `(entity, other, tick)` — 缺少 axis/force | +| `world.onEntitySeparate(fn)` | `world.onEntitySeparate(fn)` → GameEventHandlerToken | ✅ | 同上 | +| `world.onVoxelContact(fn)` | `world.onVoxelContact(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{axis, entity, force, voxel, tick, x, y, z}`;Box3JS 传入 `(entity, voxelId, x, y, z, contactType, force, tick)` | +| `world.onFluidEnter(fn)` | `world.onFluidEnter(fn)` → GameEventHandlerToken | ✅ | Box3 传入 `{entity, voxel, tick}`;Box3JS 传入 `(entity, fluid, x, y, z, tick)` | +| `world.onFluidLeave(fn)` | `world.onFluidLeave(fn)` → GameEventHandlerToken | ✅ | 同上 | +| `world.onEntityCreate(fn)` | — | ❌ | | +| `world.onEntityDestroy(fn)` | — | ❌ | 实体层面有 `entity.onDestroy` 属性 | +| `world.onClick(fn)` | — | ❌ | Box3 鼠标点击事件,MC 无对应 | +| `world.onPress(fn)` | `world.onButtonPressed(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, button, raycast, tick, position, pressed}`;Box3JS 传入 `(entity, button, tick)` — 简化版 | +| `world.onRelease(fn)` | — | ❌ | 按键释放事件 | +| `world.onTakeDamage(fn)` | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damage, damageType, tick}`;Box3JS 传入 `(entity, amount, source, attacker, tick)` — 展开参数 | +| `world.onDie(fn)` | `world.onEntityDeath(fn)` → GameEventHandlerToken | ⚠️ | Box3 传入 `{entity, attacker, damageType, tick}`;Box3JS 传入 `(entity, killer, tick)`,killer 可能为 null | +| `world.onRespawn(fn)` | `world.onPlayerRespawn(fn)` → GameEventHandlerToken | ⚠️ | 命名不同(Respawn → PlayerRespawn),Box3 传入 `{entity, tick}`;Box3JS 传入 `(entity, tick)` | +| `world.onPlayerPurchaseSuccess(fn)` | — | ❌ | MC 无商城系统 | +| — | `world.onVoxelDestroy(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | +| — | `world.onBlockPlace(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, voxelId, tick)` | +| — | `world.onBlockActivate(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, x, y, z, voxel, tick)` | +| — | `world.onEntityDamage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。`(entity, amount, source, attacker, tick)` | +| — | `world.onMessage(fn)` → GameEventHandlerToken | ⬆ | MC 扩展。跨脚本消息 | **关键差异总结**: + 1. 所有事件注册**已返回 GameEventHandlerToken**,支持 `.cancel()` 和 `.active()` 2. Box3 事件回调接收**事件对象**,Box3JS 回调接收**展开的参数列表**(JS 展开参数风格) 3. `onTick` 已对齐传入 `{tick, prevTick, elapsedTimeMS, skip}`;`onPlayerJoin/Leave/Respawn` 已添加 tick 参数 @@ -198,235 +183,231 @@ Zone 可以设置局部天气/光照/力场参数,这在 MC 服务端无法实 ### 1.13 音效 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.ambientSound` (属性) | `world.ambientSound` (属性) | ✅ | 一致。存储音效路径字符串 | -| `world.playerJoinSound` (属性) | `world.playerJoinSound` (属性) | ✅ | 一致 | -| `world.playerLeaveSound` (属性) | `world.playerLeaveSound` (属性) | ✅ | 一致 | -| `world.placeVoxelSound` (属性) | `world.placeVoxelSound` (属性) | ✅ | 一致 | -| `world.breakVoxelSound` (属性) | `world.breakVoxelSound` (属性) | ✅ | 一致 | -| `world.sound(config)` | `world.sound(config)` | ✅ | 一致。接受 String 路径或 `{path, position, volume, pitch}` 对象。内部委托 playSound | -| — | `world.playSound(path, x, y, z, volume, pitch)` | ⬆ | MC 扩展。展开参数版本 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ----------------------------------------------- | ---- | ----------------------------------------------------------------------------------- | +| `world.ambientSound` (属性) | `world.ambientSound` (属性) | ✅ | 一致。存储音效路径字符串 | +| `world.playerJoinSound` (属性) | `world.playerJoinSound` (属性) | ✅ | 一致 | +| `world.playerLeaveSound` (属性) | `world.playerLeaveSound` (属性) | ✅ | 一致 | +| `world.placeVoxelSound` (属性) | `world.placeVoxelSound` (属性) | ✅ | 一致 | +| `world.breakVoxelSound` (属性) | `world.breakVoxelSound` (属性) | ✅ | 一致 | +| `world.sound(config)` | `world.sound(config)` | ✅ | 一致。接受 String 路径或 `{path, position, volume, pitch}` 对象。内部委托 playSound | +| — | `world.playSound(path, x, y, z, volume, pitch)` | ⬆ | MC 扩展。展开参数版本 | ### 1.14 计时器 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.setTimeout(fn, ticks)` | `world.setTimeout(fn, ticks)` | ✅ | 一致 | -| `world.setInterval(fn, ticks)` | `world.setInterval(fn, ticks)` | ✅ | 一致 | -| `world.clearTimeout(id)` | `world.clearTimeout(id)` | ✅ | 一致 | -| `world.clearInterval(id)` | `world.clearInterval(id)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------ | ------------------------ | ---- | -------- | +| `setTimeout(fn, ticks)` | `setTimeout(fn, ticks)` | ✅ | 一致 | +| `setInterval(fn, ticks)` | `setInterval(fn, ticks)` | ✅ | 一致 | -**注意**: Box3 的 setTimeout/setInterval 在官方文档中标注为 ,但 Box3JS 在 `world` 全局对象上直接提供,用法一致。 +**注意**: Box3 的 setTimeout/setInterval 在官方文档中标注为 `world.xxx`,但 Box3JS 以全局函数直接提供(返回 `GameEventHandlerToken`,调用 `.cancel()` 取消)。 ### 1.15 视觉效果 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.strikeLightning(x, y, z)` | `world.strikeLightning(x, y, z)` | ✅ | 一致。额外支持 damage 参数 | -| `world.strikeLightning(pos)` | `world.strikeLightning(pos)` | ✅ | GameVector3 重载 | -| — | `world.strikeLightning(x, y, z, damage)` | ⬆ | 传自定义伤害值 | -| `world.launchFirework(x, y, z, color, shape)` | `world.launchFirework(x, y, z, color, shape)` | ✅ | 颜色和形状枚举值一致 | -| `world.launchFirework(pos, color, shape)` | `world.launchFirework(pos, color, shape)` | ✅ | GameVector3 重载 | -| `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | ✅ | 一致 | -| `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | ✅ | GameVector3 重载 | -| — | `world.spawnParticleCircle(x, y, z, radius, type, count)` | ⬆ | MC 扩展。圆形粒子圈 | -| — | `world.spawnParticleCircle(pos, radius, type, count)` | ⬆ | GameVector3 重载 | -| — | `world.spawnParticle(x, y, z, color, count, dx, dy, dz, speed)` | ⬆ | MC 扩展。使用 GameRGBColor 生成彩色 dust 粒子 | -| — | `world.spawnParticle(pos, color, count, dx, dy, dz, speed)` | ⬆ | GameVector3 + GameRGBColor 重载 | -| — | `world.launchFirework(x, y, z, colors, shape)` | ⬆ | MC 扩展。colors 为 GameRGBColor[] 数组 | -| — | `world.launchFirework(pos, colors, shape)` | ⬆ | GameVector3 + GameRGBColor[] 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------------------------------- | --------------------------------------------------------------- | ---- | --------------------------------------------- | +| `world.strikeLightning(x, y, z)` | `world.strikeLightning(x, y, z)` | ✅ | 一致。额外支持 damage 参数 | +| `world.strikeLightning(pos)` | `world.strikeLightning(pos)` | ✅ | GameVector3 重载 | +| — | `world.strikeLightning(x, y, z, damage)` | ⬆ | 传自定义伤害值 | +| `world.launchFirework(x, y, z, color, shape)` | `world.launchFirework(x, y, z, color, shape)` | ✅ | 颜色和形状枚举值一致 | +| `world.launchFirework(pos, color, shape)` | `world.launchFirework(pos, color, shape)` | ✅ | GameVector3 重载 | +| `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | `world.spawnParticle(type, x, y, z, count, dx, dy, dz, speed)` | ✅ | 一致 | +| `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | `world.spawnParticle(type, pos, count, dx, dy, dz, speed)` | ✅ | GameVector3 重载 | +| — | `world.spawnParticleCircle(x, y, z, radius, type, count)` | ⬆ | MC 扩展。圆形粒子圈 | +| — | `world.spawnParticleCircle(pos, radius, type, count)` | ⬆ | GameVector3 重载 | +| — | `world.spawnParticle(x, y, z, color, count, dx, dy, dz, speed)` | ⬆ | MC 扩展。使用 GameRGBColor 生成彩色 dust 粒子 | +| — | `world.spawnParticle(pos, color, count, dx, dy, dz, speed)` | ⬆ | GameVector3 + GameRGBColor 重载 | +| — | `world.launchFirework(x, y, z, colors, shape)` | ⬆ | MC 扩展。colors 为 GameRGBColor[] 数组 | +| — | `world.launchFirework(pos, colors, shape)` | ⬆ | GameVector3 + GameRGBColor[] 重载 | ### 1.16 物品与抛射物 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.dropItem(x, y, z, itemId, count)` | `world.dropItem(x, y, z, itemId, count)` | ✅ | 一致 | -| `world.dropItem(pos, itemId, count)` | `world.dropItem(pos, itemId, count)` | ✅ | GameVector3 重载 | -| `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | ✅ | 一致 | -| `world.launchProjectile(type, pos, target, speed)` | `world.launchProjectile(type, pos, target, speed)` | ✅ | GameVector3 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---- | ---------------- | +| `world.dropItem(x, y, z, itemId, count)` | `world.dropItem(x, y, z, itemId, count)` | ✅ | 一致 | +| `world.dropItem(pos, itemId, count)` | `world.dropItem(pos, itemId, count)` | ✅ | GameVector3 重载 | +| `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | `world.launchProjectile(type, x, y, z, tx, ty, tz, speed)` | ✅ | 一致 | +| `world.launchProjectile(type, pos, target, speed)` | `world.launchProjectile(type, pos, target, speed)` | ✅ | GameVector3 重载 | ### 1.17 爆炸 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.explode(x, y, z, power)` | `world.explode(x, y, z, power)` | ✅ | 一致 | -| `world.explode(pos, power)` | `world.explode(pos, power)` | ✅ | GameVector3 重载 | -| — | `world.explode(x, y, z, power, fire)` | ⬆ | MC 扩展。fire=true 可引燃方块 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ------------------------------------- | ---- | ----------------------------- | +| `world.explode(x, y, z, power)` | `world.explode(x, y, z, power)` | ✅ | 一致 | +| `world.explode(pos, power)` | `world.explode(pos, power)` | ✅ | GameVector3 重载 | +| — | `world.explode(x, y, z, power, fire)` | ⬆ | MC 扩展。fire=true 可引燃方块 | ### 1.18 动画 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.animate(keyframes, playback?)` | — | ❌ | 世界级关键帧动画 | -| `world.getAnimations()` | — | ❌ | | -| `world.getEntityAnimations()` | — | ❌ | | -| `world.getPlayerAnimations()` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------- | ----------- | ---- | ---------------- | +| `world.animate(keyframes, playback?)` | — | ❌ | 世界级关键帧动画 | +| `world.getAnimations()` | — | ❌ | | +| `world.getEntityAnimations()` | — | ❌ | | +| `world.getPlayerAnimations()` | — | ❌ | | ### 1.19 传送 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `world.teleport(mapId, players, serverId?)` | — | ❌ | 地图组间传送。MC 无地图组概念 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------- | ----------- | ---- | ----------------------------- | +| `world.teleport(mapId, players, serverId?)` | — | ❌ | 地图组间传送。MC 无地图组概念 | ### 1.20 跨脚本消息 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| — | `world.sendMessage(target, data)` | ⬆ | MC 扩展。target 为 `"*"` 或项目名 | -| — | `world.onMessage(fn)` | ⬆ | MC 扩展。回调 `(from, data)` | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------- | --------------------------------- | ---- | --------------------------------- | +| — | `world.sendMessage(target, data)` | ⬆ | MC 扩展。target 为 `"*"` 或项目名 | +| — | `world.onMessage(fn)` | ⬆ | MC 扩展。回调 `(from, data)` | ### 1.21 控制台命令 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| — | `world.runCommand(cmd)` | ⬆ | MC 扩展。以控制台身份执行命令 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------- | ----------------------- | ---- | ----------------------------- | +| — | `world.runCommand(cmd)` | ⬆ | MC 扩展。以控制台身份执行命令 | ### 1.22 记分板 (全部 MC 扩展) Box3 无记分板系统。 -| Box3JS API | 说明 | -|------------|------| -| `world.addScoreboard(name)` | 创建 dummy 类型记分项 | -| `world.addScoreboard(name, criteria)` | 指定标准(dummy/deathCount 等) | -| `world.removeScoreboard(name)` | 删除记分项 | -| `world.setScore(entityOrName, objective, value)` | 设置分数 | -| `world.getScore(entityOrName, objective)` | 获取分数 | -| `world.showScoreboard(slot, objective)` | 在 sidebar/list/belowname 显示 | -| `world.hideScoreboard(slot)` | 隐藏槽位 | -| `world.listScores(objective)` | 列出所有条目 | +| Box3JS API | 说明 | +| ------------------------------------------------ | ------------------------------- | +| `world.addScoreboard(name)` | 创建 dummy 类型记分项 | +| `world.addScoreboard(name, criteria)` | 指定标准(dummy/deathCount 等) | +| `world.removeScoreboard(name)` | 删除记分项 | +| `world.setScore(entityOrName, objective, value)` | 设置分数 | +| `world.getScore(entityOrName, objective)` | 获取分数 | +| `world.showScoreboard(slot, objective)` | 在 sidebar/list/belowname 显示 | +| `world.hideScoreboard(slot)` | 隐藏槽位 | +| `world.listScores(objective)` | 列出所有条目 | ### 1.23 Boss 血条 (全部 MC 扩展) Box3 无 Bossbar 系统。 -| Box3JS API | 说明 | -|------------|------| +| Box3JS API | 说明 | +| ------------------------------------------------ | -------------- | | `world.showBossbar(name, text, progress, color)` | 显示或更新血条 | -| `world.removeBossbar(name)` | 移除血条 | +| `world.removeBossbar(name)` | 移除血条 | ### 1.24 队伍 (全部 MC 扩展) Box3 无队伍系统。 -| Box3JS API | 说明 | -|------------|------| -| `world.createTeam(name, color)` | 创建队伍 | -| `world.removeTeam(name)` | 删除队伍 | +| Box3JS API | 说明 | +| ---------------------------------- | -------- | +| `world.createTeam(name, color)` | 创建队伍 | +| `world.removeTeam(name)` | 删除队伍 | | `world.joinTeam(entity, teamName)` | 加入队伍 | -| `world.leaveTeam(entity)` | 离开队伍 | -| `world.getTeamOf(entity)` | 查询队伍 | +| `world.leaveTeam(entity)` | 离开队伍 | +| `world.getTeamOf(entity)` | 查询队伍 | ### 1.25 世界边界 (全部 MC 扩展) Box3 无世界边界概念。 -| Box3JS API | 说明 | -|------------|------| -| `world.borderSize` (属性) | 获取/设置边界大小 | -| `world.setBorderCenter(x, z)` | 设置边界中心 | -| `world.shrinkBorder(targetSize, seconds)` | 平滑缩圈 | -| `world.setBorderDamage(damage)` | 边界外伤害值 | -| `world.setBorderWarning(blocks)` | 警告距离 | +| Box3JS API | 说明 | +| ----------------------------------------- | ----------------- | +| `world.borderSize` (属性) | 获取/设置边界大小 | +| `world.setBorderCenter(x, z)` | 设置边界中心 | +| `world.shrinkBorder(targetSize, seconds)` | 平滑缩圈 | +| `world.setBorderDamage(damage)` | 边界外伤害值 | +| `world.setBorderWarning(blocks)` | 警告距离 | ### 1.26 合成管理 (全部 MC 扩展) -| Box3JS API | 说明 | -|------------|------| -| `world.listRecipes(filter)` | 搜索匹配 filter 的合成配方 ID 列表 | -| `world.removeRecipe(recipeId)` | 禁用指定配方(加入黑名单) | -| `world.clearRecipes()` | 清除黑名单,恢复所有原始配方 | +| Box3JS API | 说明 | +| ------------------------------ | ---------------------------------- | +| `world.listRecipes(filter)` | 搜索匹配 filter 的合成配方 ID 列表 | +| `world.removeRecipe(recipeId)` | 禁用指定配方(加入黑名单) | +| `world.clearRecipes()` | 清除黑名单,恢复所有原始配方 | ### 1.28 结构与成就 (全部 MC 扩展) -| Box3JS API | 说明 | -|------------|------| -| `world.placeStructure(x, y, z, structureId)` | 在指定位置放置数据包结构模板 (NBT) | -| `world.placeStructure(pos, structureId)` | GameVector3 重载 | -| `world.grantAdvancement(playerName, advancementId)` | 按玩家名称授予进度 | +| Box3JS API | 说明 | +| --------------------------------------------------- | ---------------------------------- | +| `world.placeStructure(x, y, z, structureId)` | 在指定位置放置数据包结构模板 (NBT) | +| `world.placeStructure(pos, structureId)` | GameVector3 重载 | +| `world.grantAdvancement(playerName, advancementId)` | 按玩家名称授予进度 | ### 1.29 数据库 (db 全局对象) Box3 平台无独立数据库 API。Box3JS 通过 `db` 全局对象提供 SQLite 能力。 -| Box3JS API | 说明 | -|------------|------| -| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | -| `db.sql\`SELECT * FROM t WHERE id = ${id}\`` | Tagged template 语法,自动参数化 | +| Box3JS API | 说明 | +| --------------------------------------------- | ------------------------------------------ | +| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | +| `db.sql\`SELECT \* FROM t WHERE id = ${id}\`` | Tagged template 语法,自动参数化 | **GameQueryResult**: `rows` (数组), `firstRow`, `columnNames`, `columnCount`, `rowCount`, `affectedRows`, `isQuery` **GameQueryResult 方法**: `next()`, `reset()`, `then(resolve, reject?)` 每个脚本项目拥有独立的数据库文件 `config/box3/data/.db`。 ---- - ## 2. GameEntity (entity) ### 2.1 身份标识 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.id` (只读属性) | `entity.id` (只读属性) | ✅ | 一致。返回 UUID 字符串 | -| `entity.isPlayer` (只读属性) | `entity.isPlayer()` (方法) | ⚠️ | Box3 为属性,Box3JS 为方法 | -| `entity.player` (只读属性) | `entity.player` (只读属性) | ✅ | 一致。非玩家返回 null | -| `entity.entityType` (只读属性) | `entity.entityType` (只读属性) | ✅ | 一致。返回命名空间 ID(如 "minecraft:zombie") | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | ---------------------------------------------- | +| `entity.id` (只读属性) | `entity.id` (只读属性) | ✅ | 一致。返回 UUID 字符串 | +| `entity.isPlayer` (只读属性) | `entity.isPlayer()` (方法) | ⚠️ | Box3 为属性,Box3JS 为方法 | +| `entity.player` (只读属性) | `entity.player` (只读属性) | ✅ | 一致。非玩家返回 null | +| `entity.entityType` (只读属性) | `entity.entityType` (只读属性) | ✅ | 一致。返回命名空间 ID(如 "minecraft:zombie") | ### 2.2 标签 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.addTag(tag)` | `entity.addTag(tag)` | ✅ | 一致 | -| `entity.hasTag(tag)` | `entity.hasTag(tag)` | ✅ | 一致 | -| `entity.removeTag(tag)` | `entity.removeTag(tag)` | ✅ | 一致 | -| `entity.tags()` (返回数组) | `entity.tags()` | ✅ | 一致。返回所有标签的字符串数组 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------- | ----------------------- | ---- | ------------------------------ | +| `entity.addTag(tag)` | `entity.addTag(tag)` | ✅ | 一致 | +| `entity.hasTag(tag)` | `entity.hasTag(tag)` | ✅ | 一致 | +| `entity.removeTag(tag)` | `entity.removeTag(tag)` | ✅ | 一致 | +| `entity.tags()` (返回数组) | `entity.tags()` | ✅ | 一致。返回所有标签的字符串数组 | ### 2.3 位置/速度/包围盒 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.position` (属性) | `entity.position` (属性) | ✅ | 一致。LiveVec3 — 赋值即传送 | -| `entity.velocity` (属性) | `entity.velocity` (属性) | ✅ | 一致。赋值即设置速度 | -| `entity.bounds` (只读属性) | `entity.bounds` (只读属性) | ✅ | 一致。返回半尺寸 (half-extents) | -| — | `entity.eyePosition` (只读属性) | ⬆ | MC 扩展。返回眼睛位置的 GameVector3 | -| — | `entity.onGround` (只读属性) | ⬆ | MC 扩展。是否着地 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------- | ------------------------------- | ---- | ----------------------------------- | +| `entity.position` (属性) | `entity.position` (属性) | ✅ | 一致。LiveVec3 — 赋值即传送 | +| `entity.velocity` (属性) | `entity.velocity` (属性) | ✅ | 一致。赋值即设置速度 | +| `entity.bounds` (只读属性) | `entity.bounds` (只读属性) | ✅ | 一致。返回半尺寸 (half-extents) | +| — | `entity.eyePosition` (只读属性) | ⬆ | MC 扩展。返回眼睛位置的 GameVector3 | +| — | `entity.onGround` (只读属性) | ⬆ | MC 扩展。是否着地 | ### 2.4 外观 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.mesh` | — | ❌ | Box3 自定义模型 | -| `entity.meshColor` | — | ❌ | | -| `entity.meshScale` | — | ❌ | | -| `entity.meshOrientation` | — | ❌ | | -| `entity.meshMetalness` | — | ❌ | | -| `entity.meshEmissive` | — | ❌ | | -| `entity.meshShininess` | — | ❌ | | -| `entity.anchor` | — | ❌ | | -| `entity.anchorOffset` | — | ❌ | | -| `entity.meshInvisible` (属性) | `entity.meshInvisible` (属性) | ✅ | 一致。底层设置 MC 隐身 | -| — | `entity.nameTag` (属性) | ⬆ | MC 扩展。获取/设置实体名牌 | -| — | `entity.glowing` (属性) | ⬆ | MC 扩展。获取/设置发光效果 | -| — | `entity.invulnerable` (属性) | ⬆ | MC 扩展。获取/设置无敌状态 | -| — | `entity.fire` (setFire/clearFire) | ⬆ | MC 扩展。设置/清除着火 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | --------------------------------- | ---- | -------------------------- | +| `entity.mesh` | — | ❌ | Box3 自定义模型 | +| `entity.meshColor` | — | ❌ | | +| `entity.meshScale` | — | ❌ | | +| `entity.meshOrientation` | — | ❌ | | +| `entity.meshMetalness` | — | ❌ | | +| `entity.meshEmissive` | — | ❌ | | +| `entity.meshShininess` | — | ❌ | | +| `entity.anchor` | — | ❌ | | +| `entity.anchorOffset` | — | ❌ | | +| `entity.meshInvisible` (属性) | `entity.meshInvisible` (属性) | ✅ | 一致。底层设置 MC 隐身 | +| — | `entity.nameTag` (属性) | ⬆ | MC 扩展。获取/设置实体名牌 | +| — | `entity.glowing` (属性) | ⬆ | MC 扩展。获取/设置发光效果 | +| — | `entity.invulnerable` (属性) | ⬆ | MC 扩展。获取/设置无敌状态 | +| — | `entity.fire` (setFire/clearFire) | ⬆ | MC 扩展。设置/清除着火 | ### 2.5 物理属性 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.collides` (属性) | `entity.collides` (属性) | ✅ | 一致。false 时对 LivingEntity 设置 noGravity | -| `entity.fixed` (属性) | `entity.fixed` (属性) | ✅ | 一致。true 时设置 noGravity 并锁定位置 | -| `entity.friction` (属性) | `entity.friction` (属性) | ✅ | 一致。存储为自定义属性(脚本自行利用) | -| `entity.gravity` (属性) | `entity.gravity` (属性) | ✅ | 一致。false 时设置 noGravity(true) | -| `entity.mass` (属性) | `entity.mass` (属性) | ✅ | 一致。存储为自定义属性,默认 1.0 | -| `entity.restitution` (属性) | `entity.restitution` (属性) | ✅ | 一致。存储为自定义属性,默认 0.0 | -| `entity.contactForce` | — | ❌ | | -| `entity.entityContacts` (只读) | — | ❌ | | -| `entity.voxelContacts` (只读) | — | ❌ | | -| `entity.fluidContacts` (只读) | — | ❌ | | -| `entity.useOBB` | — | ❌ | | -| `entity.ignoreEntityGravity` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | --------------------------- | ---- | -------------------------------------------- | +| `entity.collides` (属性) | `entity.collides` (属性) | ✅ | 一致。false 时对 LivingEntity 设置 noGravity | +| `entity.fixed` (属性) | `entity.fixed` (属性) | ✅ | 一致。true 时设置 noGravity 并锁定位置 | +| `entity.friction` (属性) | `entity.friction` (属性) | ✅ | 一致。存储为自定义属性(脚本自行利用) | +| `entity.gravity` (属性) | `entity.gravity` (属性) | ✅ | 一致。false 时设置 noGravity(true) | +| `entity.mass` (属性) | `entity.mass` (属性) | ✅ | 一致。存储为自定义属性,默认 1.0 | +| `entity.restitution` (属性) | `entity.restitution` (属性) | ✅ | 一致。存储为自定义属性,默认 0.0 | +| `entity.contactForce` | — | ❌ | | +| `entity.entityContacts` (只读) | — | ❌ | | +| `entity.voxelContacts` (只读) | — | ❌ | | +| `entity.fluidContacts` (只读) | — | ❌ | | +| `entity.useOBB` | — | ❌ | | +| `entity.ignoreEntityGravity` | — | ❌ | | **注意**: MC 物理由原版引擎控制,物理属性(friction/mass/restitution)存储为自定义属性供脚本读取,不会改变原版物理行为。collides/fixed/gravity 有对应 MC 副作用(noGravity)。 @@ -434,324 +415,343 @@ Box3 平台无独立数据库 API。Box3JS 通过 `db` 全局对象提供 SQLite Box3 实体可独立发射粒子,Box3JS 完全未实现实体级粒子: -| Box3 API | 状态 | -|----------|------| -| `entity.particleRate` | ❌ | -| `entity.particleRateSpread` | ❌ | -| `entity.particleLimit` | ❌ | -| `entity.particleLifetime` | ❌ | -| `entity.particleLifetimeSpread` | ❌ | -| `entity.particleSize` | ❌ | -| `entity.particleSizeSpread` | ❌ | -| `entity.particleColor` | ❌ | -| `entity.particleVelocity` | ❌ | -| `entity.particleVelocitySpread` | ❌ | -| `entity.particleDamping` | ❌ | -| `entity.particleAcceleration` | ❌ | -| `entity.particleNoise` | ❌ | -| `entity.particleNoiseFrequency` | ❌ | -| `entity.particleTarget` | ❌ | -| `entity.particleTargetOffset` | ❌ | +| Box3 API | 状态 | +| ------------------------------- | ---- | +| `entity.particleRate` | ❌ | +| `entity.particleRateSpread` | ❌ | +| `entity.particleLimit` | ❌ | +| `entity.particleLifetime` | ❌ | +| `entity.particleLifetimeSpread` | ❌ | +| `entity.particleSize` | ❌ | +| `entity.particleSizeSpread` | ❌ | +| `entity.particleColor` | ❌ | +| `entity.particleVelocity` | ❌ | +| `entity.particleVelocitySpread` | ❌ | +| `entity.particleDamping` | ❌ | +| `entity.particleAcceleration` | ❌ | +| `entity.particleNoise` | ❌ | +| `entity.particleNoiseFrequency` | ❌ | +| `entity.particleTarget` | ❌ | +| `entity.particleTargetOffset` | ❌ | ### 2.7 音效 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.chatSound` | — | ❌ | | -| `entity.hurtSound` | — | ❌ | | -| `entity.dieSound` | — | ❌ | | -| `entity.interactSound` | — | ❌ | | -| `entity.sound(config)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------- | ----------- | ---- | -------- | +| `entity.chatSound` | — | ❌ | | +| `entity.hurtSound` | — | ❌ | | +| `entity.dieSound` | — | ❌ | | +| `entity.interactSound` | — | ❌ | | +| `entity.sound(config)` | — | ❌ | | ### 2.8 交互 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.enableInteract` | — | ❌ | | -| `entity.interactRadius` | — | ❌ | | -| `entity.interactHint` | — | ❌ | | -| `entity.interactColor` | — | ❌ | | -| `entity.say(message)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------- | ----------- | ---- | -------- | +| `entity.enableInteract` | — | ❌ | | +| `entity.interactRadius` | — | ❌ | | +| `entity.interactHint` | — | ❌ | | +| `entity.interactColor` | — | ❌ | | +| `entity.say(message)` | — | ❌ | | ### 2.9 战斗/生命 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.destroyed` (只读属性) | `entity.destroyed` (只读属性) | ✅ | 一致 | -| `entity.hp` (属性) | `entity.hp` (属性) | ✅ | 一致 | -| `entity.maxHp` (属性) | `entity.maxHp` (属性) | ✅ | 一致 | -| `entity.enableDamage` | — | ❌ | | -| `entity.showHealthBar` | — | ❌ | | -| `entity.hurt(amount)` | `entity.hurt(amount)` | ✅ | 一致 | -| — | `entity.heal(amount)` | ⬆ | MC 扩展。治疗实体 | -| `entity.destroy()` | `entity.destroy()` | ✅ | 一致。触发 onDestroy 回调后移除。Box3 中 destroy() 调用后实体立即消失 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ----------------------------- | ---- | --------------------------------------------------------------------- | +| `entity.destroyed` (只读属性) | `entity.destroyed` (只读属性) | ✅ | 一致 | +| `entity.hp` (属性) | `entity.hp` (属性) | ✅ | 一致 | +| `entity.maxHp` (属性) | `entity.maxHp` (属性) | ✅ | 一致 | +| `entity.enableDamage` | — | ❌ | | +| `entity.showHealthBar` | — | ❌ | | +| `entity.hurt(amount)` | `entity.hurt(amount)` | ✅ | 一致 | +| — | `entity.heal(amount)` | ⬆ | MC 扩展。治疗实体 | +| `entity.destroy()` | `entity.destroy()` | ✅ | 一致。触发 onDestroy 回调后移除。Box3 中 destroy() 调用后实体立即消失 | ### 2.10 实体事件 Box3 的实体级事件非常丰富: -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.onClick(fn)` | — | ❌ | | -| `entity.onInteract(fn)` | — | ❌ | 可用 `world.onInteract` 替代 | -| `entity.onEntityContact(fn)` | — | ❌ | 可用 `world.onEntityContact` 替代 | -| `entity.onEntitySeparate(fn)` | — | ❌ | 可用 `world.onEntitySeparate` 替代 | -| `entity.onFluidEnter(fn)` | — | ❌ | 可用 `world.onFluidEnter` 替代 | -| `entity.onFluidLeave(fn)` | — | ❌ | 可用 `world.onFluidLeave` 替代 | -| `entity.onVoxelContact(fn)` | — | ❌ | 可用 `world.onVoxelContact` 替代 | -| `entity.onVoxelSeparate(fn)` | — | ❌ | | -| `entity.onDestroy(fn)` | `entity.onDestroy` (可赋值属性) | ⚠️ | Box3 为 `onDestroy(handler)` 方法;Box3JS 为可赋值属性 `entity.onDestroy = fn` | -| `entity.onTakeDamage(fn)` | — | ❌ | 可用 `world.onEntityDamage` 替代 | -| `entity.onDie(fn)` | — | ❌ | 可用 `world.onEntityDeath` 替代 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ------------------------------- | ---- | ------------------------------------------------------------------------------ | +| `entity.onClick(fn)` | — | ❌ | | +| `entity.onInteract(fn)` | — | ❌ | 可用 `world.onInteract` 替代 | +| `entity.onEntityContact(fn)` | — | ❌ | 可用 `world.onEntityContact` 替代 | +| `entity.onEntitySeparate(fn)` | — | ❌ | 可用 `world.onEntitySeparate` 替代 | +| `entity.onFluidEnter(fn)` | — | ❌ | 可用 `world.onFluidEnter` 替代 | +| `entity.onFluidLeave(fn)` | — | ❌ | 可用 `world.onFluidLeave` 替代 | +| `entity.onVoxelContact(fn)` | — | ❌ | 可用 `world.onVoxelContact` 替代 | +| `entity.onVoxelSeparate(fn)` | — | ❌ | | +| `entity.onDestroy(fn)` | `entity.onDestroy` (可赋值属性) | ⚠️ | Box3 为 `onDestroy(handler)` 方法;Box3JS 为可赋值属性 `entity.onDestroy = fn` | +| `entity.onTakeDamage(fn)` | — | ❌ | 可用 `world.onEntityDamage` 替代 | +| `entity.onDie(fn)` | — | ❌ | 可用 `world.onEntityDeath` 替代 | **注意**: 所有 `world.onXxx()` 事件回调注册后返回 `GameEventHandlerToken`,支持 `.cancel()` / `.active()`,与 Box3 一致。实体级事件 (`entity.onDestroy`) 暂不返回 token。 ### 2.11 动画 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.motion` (属性) | — | ❌ | Box3 的 GameMotionController | -| `entity.animate(keyframes, playback?)` | — | ❌ | | -| `entity.getAnimations()` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------------------- | ----------- | ---- | ---------------------------- | +| `entity.motion` (属性) | — | ❌ | Box3 的 GameMotionController | +| `entity.animate(keyframes, playback?)` | — | ❌ | | +| `entity.getAnimations()` | — | ❌ | | ### 2.12 变换方法 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `entity.lookAt(target)` | `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | ✅ | 一致。Box3 接受多种目标类型,Box3JS 接受坐标或 GameVector3 | -| `entity.rotateLocal(quat)` | — | ❌ | 局部旋转 | -| `entity.scaleLocal(vec)` | — | ❌ | 局部缩放 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| -------------------------- | ----------------------------------------------- | ---- | ---------------------------------------------------------- | +| `entity.lookAt(target)` | `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | ✅ | 一致。Box3 接受多种目标类型,Box3JS 接受坐标或 GameVector3 | +| `entity.rotateLocal(quat)` | — | ❌ | 局部旋转 | +| `entity.scaleLocal(vec)` | — | ❌ | 局部缩放 | ### 2.13 MC 独有扩展 (Entity) -| Box3JS API | 说明 | -|------------|------| -| `entity.navigateTo(x, y, z, speed)` / `entity.navigateTo(pos, speed)` | 生物寻路到目标位置 | -| `entity.setTarget(targetEntity)` | 设置生物攻击目标 | -| `entity.clearTarget()` | 清除攻击目标 | -| `entity.getTarget()` | 获取当前攻击目标 | -| `entity.setAI(enabled)` | 启用/禁用生物 AI | -| `entity.addEffect(effectId, duration, amplifier)` | 添加药水效果 | -| `entity.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | -| `entity.setEquipment(slot, itemId)` | 设置生物装备(mainhand/offhand/head/chest/legs/feet) | -| `entity.setDropChance(slot, chance)` / `entity.setDropChance("all", chance)` | 设置装备掉落概率 | -| `entity.setPersistent(true)` | 使生物持久化(不自然消失) | -| `entity.getAttribute(attributeId)` | 获取属性值(如 "minecraft:generic.attack_damage") | -| `entity.setAttribute(attributeId, value)` | 设置属性值 | -| `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | 使实体看向某位置 | - ---- +| Box3JS API | 说明 | +| ---------------------------------------------------------------------------- | ----------------------------------------------------- | +| `entity.navigateTo(x, y, z, speed)` / `entity.navigateTo(pos, speed)` | 生物寻路到目标位置 | +| `entity.setTarget(targetEntity)` | 设置生物攻击目标 | +| `entity.clearTarget()` | 清除攻击目标 | +| `entity.getTarget()` | 获取当前攻击目标 | +| `entity.setAI(enabled)` | 启用/禁用生物 AI | +| `entity.addEffect(effectId, duration, amplifier)` | 添加药水效果 | +| `entity.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | +| `entity.setEquipment(slot, itemId)` | 设置生物装备(mainhand/offhand/head/chest/legs/feet) | +| `entity.setDropChance(slot, chance)` / `entity.setDropChance("all", chance)` | 设置装备掉落概率 | +| `entity.setPersistent(true)` | 使生物持久化(不自然消失) | +| `entity.getAttribute(attributeId)` | 获取属性值(如 "minecraft:generic.attack_damage") | +| `entity.setAttribute(attributeId, value)` | 设置属性值 | +| `entity.lookAt(x, y, z)` / `entity.lookAt(pos)` | 使实体看向某位置 | ## 3. GamePlayerEntity (entity.player) ### 3.1 基础信息 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.name` (只读属性) | `player.name` (只读属性) | ✅ | 一致 | -| `player.userId` (只读属性) | `player.userId` (只读属性) | ✅ | Box3 返回用户数字 ID;Box3JS 返回 UUID 字符串 | -| `player.boxId` (只读属性) | — | ❌ | 已弃用的 Box ID | -| `player.userKey` (只读属性) | — | ❌ | 已弃用的用户密钥 | -| `player.avatar` (只读属性) | — | ❌ | 头像 URL | -| `player.movementBounds` (属性) | — | ❌ | | -| `player.url` (属性) | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | -------------------------- | ---- | --------------------------------------------- | +| `player.name` (只读属性) | `player.name` (只读属性) | ✅ | 一致 | +| `player.userId` (只读属性) | `player.userId` (只读属性) | ✅ | Box3 返回用户数字 ID;Box3JS 返回 UUID 字符串 | +| `player.boxId` (只读属性) | — | ❌ | 已弃用的 Box ID | +| `player.userKey` (只读属性) | — | ❌ | 已弃用的用户密钥 | +| `player.avatar` (只读属性) | — | ❌ | 头像 URL | +| `player.movementBounds` (属性) | — | ❌ | | +| `player.url` (属性) | — | ❌ | | ### 3.2 社交 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.querySocial(type)` | — | ❌ | -| `player.querySocialStatistic()` | — | ❌ | -| `player.openUserProfileDialog(userId)` | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| -------------------------------------- | ----------- | ---- | +| `player.querySocial(type)` | — | ❌ | +| `player.querySocialStatistic()` | — | ❌ | +| `player.openUserProfileDialog(userId)` | — | ❌ | ### 3.3 外观 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.color` | — | ❌ | | -| `player.emissive` | — | ❌ | | -| `player.invisible` (属性) | `player.invisible` (属性) | ✅ | 一致 | -| `player.showName` | — | ❌ | | -| `player.showIndicator` | — | ❌ | | -| `player.scale` (属性) | `player.scale` (只读) | ⚠️ | Box3JS 只读 | -| `player.metalness` | — | ❌ | | -| `player.shininess` | — | ❌ | | -| `player.skin` | — | ❌ | | -| `player.skinInvisible` | — | ❌ | | -| `player.setSkinByName(name)` | — | ❌ | | -| `player.resetToDefaultSkin()` | — | ❌ | | -| `player.clearSkin()` | — | ❌ | | -| `player.addWearable(config)` | — | ❌ | | -| `player.removeWearable(config)` | — | ❌ | | -| `player.wearables(bodyPart?)` | — | ❌ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ------------------------- | ---- | ----------- | +| `player.color` | — | ❌ | | +| `player.emissive` | — | ❌ | | +| `player.invisible` (属性) | `player.invisible` (属性) | ✅ | 一致 | +| `player.showName` | — | ❌ | | +| `player.showIndicator` | — | ❌ | | +| `player.scale` (属性) | `player.scale` (只读) | ⚠️ | Box3JS 只读 | +| `player.metalness` | — | ❌ | | +| `player.shininess` | — | ❌ | | +| `player.skin` | — | ❌ | | +| `player.skinInvisible` | — | ❌ | | +| `player.setSkinByName(name)` | — | ❌ | | +| `player.resetToDefaultSkin()` | — | ❌ | | +| `player.clearSkin()` | — | ❌ | | +| `player.addWearable(config)` | — | ❌ | | +| `player.removeWearable(config)` | — | ❌ | | +| `player.wearables(bodyPart?)` | — | ❌ | | ### 3.4 相机 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.cameraMode` (属性) | `player.cameraMode` (属性) | ⚠️ | Box3 支持 FIXED/FOLLOW/FPS/RELATIVE 四种模式;Box3JS 仅支持 FPS/FOLLOW | -| `player.cameraEntity` (属性) | `player.cameraEntity` (属性) | ⚠️ | Box3 可设为 null/实体;Box3JS 支持但设为非 null 时自动切换到 FOLLOW | -| `player.cameraPosition` (属性) | — | ❌ | FIXED/RELATIVE 模式用 | -| `player.cameraTarget` (属性) | `player.cameraTarget` (只读属性) | ⚠️ | Box3 可读写;Box3JS 只读,返回玩家视线方向 5 格处的点 | -| `player.cameraUp` (属性) | — | ❌ | | -| `player.cameraFovY` (属性) | — | ❌ | | -| `player.enable3DCursor` | — | ❌ | | -| `player.cameraFreezedAxis` | — | ❌ | | -| `player.freezedForwardDirection` | — | ❌ | | -| `player.cameraDistance` (属性) | — | ❌ | 相机到目标距离 | -| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | Box3JS 可写(`setCameraPitch` 方法效果) | -| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | Box3JS 可写 | -| `player.setCameraPitch(v)` | `player.setCameraPitch(v)` (作为属性 setter) | ✅ | 通过属性赋值实现 | -| `player.setCameraYaw(v)` | `player.setCameraYaw(v)` (作为属性 setter) | ✅ | 同上 | -| `player.facingDirection` (只读属性) | `player.facingDirection` (只读属性) | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | -------------------------------------------- | ---- | ---------------------------------------------------------------------- | +| `player.cameraMode` (属性) | `player.cameraMode` (属性) | ⚠️ | Box3 支持 FIXED/FOLLOW/FPS/RELATIVE 四种模式;Box3JS 仅支持 FPS/FOLLOW | +| `player.cameraEntity` (属性) | `player.cameraEntity` (属性) | ⚠️ | Box3 可设为 null/实体;Box3JS 支持但设为非 null 时自动切换到 FOLLOW | +| `player.cameraPosition` (属性) | — | ❌ | FIXED/RELATIVE 模式用 | +| `player.cameraTarget` (属性) | `player.cameraTarget` (只读属性) | ⚠️ | Box3 可读写;Box3JS 只读,返回玩家视线方向 5 格处的点 | +| `player.cameraUp` (属性) | — | ❌ | | +| `player.cameraFovY` (属性) | — | ❌ | | +| `player.enable3DCursor` | — | ❌ | | +| `player.cameraFreezedAxis` | — | ❌ | | +| `player.freezedForwardDirection` | — | ❌ | | +| `player.cameraDistance` (属性) | — | ❌ | 相机到目标距离 | +| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | Box3JS 可写(`setCameraPitch` 方法效果) | +| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | Box3JS 可写 | +| `player.setCameraPitch(v)` | `player.setCameraPitch(v)` (作为属性 setter) | ✅ | 通过属性赋值实现 | +| `player.setCameraYaw(v)` | `player.setCameraYaw(v)` (作为属性 setter) | ✅ | 同上 | +| `player.facingDirection` (只读属性) | `player.facingDirection` (只读属性) | ✅ | 一致 | ### 3.5 画面滤镜 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.colorLUT` (属性) | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| ------------------------ | ----------- | ---- | +| `player.colorLUT` (属性) | — | ❌ | ### 3.6 聊天/消息 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.directMessage(message)` | `player.directMessage(message)` | ✅ | 一致 | -| `player.dialog(config)` | `player.dialog(config)` | ⚠️ | **大幅简化**。Box3 支持 TEXT/INPUT/SELECT 三种类型 + 丰富样式配置 + 异步返回;Box3JS 仅发送系统消息并返回 `{index: 0, value: "OK"}`,不支持真正的交互对话框 | -| `player.cancelDialogs()` | — | ❌ | | -| `player.share(content)` | — | ❌ | | -| `player.onChat(handler)` | `player.onChat(handler)` | ✅ | Box3 传入 `{entity, message, tick}` 事件对象;Box3JS 直接展开参数 | -| — | `player.actionBar(message)` | ⬆ | MC 扩展。ActionBar 消息 | -| — | `player.title(title, subtitle)` | ⬆ | MC 扩展。标题/副标题 | -| — | `player.title(title, subtitle, fadeIn, stay, fadeOut)` | ⬆ | MC 扩展。带时间的标题 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | ------------------------------------------------------ | ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `player.directMessage(message)` | `player.directMessage(message)` | ✅ | 一致 | +| `player.dialog(config)` | `player.dialog(config)` | ⚠️ | **大幅简化**。Box3 支持 TEXT/INPUT/SELECT 三种类型 + 丰富样式配置 + 异步返回;Box3JS 仅发送系统消息并返回 `{index: 0, value: "OK"}`,不支持真正的交互对话框 | +| `player.cancelDialogs()` | — | ❌ | | +| `player.share(content)` | — | ❌ | | +| `player.onChat(handler)` | `player.onChat(handler)` | ✅ | Box3 传入 `{entity, message, tick}` 事件对象;Box3JS 直接展开参数 | +| — | `player.actionBar(message)` | ⬆ | MC 扩展。ActionBar 消息 | +| — | `player.title(title, subtitle)` | ⬆ | MC 扩展。标题/副标题 | +| — | `player.title(title, subtitle, fadeIn, stay, fadeOut)` | ⬆ | MC 扩展。带时间的标题 | ### 3.7 战斗/生命 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.dead` (只读属性) | `player.dead` (只读属性) | ✅ | 一致。返回 `player.isDeadOrDying()` | -| `player.spawnPoint` (属性) | `player.spawnPoint` (读写属性) | ✅ | 一致。可读可写,写入委托 setRespawnPoint | -| `player.forceRespawn()` | `player.respawn()` | ⚠️ | 方法名不同(forceRespawn → respawn) | -| `player.onRespawn(handler)` | — | ❌ | 可用 `world.onPlayerRespawn` 替代 | -| `player.hp` (属性) | `player.hp` (属性) | ✅ | 一致 | -| `player.maxHp` (属性) | `player.maxHp` (属性) | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| --------------------------- | ------------------------------ | ---- | ---------------------------------------- | +| `player.dead` (只读属性) | `player.dead` (只读属性) | ✅ | 一致。返回 `player.isDeadOrDying()` | +| `player.spawnPoint` (属性) | `player.spawnPoint` (读写属性) | ✅ | 一致。可读可写,写入委托 setRespawnPoint | +| `player.forceRespawn()` | `player.respawn()` | ⚠️ | 方法名不同(forceRespawn → respawn) | +| `player.onRespawn(handler)` | — | ❌ | 可用 `world.onPlayerRespawn` 替代 | +| `player.hp` (属性) | `player.hp` (属性) | ✅ | 一致 | +| `player.maxHp` (属性) | `player.maxHp` (属性) | ✅ | 一致 | ### 3.8 移动/输入 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.gamepad` | — | ❌ | 虚拟按键图片 | -| `player.disableInputDirection` | — | ❌ | | -| `player.enableAction0` | — | ❌ | | -| `player.enableAction1` | — | ❌ | | -| `player.action0Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION0 | -| `player.action1Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION1 | -| `player.jumpButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 JUMP | -| `player.walkButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 WALK | -| `player.swapInputDirection` | — | ❌ | | -| `player.reverseInputDirection` | — | ❌ | | -| `player.canFly` (属性) | `player.canFly` (属性) | ✅ | 一致 | -| — | `player.flying` (属性) | ⬆ | MC 扩展。当前是否在飞行 | -| `player.spectator` (只读属性) | `player.spectator` (只读属性) | ✅ | 一致 | -| — | `player.collision` (属性) | ⬆ | MC 扩展。实体碰撞开关 | -| `player.enableJump` (属性) | `player.enableJump` (属性) | ✅ | 一致。false 时保存并清零跳跃强度,true 时恢复 | -| `player.enableDoubleJump` | — | ❌ | 二段跳已移除 | -| `player.walkSpeed` (属性) | `player.walkSpeed` (属性) | ✅ | 一致 | -| `player.runSpeed` (属性) | `player.runSpeed` (属性) | ✅ | 一致 | -| `player.runAcceleration` | — | ❌ | | -| `player.jumpPower` (属性) | `player.jumpPower` (属性) | ✅ | 一致 | -| `player.jumpSpeedFactor` | — | ❌ | | -| `player.jumpAccelerationFactor` | — | ❌ | | -| `player.doubleJumpPower` | — | ❌ | | -| `player.crouchSpeed` (属性) | `player.crouchSpeed` (属性) | ✅ | 一致。存储为自定义属性 | -| `player.crouchAcceleration` | — | ❌ | | -| `player.flySpeed` (属性) | `player.flySpeed` (属性) | ✅ | 一致 | -| `player.flyAcceleration` | — | ❌ | | -| `player.swimAcceleration` | — | ❌ | | -| `player.swimSpeed` (属性) | `player.swimSpeed` (属性) | ✅ | 一致。映射到 WATER_MOVEMENT_EFFICIENCY 属性 | -| `player.walkAcceleration` | — | ❌ | | -| `player.moveState` (只读属性) | `player.moveState` (只读属性) | ✅ | 一致。枚举值: FLYING/GROUND/SWIM/FALL/JUMP | -| `player.walkState` (只读属性) | `player.walkState` (只读属性) | ✅ | 一致。枚举值: NONE/CROUCH/WALK/RUN | -| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | 如上 | -| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | 如上 | -| `player.kick()` | `player.kick()` / `player.kick(reason)` | ✅ | 一致。额外支持自定义踢出原因 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------- | --------------------------------------- | ---- | --------------------------------------------- | +| `player.gamepad` | — | ❌ | 虚拟按键图片 | +| `player.disableInputDirection` | — | ❌ | | +| `player.enableAction0` | — | ❌ | | +| `player.enableAction1` | — | ❌ | | +| `player.action0Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION0 | +| `player.action1Button` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 ACTION1 | +| `player.jumpButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 JUMP | +| `player.walkButton` (只读) | — | ❌ | 可通过 `world.onButtonPressed` 检测 WALK | +| `player.swapInputDirection` | — | ❌ | | +| `player.reverseInputDirection` | — | ❌ | | +| `player.canFly` (属性) | `player.canFly` (属性) | ✅ | 一致 | +| — | `player.flying` (属性) | ⬆ | MC 扩展。当前是否在飞行 | +| `player.spectator` (只读属性) | `player.spectator` (只读属性) | ✅ | 一致 | +| — | `player.collision` (属性) | ⬆ | MC 扩展。实体碰撞开关 | +| `player.enableJump` (属性) | `player.enableJump` (属性) | ✅ | 一致。false 时保存并清零跳跃强度,true 时恢复 | +| `player.enableDoubleJump` | — | ❌ | 二段跳已移除 | +| `player.walkSpeed` (属性) | `player.walkSpeed` (属性) | ✅ | 一致 | +| `player.runSpeed` (属性) | `player.runSpeed` (属性) | ✅ | 一致 | +| `player.runAcceleration` | — | ❌ | | +| `player.jumpPower` (属性) | `player.jumpPower` (属性) | ✅ | 一致 | +| `player.jumpSpeedFactor` | — | ❌ | | +| `player.jumpAccelerationFactor` | — | ❌ | | +| `player.doubleJumpPower` | — | ❌ | | +| `player.crouchSpeed` (属性) | `player.crouchSpeed` (属性) | ✅ | 一致。存储为自定义属性 | +| `player.crouchAcceleration` | — | ❌ | | +| `player.flySpeed` (属性) | `player.flySpeed` (属性) | ✅ | 一致 | +| `player.flyAcceleration` | — | ❌ | | +| `player.swimAcceleration` | — | ❌ | | +| `player.swimSpeed` (属性) | `player.swimSpeed` (属性) | ✅ | 一致。映射到 WATER_MOVEMENT_EFFICIENCY 属性 | +| `player.walkAcceleration` | — | ❌ | | +| `player.moveState` (只读属性) | `player.moveState` (只读属性) | ✅ | 一致。枚举值: FLYING/GROUND/SWIM/FALL/JUMP | +| `player.walkState` (只读属性) | `player.walkState` (只读属性) | ✅ | 一致。枚举值: NONE/CROUCH/WALK/RUN | +| `player.cameraPitch` (只读属性) | `player.cameraPitch` (读/写属性) | ✅ | 如上 | +| `player.cameraYaw` (只读属性) | `player.cameraYaw` (读/写属性) | ✅ | 如上 | +| `player.kick()` | `player.kick()` / `player.kick(reason)` | ✅ | 一致。额外支持自定义踢出原因 | ### 3.9 输入事件 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.onPress(handler)` | — | ❌ | 可用 `world.onButtonPressed` 替代 | -| `player.onRelease(handler)` | — | ❌ | | -| `player.onKeyDown(handler)` | — | ❌ | 键盘事件,MC 服务端无法获取 | -| `player.onKeyUp(handler)` | — | ❌ | | +Box3 的 `player.onPress/onRelease/onKeyDown/onKeyUp` 是客户端事件。Box3JS 通过客户端脚本引擎提供独立的 `input` 全局对象: + +| Box3 API | Box3JS 客户端实现 | 状态 | 差异说明 | +| --------------------------- | --------------------------------- | ---- | ------------------------------------------ | +| `player.onPress(handler)` | `world.onButtonPressed(handler)` | ⚠️ | 服务端可检测按钮点击;客户端用 `input` API | +| `player.onRelease(handler)` | — | ❌ | | +| `player.onKeyDown(handler)` | `input.onKeyPress(key, callback)` | ⚠️ | 客户端 `input` API,按键名如 `"f5"`、`"c"` | +| `player.onKeyUp(handler)` | — | ❌ | | + +**客户端 `input` 全局对象补充能力**: + +| Box3JS 客户端 API | 说明 | +| ----------------------------------- | ------------------------------------- | +| `input.onKeyPress(key, fn)` | 注册按键按下回调(如 `"f5"`) | +| `input.isKeyDown(key)` | 检查按键是否正在按住 | +| `input.onMouseClick(fn)` | 鼠标点击回调 `(button, action, x, y)` | +| `input.getMouseX()` / `getMouseY()` | 获取鼠标屏幕坐标 | ### 3.10 音效 -Box3 玩家有 14 种音效属性,Box3JS 仅提供播放方法: - -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.music` | — | ❌ | -| `player.action0Sound` | — | ❌ | -| `player.action1Sound` | — | ❌ | -| `player.crouchSound` | — | ❌ | -| `player.jumpSound` | — | ❌ | -| `player.doubleJumpSound` | — | ❌ | -| `player.landSound` | — | ❌ | -| `player.enterWaterSound` | — | ❌ | -| `player.leaveWaterSound` | — | ❌ | -| `player.swimSound` | — | ❌ | -| `player.spawnSound` | — | ❌ | -| `player.stepSound` | — | ❌ | -| `player.startFlySound` | — | ❌ | -| `player.stopFlySound` | — | ❌ | -| `player.sound(config)` | `player.playSound(path, volume, pitch)` | ⚠️ | Box3 接受完整 Sound 对象或路径;Box3JS 展开参数 | +Box3 玩家有 14 种音效属性(`music`/`jumpSound`/`landSound` 等),这些是客户端音效控制。Box3JS 通过客户端脚本引擎提供 `audio` 全局对象: + +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------ | --------------------------------------- | ---- | ----------------------------------------------- | +| `player.music` | `audio.playMusic(path, volume)` | ⚠️ | 客户端 `audio` API,通过背景音乐类别播放 | +| `player.action0Sound` | — | ❌ | | +| `player.action1Sound` | — | ❌ | | +| `player.crouchSound` | — | ❌ | | +| `player.jumpSound` | — | ❌ | | +| `player.doubleJumpSound` | — | ❌ | | +| `player.landSound` | — | ❌ | | +| `player.enterWaterSound` | — | ❌ | | +| `player.leaveWaterSound` | — | ❌ | | +| `player.swimSound` | — | ❌ | | +| `player.spawnSound` | — | ❌ | | +| `player.stepSound` | — | ❌ | | +| `player.startFlySound` | — | ❌ | | +| `player.stopFlySound` | — | ❌ | | +| `player.sound(config)` | `player.playSound(path, volume, pitch)` | ⚠️ | Box3 接受完整 Sound 对象或路径;Box3JS 展开参数 | + +**客户端 `audio` 全局对象补充能力**: + +| Box3JS 客户端 API | 说明 | +| -------------------------------------- | --------------------------------- | +| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | +| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | +| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | +| `audio.getVolume(category)` | 获取指定音频类别的音量 | +| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | + +> **注意**: Box3 的逐事件音效属性(如 `player.jumpSound`)需要 hook 每个游戏事件,Box3JS 目前提供的是主动播放 API。如需自动触发,可在 `client.onTick()` 中检测玩家状态变化后调用 `audio.playSound()`。 ### 3.11 链接/跳转 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `player.link(href, options?)` | `player.link(href)` | ⚠️ | Box3JS 简化版,无 isConfirm/isNewTab 选项。通过发送可点击的聊天组件实现 | -| `player.teleport(pos)` | `player.teleport(pos)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------- | ---------------------- | ---- | ----------------------------------------------------------------------- | +| `player.link(href, options?)` | `player.link(href)` | ⚠️ | Box3JS 简化版,无 isConfirm/isNewTab 选项。通过发送可点击的聊天组件实现 | +| `player.teleport(pos)` | `player.teleport(pos)` | ✅ | 一致 | ### 3.12 商城 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.openMarketplace(productIds)` | — | ❌ | -| `player.getMiaoShells()` | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| ------------------------------------ | ----------- | ---- | +| `player.openMarketplace(productIds)` | — | ❌ | +| `player.getMiaoShells()` | — | ❌ | ### 3.13 玩家动画 -| Box3 API | Box3JS 实现 | 状态 | -|----------|-------------|------| -| `player.animate(keyframes, playback?)` | — | ❌ | -| `player.getAnimations()` | — | ❌ | +| Box3 API | Box3JS 实现 | 状态 | +| -------------------------------------- | ----------- | ---- | +| `player.animate(keyframes, playback?)` | — | ❌ | +| `player.getAnimations()` | — | ❌ | ### 3.14 MC 独有扩展 (Player) -| Box3JS API | 说明 | -|------------|------| -| `player.opLevel` (属性) | 获取/设置玩家 OP 权限级别 | -| `player.gameMode` (属性) | 获取/设置游戏模式(survival/creative/adventure/spectator) | -| `player.dimension` (属性) | 获取/设置玩家所在维度(如 "minecraft:overworld") | -| `player.disableFly` (属性) | 禁用飞行(退出飞行状态并阻止重新开启) | -| `player.giveItem(itemId, count)` | 给予物品 | -| `player.giveEnchantedItem(itemId, count, enchants)` | 给予附魔物品 | -| `player.giveNamedItem(itemId, count, name, lore)` | 给予自定义名称/描述物品 | -| `player.getHeldItem()` | 获取手持物品 `{id, count}` | -| `player.clearInventory()` | 清空背包 | -| `player.addEffect(effectId, duration, amplifier)` | 添加药水效果 | -| `player.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | -| `player.clearEffects()` | 清除所有效果 | -| `player.xp` (属性) | 获取/设置经验等级 | -| `player.addExperienceLevels(levels)` | 增加经验等级 | -| `player.food` (属性) | 获取/设置饥饿值 | -| `player.saturation` (属性) | 获取/设置饱和值 | -| `player.runCommand(cmd)` | 以玩家身份执行命令 | -| `player.lookAt(x, y, z)` / `player.lookAt(pos)` | 使玩家看向某位置 | -| `player.setPlayerListName(name)` | 设置 TAB 列表显示名称 | - ---- +| Box3JS API | 说明 | +| ---------------------------------------------------------------- | ---------------------------------------------------------- | +| `player.opLevel` (属性) | 获取/设置玩家 OP 权限级别 | +| `player.gameMode` (属性) | 获取/设置游戏模式(survival/creative/adventure/spectator) | +| `player.dimension` (属性) | 获取/设置玩家所在维度(如 "minecraft:overworld") | +| `player.disableFly` (属性) | 禁用飞行(退出飞行状态并阻止重新开启) | +| `player.giveItem(itemId, count)` | 给予物品 | +| `player.giveEnchantedItem(itemId, count, enchants)` | 给予附魔物品 | +| `player.giveNamedItem(itemId, count, name, lore)` | 给予自定义名称/描述物品 | +| `player.getHeldItem()` | 获取手持物品 `{id, count}` | +| `player.clearInventory()` | 清空背包 | +| `player.addEffect(effectId, duration, amplifier)` | 添加药水效果 | +| `player.addEffect(effectId, duration, amplifier, hideParticles)` | 添加效果(可隐藏粒子) | +| `player.clearEffects()` | 清除所有效果 | +| `player.xp` (属性) | 获取/设置经验等级 | +| `player.addExperienceLevels(levels)` | 增加经验等级 | +| `player.food` (属性) | 获取/设置饥饿值 | +| `player.saturation` (属性) | 获取/设置饱和值 | +| `player.runCommand(cmd)` | 以玩家身份执行命令 | +| `player.lookAt(x, y, z)` / `player.lookAt(pos)` | 使玩家看向某位置 | +| `player.setPlayerListName(name)` | 设置 TAB 列表显示名称 | ## 4. GameVoxels (voxels) @@ -759,76 +759,74 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 ### 4.1 属性 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.shape` (只读属性) | `voxels.shape` (只读属性) | ✅ | 一致。返回世界最大尺寸 | -| `voxels.VoxelTypes` (只读属性) | `voxels.VoxelTypes` (只读属性) | ✅ | 一致。所有可用方块名称数组 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | -------------------------- | +| `voxels.shape` (只读属性) | `voxels.shape` (只读属性) | ✅ | 一致。返回世界最大尺寸 | +| `voxels.VoxelTypes` (只读属性) | `voxels.VoxelTypes` (只读属性) | ✅ | 一致。所有可用方块名称数组 | ### 4.2 名称/ID 映射 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.id(name)` | `voxels.id(name)` | ✅ | 一致。名称→数字 ID | -| `voxels.name(id)` | `voxels.name(id)` | ✅ | 一致。数字 ID→名称 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------- | ----------------- | ---- | ------------------ | +| `voxels.id(name)` | `voxels.id(name)` | ✅ | 一致。名称→数字 ID | +| `voxels.name(id)` | `voxels.name(id)` | ✅ | 一致。数字 ID→名称 | ### 4.3 写入 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.setVoxel(x, y, z, voxel)` | `voxels.setVoxel(x, y, z, voxel)` | ✅ | 一致 | -| `voxels.setVoxel(x, y, z, voxel, rotation)` | `voxels.setVoxel(x, y, z, voxel, rotation)` | ✅ | 一致。rotation 0-3 | -| `voxels.setVoxel(pos, voxel)` | `voxels.setVoxel(pos, voxel)` | ✅ | GameVector3 重载 | -| `voxels.setVoxel(pos, voxel, rotation)` | `voxels.setVoxel(pos, voxel, rotation)` | ✅ | GameVector3 重载 | -| `voxels.setVoxelId(x, y, z, voxel)` | `voxels.setVoxelId(x, y, z, voxel)` | ✅ | 一致。ID 包含旋转编码 | -| `voxels.setVoxelId(pos, voxel)` | `voxels.setVoxelId(pos, voxel)` | ✅ | GameVector3 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------- | ------------------------------------------- | ---- | --------------------- | +| `voxels.setVoxel(x, y, z, voxel)` | `voxels.setVoxel(x, y, z, voxel)` | ✅ | 一致 | +| `voxels.setVoxel(x, y, z, voxel, rotation)` | `voxels.setVoxel(x, y, z, voxel, rotation)` | ✅ | 一致。rotation 0-3 | +| `voxels.setVoxel(pos, voxel)` | `voxels.setVoxel(pos, voxel)` | ✅ | GameVector3 重载 | +| `voxels.setVoxel(pos, voxel, rotation)` | `voxels.setVoxel(pos, voxel, rotation)` | ✅ | GameVector3 重载 | +| `voxels.setVoxelId(x, y, z, voxel)` | `voxels.setVoxelId(x, y, z, voxel)` | ✅ | 一致。ID 包含旋转编码 | +| `voxels.setVoxelId(pos, voxel)` | `voxels.setVoxelId(pos, voxel)` | ✅ | GameVector3 重载 | **旋转编码方案一致**: `finalId = rotation * 16384 + baseId`,rotation 0=南(0°), 1=西(90°), 2=北(180°), 3=东(270°)。 ### 4.4 读取 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `voxels.getVoxel(x, y, z)` | `voxels.getVoxel(x, y, z)` | ✅ | 一致。返回基础 ID(无旋转) | -| `voxels.getVoxel(pos)` | `voxels.getVoxel(pos)` | ✅ | GameVector3 重载 | -| `voxels.getVoxelId(x, y, z)` | `voxels.getVoxelId(x, y, z)` | ✅ | 一致。返回含旋转的完整 ID | -| `voxels.getVoxelId(pos)` | `voxels.getVoxelId(pos)` | ✅ | GameVector3 重载 | -| `voxels.getVoxelRotation(x, y, z)` | `voxels.getVoxelRotation(x, y, z)` | ✅ | 一致 | -| `voxels.getVoxelRotation(pos)` | `voxels.getVoxelRotation(pos)` | ✅ | GameVector3 重载 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ---------------------------------- | ---------------------------------- | ---- | --------------------------- | +| `voxels.getVoxel(x, y, z)` | `voxels.getVoxel(x, y, z)` | ✅ | 一致。返回基础 ID(无旋转) | +| `voxels.getVoxel(pos)` | `voxels.getVoxel(pos)` | ✅ | GameVector3 重载 | +| `voxels.getVoxelId(x, y, z)` | `voxels.getVoxelId(x, y, z)` | ✅ | 一致。返回含旋转的完整 ID | +| `voxels.getVoxelId(pos)` | `voxels.getVoxelId(pos)` | ✅ | GameVector3 重载 | +| `voxels.getVoxelRotation(x, y, z)` | `voxels.getVoxelRotation(x, y, z)` | ✅ | 一致 | +| `voxels.getVoxelRotation(pos)` | `voxels.getVoxelRotation(pos)` | ✅ | GameVector3 重载 | ### 4.5 MC 独有扩展 (Voxels) -| Box3JS API | 说明 | -|------------|------| -| `voxels.getVoxelName(x, y, z)` / `voxels.getVoxelName(pos)` | 返回方块的命名空间 ID(如 "minecraft:stone") | -| `voxels.fillVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.fillVoxel(pos1, pos2, voxel)` | 批量填充方块区域 | -| `voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.countVoxel(pos1, pos2, voxel)` | 统计区域内匹配方块数量 | -| `voxels.setSpawner(x, y, z, entityType)` / `voxels.setSpawner(pos, entityType)` | 设置刷怪笼类型 | - ---- +| Box3JS API | 说明 | +| ------------------------------------------------------------------------------------------- | --------------------------------------------- | +| `voxels.getVoxelName(x, y, z)` / `voxels.getVoxelName(pos)` | 返回方块的命名空间 ID(如 "minecraft:stone") | +| `voxels.fillVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.fillVoxel(pos1, pos2, voxel)` | 批量填充方块区域 | +| `voxels.countVoxel(x1, y1, z1, x2, y2, z2, voxel)` / `voxels.countVoxel(pos1, pos2, voxel)` | 统计区域内匹配方块数量 | +| `voxels.setSpawner(x, y, z, entityType)` / `voxels.setSpawner(pos, entityType)` | 设置刷怪笼类型 | ## 5. GameDataStorage (storage) ### 5.1 存储空间管理 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `storage.key` (只读属性) | `storage.key` (只读属性) | ✅ | 一致。但 Box3JS 返回空字符串(根 storage) | -| `storage.getDataStorage(key)` | `storage.getDataStorage(key)` | ✅ | 一致 | -| `storage.getGroupStorage(key)` | `storage.getGroupStorage(key)` | ✅ | 一致 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------ | ------------------------------ | ---- | ------------------------------------------ | +| `storage.key` (只读属性) | `storage.key` (只读属性) | ✅ | 一致。但 Box3JS 返回空字符串(根 storage) | +| `storage.getDataStorage(key)` | `storage.getDataStorage(key)` | ✅ | 一致 | +| `storage.getGroupStorage(key)` | `storage.getGroupStorage(key)` | ✅ | 一致 | ### 5.2 数据操作 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `ds.set(key, value)` | `ds.set(key, value)` | ⚠️ | **Box3 异步返回 Promise,Box3JS 同步执行** | -| `ds.get(key)` | `ds.get(key)` | ⚠️ | **Box3 异步返回 `Promise`,Box3JS 同步返回 value 或 null** | -| `ds.update(key, handler)` | `ds.update(key, handler)` | ⚠️ | **Box3 异步,Box3JS 同步** | -| `ds.remove(key)` | `ds.remove(key)` | ⚠️ | **Box3 异步,Box3JS 同步** | -| `ds.increment(key, value?)` | `ds.increment(key)` / `ds.increment(key, value)` | ⚠️ | **Box3 异步,Box3JS 同步** | -| `ds.list(options)` | `ds.list(options)` | ⚠️ | **Box3 异步,Box3JS 同步**。Box3JS 无 cursor 分页语义(cursor 仅作偏移量),不支持 constraintTarget 深层排序 | -| `ds.destroy()` | `ds.destroy()` | ⚠️ | **Box3 异步,Box3JS 同步** | -| — | `ds.keys()` | ⬆ | MC 扩展。返回所有 key 数组 | -| — | `ds.getKey()` | ⬆ | MC 扩展。返回存储空间名称 | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| --------------------------- | ------------------------------------------------ | ---- | ------------------------------------------------------------------------------------------------------------ | +| `ds.set(key, value)` | `ds.set(key, value)` | ⚠️ | **Box3 异步返回 Promise,Box3JS 同步执行** | +| `ds.get(key)` | `ds.get(key)` | ⚠️ | **Box3 异步返回 `Promise`,Box3JS 同步返回 value 或 null** | +| `ds.update(key, handler)` | `ds.update(key, handler)` | ⚠️ | **Box3 异步,Box3JS 同步** | +| `ds.remove(key)` | `ds.remove(key)` | ⚠️ | **Box3 异步,Box3JS 同步** | +| `ds.increment(key, value?)` | `ds.increment(key)` / `ds.increment(key, value)` | ⚠️ | **Box3 异步,Box3JS 同步** | +| `ds.list(options)` | `ds.list(options)` | ⚠️ | **Box3 异步,Box3JS 同步**。Box3JS 无 cursor 分页语义(cursor 仅作偏移量),不支持 constraintTarget 深层排序 | +| `ds.destroy()` | `ds.destroy()` | ⚠️ | **Box3 异步,Box3JS 同步** | +| — | `ds.keys()` | ⬆ | MC 扩展。返回所有 key 数组 | +| — | `ds.getKey()` | ⬆ | MC 扩展。返回存储空间名称 | ### 5.3 关键差异 @@ -838,131 +836,127 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 4. **速率限制**: Box3 有严格的读写速率限制,Box3JS 无限制。 5. **错误处理**: Box3 有详细的错误码(400/429/500),Box3JS 静默失败。 ---- - ## 6. Math 类型 ### 6.1 GameVector3 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(x, y, z)` | ✅ | ✅ | | -| `x, y, z` 属性 | ✅ | ✅ | | -| `set(x, y, z)` | ✅ | ✅ | | -| `copy(v)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `add(v)` | ✅ | ✅ | | -| `sub(v)` | ✅ | ✅ | | -| `mul(v)` | ✅ | ✅ | 逐分量乘法,零保护 | -| `div(v)` | ✅ | ✅ | 逐分量除法,零保护 | -| `addEq(v)` | ✅ | ✅ | 返回 this | -| `subEq(v)` | ✅ | ✅ | 返回 this | -| `mulEq(v)` | ✅ | ✅ | 返回 this | -| `divEq(v)` | ✅ | ✅ | 返回 this,零保护 | -| `dot(v)` | ✅ | ✅ | | -| `cross(v)` | ✅ | ✅ | 叉积 | -| `scale(n)` | ✅ | ✅ | | -| `lerp(v, n)` | ✅ | ✅ | | -| `towards(v)` | ✅ | ✅ | 返回归一化方向向量 | -| `mag()` | ✅ | ✅ | | -| `sqrMag()` | ✅ | ✅ | | -| `angle(v)` | ✅ | ✅ | 返回弧度 | -| `distance(v)` | ✅ | ✅ | | -| `equals(v)` | ✅ | ✅ | 使用 1e-6 容差,匹配 Box3 | -| `exactEquals(v)` | ✅ | ✅ | 精确比较 | -| `max(v)` | ✅ | ✅ | 逐分量取最大值 | -| `min(v)` | ✅ | ✅ | 逐分量取最小值 | -| `normalize()` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | -| `fromPolar(mag, phi, theta)` (静态) | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | ----------- | ---- | ------------------------- | +| 构造函数 `(x, y, z)` | ✅ | ✅ | | +| `x, y, z` 属性 | ✅ | ✅ | | +| `set(x, y, z)` | ✅ | ✅ | | +| `copy(v)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `add(v)` | ✅ | ✅ | | +| `sub(v)` | ✅ | ✅ | | +| `mul(v)` | ✅ | ✅ | 逐分量乘法,零保护 | +| `div(v)` | ✅ | ✅ | 逐分量除法,零保护 | +| `addEq(v)` | ✅ | ✅ | 返回 this | +| `subEq(v)` | ✅ | ✅ | 返回 this | +| `mulEq(v)` | ✅ | ✅ | 返回 this | +| `divEq(v)` | ✅ | ✅ | 返回 this,零保护 | +| `dot(v)` | ✅ | ✅ | | +| `cross(v)` | ✅ | ✅ | 叉积 | +| `scale(n)` | ✅ | ✅ | | +| `lerp(v, n)` | ✅ | ✅ | | +| `towards(v)` | ✅ | ✅ | 返回归一化方向向量 | +| `mag()` | ✅ | ✅ | | +| `sqrMag()` | ✅ | ✅ | | +| `angle(v)` | ✅ | ✅ | 返回弧度 | +| `distance(v)` | ✅ | ✅ | | +| `equals(v)` | ✅ | ✅ | 使用 1e-6 容差,匹配 Box3 | +| `exactEquals(v)` | ✅ | ✅ | 精确比较 | +| `max(v)` | ✅ | ✅ | 逐分量取最大值 | +| `min(v)` | ✅ | ✅ | 逐分量取最小值 | +| `normalize()` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | +| `fromPolar(mag, phi, theta)` (静态) | ✅ | ✅ | | **GameVector3 完全实现**,所有 28 个 Box3 方法均已对齐。 ### 6.2 GameBounds3 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(lo, hi)` | ✅ | ✅ | | -| `lo, hi` 属性 | ✅ | ✅ | | -| `set(lox, loy, loz, hix, hiy, hiz)` | ✅ | ✅ | | -| `copy(b)` | ✅ | ✅ | | -| `intersect(b)` | ✅ | ✅ | 返回新对象或 null(无交集时) | -| `intersects(b)` | ✅ | ✅ | | -| `contains(v)` | ✅ | ✅ | | -| `containsBounds(b)` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | -| `fromPoints(...points)` (静态) | ✅ | ✅ | 接受 NativeArray | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ----------------------------------- | ----------- | ---- | ----------------------------- | +| 构造函数 `(lo, hi)` | ✅ | ✅ | | +| `lo, hi` 属性 | ✅ | ✅ | | +| `set(lox, loy, loz, hix, hiy, hiz)` | ✅ | ✅ | | +| `copy(b)` | ✅ | ✅ | | +| `intersect(b)` | ✅ | ✅ | 返回新对象或 null(无交集时) | +| `intersects(b)` | ✅ | ✅ | | +| `contains(v)` | ✅ | ✅ | | +| `containsBounds(b)` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | +| `fromPoints(...points)` (静态) | ✅ | ✅ | 接受 NativeArray | **GameBounds3 完全实现**。 ### 6.3 GameRGBColor -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(r, g, b)` | ✅ | ✅ | | -| `r, g, b` 属性 | ✅ | ✅ | | -| `set(r, g, b)` | ✅ | ✅ | | -| `copy(c)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `add/sub/mul/div` | ✅ | ✅ | 除法有零保护 | -| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | 返回 this,除法有零保护 | -| `lerp(rgb, n)` | ✅ | ✅ | | -| `equals(rgb)` | ✅ | ✅ | 使用 1e-6 容差 | -| `toRGBA()` | ✅ | ✅ | 返回 `"rgba(r,g,b,1.0)"` 字符串 | -| `toString()` | ✅ | ✅ | | -| `random()` (静态) | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------- | ----------- | ---- | ------------------------------- | +| 构造函数 `(r, g, b)` | ✅ | ✅ | | +| `r, g, b` 属性 | ✅ | ✅ | | +| `set(r, g, b)` | ✅ | ✅ | | +| `copy(c)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `add/sub/mul/div` | ✅ | ✅ | 除法有零保护 | +| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | 返回 this,除法有零保护 | +| `lerp(rgb, n)` | ✅ | ✅ | | +| `equals(rgb)` | ✅ | ✅ | 使用 1e-6 容差 | +| `toRGBA()` | ✅ | ✅ | 返回 `"rgba(r,g,b,1.0)"` 字符串 | +| `toString()` | ✅ | ✅ | | +| `random()` (静态) | ✅ | ✅ | | **GameRGBColor 完全实现**,所有 16 个 Box3 方法均已对齐。 ### 6.4 GameRGBAColor -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(r, g, b, a)` | ✅ | ✅ | | -| `r, g, b, a` 属性 | ✅ | ✅ | | -| `set(r, g, b, a)` | ✅ | ✅ | | -| `copy(c)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `add/sub/mul/div` | ✅ | ✅ | | -| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | | -| `lerp(rgba, n)` | ✅ | ✅ | | -| `equals(rgba)` | ✅ | ✅ | | -| `blendEq(rgb)` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------- | ----------- | ---- | -------- | +| 构造函数 `(r, g, b, a)` | ✅ | ✅ | | +| `r, g, b, a` 属性 | ✅ | ✅ | | +| `set(r, g, b, a)` | ✅ | ✅ | | +| `copy(c)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `add/sub/mul/div` | ✅ | ✅ | | +| `addEq/subEq/mulEq/divEq` | ✅ | ✅ | | +| `lerp(rgba, n)` | ✅ | ✅ | | +| `equals(rgba)` | ✅ | ✅ | | +| `blendEq(rgb)` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | **GameRGBAColor 是完全实现的**,所有 Box3 方法均有对应。 ### 6.5 GameQuaternion -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| 构造函数 `(w, x, y, z)` | ✅ | ✅ | | -| `w, x, y, z` 属性 | ✅ | ✅ | | -| `set(w, x, y, z)` | ✅ | ✅ | | -| `copy(v)` | ✅ | ✅ | | -| `clone()` | ✅ | ✅ | | -| `rotateX/Y/Z(rad)` | ✅ | ✅ | | -| `add/sub` | ✅ | ✅ | | -| `mul(q)` | ✅ | ✅ | Hamilton 积 | -| `inv()` | ✅ | ✅ | | -| `div(q)` | ✅ | ✅ | | -| `dot(q)` | ✅ | ✅ | | -| `slerp(q, n)` | ✅ | ✅ | | -| `angle(q)` | ✅ | ✅ | | -| `getAxisAngle()` | ✅ | ✅ | 返回 `{angle, axis}` | -| `mag()` | ✅ | ✅ | | -| `sqrMag()` | ✅ | ✅ | | -| `equals(v)` | ✅ | ✅ | | -| `normalize()` | ✅ | ✅ | | -| `toString()` | ✅ | ✅ | | -| `fromAxisAngle(axis, rad)` (静态) | ✅ | ✅ | | -| `fromEuler(x, y, z)` (静态) | ✅ | ✅ | YZX 顺序 | -| `rotationBetween(a, b)` (静态) | ✅ | ✅ | | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| --------------------------------- | ----------- | ---- | -------------------- | +| 构造函数 `(w, x, y, z)` | ✅ | ✅ | | +| `w, x, y, z` 属性 | ✅ | ✅ | | +| `set(w, x, y, z)` | ✅ | ✅ | | +| `copy(v)` | ✅ | ✅ | | +| `clone()` | ✅ | ✅ | | +| `rotateX/Y/Z(rad)` | ✅ | ✅ | | +| `add/sub` | ✅ | ✅ | | +| `mul(q)` | ✅ | ✅ | Hamilton 积 | +| `inv()` | ✅ | ✅ | | +| `div(q)` | ✅ | ✅ | | +| `dot(q)` | ✅ | ✅ | | +| `slerp(q, n)` | ✅ | ✅ | | +| `angle(q)` | ✅ | ✅ | | +| `getAxisAngle()` | ✅ | ✅ | 返回 `{angle, axis}` | +| `mag()` | ✅ | ✅ | | +| `sqrMag()` | ✅ | ✅ | | +| `equals(v)` | ✅ | ✅ | | +| `normalize()` | ✅ | ✅ | | +| `toString()` | ✅ | ✅ | | +| `fromAxisAngle(axis, rad)` (静态) | ✅ | ✅ | | +| `fromEuler(x, y, z)` (静态) | ✅ | ✅ | YZX 顺序 | +| `rotationBetween(a, b)` (静态) | ✅ | ✅ | | **GameQuaternion 是完全实现的**。 ---- - ## 7. 其他服务端 API ### 7.1 GameAnimation @@ -971,12 +965,12 @@ Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 Box3 有完整的关键帧动画系统,支持 World/Entity/Player 级别的属性动画(位置、颜色、缩放、天气参数等)。Box3JS 无动画系统。 -| Box3 API | 状态 | -|----------|------| -| `world.animate(keyframes, playback?)` | ❌ | -| `entity.animate(keyframes, playback?)` | ❌ | -| `player.animate(keyframes, playback?)` | ❌ | -| `GameAnimation` 对象 (play/cancel/currentTime/playState/onFinish/...) | ❌ | +| Box3 API | 状态 | +| --------------------------------------------------------------------- | ---- | +| `world.animate(keyframes, playback?)` | ❌ | +| `entity.animate(keyframes, playback?)` | ❌ | +| `player.animate(keyframes, playback?)` | ❌ | +| `GameAnimation` 对象 (play/cancel/currentTime/playState/onFinish/...) | ❌ | ### 7.2 GameMotionController @@ -984,13 +978,13 @@ Box3 有完整的关键帧动画系统,支持 World/Entity/Player 级别的属 Box3 的 Voxa 模型动画系统,用于控制自定义模型的骨骼动画。 -| Box3 API | 状态 | -|----------|------| -| `entity.motion` | ❌ | -| `motion.loadByName(configs)` | ❌ | -| `motion.pause()` / `motion.resume()` | ❌ | -| `motion.setDefaultMotionByName(name)` | ❌ | -| `GameMotionHandler` (play/cancel/onFinish) | ❌ | +| Box3 API | 状态 | +| ------------------------------------------ | ---- | +| `entity.motion` | ❌ | +| `motion.loadByName(configs)` | ❌ | +| `motion.pause()` / `motion.resume()` | ❌ | +| `motion.setDefaultMotionByName(name)` | ❌ | +| `GameMotionHandler` (play/cancel/onFinish) | ❌ | ### 7.3 Sound @@ -1000,19 +994,23 @@ Box3 的 `sound()` 方法返回 Sound 对象,支持 `setCurrentTime`/`resume`/ ### 7.4 RemoteChannel (跨端通信) -**状态**: ❌ 完全未实现 +**状态**: ✅ 已实现(双向通信) + +Box3JS 通过自定义网络数据包实现了完整的服务端↔客户端双向事件通道,API 命名与 Box3 一致。 + +**服务端** (Box3JSWorld / Box3ScriptEngine): -Box3 的 `remoteChannel` 用于服务端↔客户端双向通信。MC 模组运行在服务端,无客户端代码,无法实现。 +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------------ | ------------------------------------------------ | ---- | ------------------------------------------------------------------- | +| `remoteChannel.sendClientEvent(entities, event)` | `remoteChannel.sendClientEvent(entities, event)` | ✅ | 一致。entities 为单个 GamePlayerEntity 或数组 | +| `remoteChannel.broadcastClientEvent(event)` | `remoteChannel.broadcastClientEvent(event)` | ✅ | 一致。向所有在线玩家广播 | +| `remoteChannel.onServerEvent(handler)` | `remoteChannel.onServerEvent(handler)` | ✅ | 一致。回调接收 `{ tick, entity, args }`,返回 GameEventHandlerToken | +| `remoteChannel.sendServerEvent(event)` (客户端) | `remoteChannel.sendServerEvent(event)` | ✅ | 一致。客户端发送事件到服务端 | +| `remoteChannel.onClientEvent(handler)` (客户端) | `remoteChannel.onClientEvent(handler)` | ✅ | 一致。回调接收 `{ tick, args }`,返回 GameEventHandlerToken | -| Box3 API | 状态 | -|----------|------| -| `remoteChannel.sendClientEvent(entities, event)` (服务端) | ❌ | -| `remoteChannel.broadcastClientEvent(event)` (服务端) | ❌ | -| `remoteChannel.onServerEvent(handler)` (服务端) | ❌ | -| `remoteChannel.sendServerEvent(event)` (客户端) | ❌ | -| `remoteChannel.onClientEvent(handler)` (客户端) | ❌ | +**客户端事件数据自动 JSON 序列化/反序列化传输**,支持任意可序列化的 JS 值。 -**替代方案**: Box3JS 提供了 `world.sendMessage(target, data)` 和 `world.onMessage(fn)` 用于**脚本间**通信(同一服务端不同脚本项目),但这与 Box3 的跨端通信不同。 +> **注意**: `world.sendMessage(target, data)` 和 `world.onMessage(fn)` 是**额外的**跨脚本通信机制(同一服务端不同项目间通信),与 remoteChannel 的跨端通信互补。 ### 7.5 GameRTC (实时语音) @@ -1026,28 +1024,28 @@ MC 无内置语音通信,无法实现。 Box3 的 `http.fetch(url, options?)` 用于服务端发起 HTTP 请求。 -| Box3 API | Box3JS 实现 | 状态 | 差异说明 | -|----------|-------------|------|---------| -| `http.fetch(url, options?)` → `Promise` | `http.fetch(url, options?)` → `Response` | ⚠️ | Box3 异步返回 Promise;Box3JS 同步阻塞调用 | -| `options.method` | ✅ | ✅ | GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS | -| `options.headers` | ✅ | ✅ | 键值对 | -| `options.body` (string) | ✅ | ✅ | 文本请求体 | -| `options.body` (ArrayBuffer) | ✅ | ✅ | 二进制请求体 | -| `options.timeout` | ✅ | ✅ | 超时毫秒 | -| — | `options.responseType` | ⬆ | 自动解析:`"json"` / `"text"` / `"arrayBuffer"`,结果见 `resp.data` | -| — | `options.maxBodySize` | ⬆ | 响应体最大字节数,超出截断并标记 `resp.truncated` | -| `Response.ok` | ✅ | ✅ | 状态码 200-299 | -| `Response.status` | ✅ | ✅ | HTTP 状态码 | -| `Response.statusText` | ✅ | ✅ | 状态描述 | -| `Response.headers` | ✅ | ✅ | 响应头键值对 | -| `Response.json()` → `Promise` | `Response.json()` → `any` | ⚠️ | Box3 异步;Box3JS 同步返回,解析失败返回 `null` | -| `Response.text()` → `Promise` | `Response.text()` → `string` | ⚠️ | Box3 异步;Box3JS 同步返回 | -| `Response.arrayBuffer()` → `Promise` | `Response.arrayBuffer()` → `ArrayBuffer` | ⚠️ | Box3 异步;Box3JS 同步返回 | -| — | `Response.getHeader(name)` | ⬆ | 获取单个响应头值 | -| — | `Response.errorMessage` | ⬆ | 请求失败时的错误信息 | -| — | `Response.truncated` | ⬆ | 响应体是否因 maxBodySize 被截断 | -| — | `Response.data` | ⬆ | responseType 自动解析的结果 | -| `Response.close()` | ✅ | ✅ | 关闭连接(Box3JS 为空操作) | +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +| ------------------------------------------------- | ---------------------------------------- | ---- | ------------------------------------------------------------------- | +| `http.fetch(url, options?)` → `Promise` | `http.fetch(url, options?)` → `Response` | ⚠️ | Box3 异步返回 Promise;Box3JS 同步阻塞调用 | +| `options.method` | ✅ | ✅ | GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS | +| `options.headers` | ✅ | ✅ | 键值对 | +| `options.body` (string) | ✅ | ✅ | 文本请求体 | +| `options.body` (ArrayBuffer) | ✅ | ✅ | 二进制请求体 | +| `options.timeout` | ✅ | ✅ | 超时毫秒 | +| — | `options.responseType` | ⬆ | 自动解析:`"json"` / `"text"` / `"arrayBuffer"`,结果见 `resp.data` | +| — | `options.maxBodySize` | ⬆ | 响应体最大字节数,超出截断并标记 `resp.truncated` | +| `Response.ok` | ✅ | ✅ | 状态码 200-299 | +| `Response.status` | ✅ | ✅ | HTTP 状态码 | +| `Response.statusText` | ✅ | ✅ | 状态描述 | +| `Response.headers` | ✅ | ✅ | 响应头键值对 | +| `Response.json()` → `Promise` | `Response.json()` → `any` | ⚠️ | Box3 异步;Box3JS 同步返回,解析失败返回 `null` | +| `Response.text()` → `Promise` | `Response.text()` → `string` | ⚠️ | Box3 异步;Box3JS 同步返回 | +| `Response.arrayBuffer()` → `Promise` | `Response.arrayBuffer()` → `ArrayBuffer` | ⚠️ | Box3 异步;Box3JS 同步返回 | +| — | `Response.getHeader(name)` | ⬆ | 获取单个响应头值 | +| — | `Response.errorMessage` | ⬆ | 请求失败时的错误信息 | +| — | `Response.truncated` | ⬆ | 响应体是否因 maxBodySize 被截断 | +| — | `Response.data` | ⬆ | responseType 自动解析的结果 | +| `Response.close()` | ✅ | ✅ | 关闭连接(Box3JS 为空操作) | > **⚠️ 重要差异:** Box3JS 的 `http.fetch()` 是**同步阻塞**调用(Rhino 引擎限制),会阻塞服务器 tick。Box3 原版是异步 Promise。请避免在高频回调(`world.onTick()` 等)中使用。 @@ -1069,29 +1067,165 @@ Box3 的 `resources.ls(type?)` 浏览资源文件。MC 无对应资源管理 API Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` / `.active()`。Box3JS 的所有 `world.onXxx()` 方法均返回 `GameEventHandlerToken`,支持 `.cancel()` 取消注册和 `.active()` 检查状态。`.resume()` 抛出 UnsupportedOperationException(需重新注册)。 ---- +## 8. 客户端 API + +Box3JS 现已支持**客户端脚本引擎**(`src/client/app.ts`),在玩家客户端上运行,提供与 Box3 客户端 API 对应的能力。客户端脚本通过 `remoteChannel` 与服务端双向通信。 + +### 8.1 客户端生命周期 (client) + +| Box3JS 客户端 API | 说明 | +| ------------------------ | ----------------------------------------------------------------------------------------------- | +| `client.onTick(fn)` | 注册每帧回调(每秒 20 次),返回 GameEventHandlerToken | +| `client.getFPS()` | 获取当前帧率 | +| `client.getPlayer()` | 获取本地玩家信息 `{ name, uuid, health, maxHealth, food, saturation, xp, dimension, position }` | +| `client.getLookingAt()` | 获取准星目标,返回 `{ type, position, entity?, blockPos?, direction? }` 或 null | +| `client.getServerInfo()` | 获取服务器信息 `{ ip, name, isLocal, playerCount, maxPlayers }` | + +### 8.2 客户端 UI (ui) + +与 Box3 的 ClientUI 对应,Box3JS 提供 HUD 绘制和屏幕消息能力: + +| Box3JS 客户端 API | 说明 | +| ------------------------------------------------------ | ------------------------------------------------------ | +| `ui.showOverlay(text)` | 显示屏幕中央覆盖文字(2 秒自动消失) | +| `ui.showTitle(title, subtitle, fadeIn, stay, fadeOut)` | 显示标题/副标题 | +| `ui.showActionBar(text)` | 显示 ActionBar 消息 | +| `ui.getScreenSize()` | 获取屏幕尺寸 `{ scaledWidth, scaledHeight, guiScale }` | +| `ui.drawText(id, x, y, text)` | 在屏幕指定位置绘制文字(每帧调用维持显示) | +| `ui.removeDrawText(id)` | 移除指定绘制文字 | +| `ui.clearDrawTexts()` | 移除所有绘制文字 | + +**与 Box3 ClientUI 差异**: Box3 有完整的 2D UI 框架(盒子、文本、图片、输入框、滚动框等),Box3JS 目前提供轻量级的屏幕文字叠加层,不支持复杂的交互式 UI 控件。复杂 UI 建议通过 `gui.openGUI()` 使用容器 GUI。 + +### 8.3 客户端输入 (input) + +与 Box3 的 `player.onKeyDown/onKeyUp` 对应: + +| Box3JS 客户端 API | 说明 | +| --------------------------- | -------------------------------------------------------- | +| `input.onKeyPress(key, fn)` | 注册按键按下回调,key 如 `"f5"`、`"c"`、`"space"` | +| `input.isKeyDown(key)` | 检查按键是否正在按住 | +| `input.onMouseClick(fn)` | 鼠标点击回调 `(button: 0=LMB/1=RMB/2=MMB, action, x, y)` | +| `input.getMouseX()` | 获取鼠标屏幕 X 坐标 | +| `input.getMouseY()` | 获取鼠标屏幕 Y 坐标 | + +**与 Box3 差异**: Box3 的 `player.onPress` 检测游戏内交互按钮(ACTION0/ACTION1/JUMP/WALK),Box3JS 的 `input.onKeyPress` 检测键盘按键。Box3 无鼠标事件,`input.onMouseClick` 是 Box3JS 独有扩展。 + +### 8.4 客户端聊天 (chat) + +与 Box3 的客户端聊天系统对应: + +| Box3JS 客户端 API | 说明 | +| ------------------------ | ------------------------------------------------------------- | +| `chat.sendMessage(text)` | 发送聊天消息 | +| `chat.sendCommand(cmd)` | 执行客户端命令(以 `/` 开头) | +| `chat.onMessage(fn)` | 监听聊天消息 `(message, sender, isSystem)`,返回 false 可拦截 | + +### 8.5 客户端 GUI (gui) + +Box3JS 的容器 GUI 系统,与 Box3 的 UI 面板理念不同(Box3 是 2D UI 控件,Box3JS 是 MC 原版容器): -## 8. 客户端 API(不适用) +| Box3JS 客户端 API | 说明 | +| --------------------------------- | ---------------------------------------------------------- | +| `gui.openGUI(config)` | 打开自定义容器 GUI,config: `{ title, rows(1-6), slots? }` | +| `controller.setItem(slot, id, n)` | 设置槽位物品 | +| `controller.getItem(slot)` | 获取槽位物品 `{ id, count }` | +| `controller.onSlotClick(fn)` | 槽位点击回调 `(slot)` | +| `controller.onClose(fn)` | GUI 关闭回调 | +| `controller.close()` | 主动关闭 GUI | -以下 Box3 API 全部运行在**客户端**(玩家浏览器),Box3JS 作为纯服务端模组**完全无法实现**: +**与 Box3 差异**: Box3 的 UI 系统是基于 2D 画布的布局控件(盒子/文本/图片/输入框),Box3JS 使用 MC 原版容器 GUI(类似箱子界面),更适合物品交互场景。Box3 的 2D UI 控件体系暂无对应实现。 -| 类别 | 全局对象 | 说明 | -|------|---------|------| -| ClientUI | `ui`, `input`, `screenWidth`, `screenHeight` | 2D UI 系统(盒子、文本、图片、输入框、滚动框等) | -| ClientAudio | `Audio` | 客户端音频播放 | -| ClientMedia | `media` | 录音/播放 | -| ClientNavigator | `navigator` | 设备信息、语言 | -| ClientScreen | `screen` | 屏幕尺寸事件 | -| ClientHttp | `http` (客户端) | 客户端 HTTP 请求 | -| ClientWorld | `world` (客户端) | 3D 渲染开关 | +### 8.6 客户端音频 (audio) ---- +与 Box3 的 ClientAudio 对应: + +| Box3JS 客户端 API | 说明 | +| -------------------------------------- | --------------------------------- | +| `audio.playSound(path, volume, pitch)` | 播放音效(支持原版和自定义音效) | +| `audio.playMusic(path, volume)` | 播放背景音乐(MUSIC 类别) | +| `audio.stopAll()` | 停止所有正在播放的音效和音乐 | +| `audio.getVolume(category)` | 获取指定音频类别的音量 | +| `audio.setVolume(category, value)` | 设置指定音频类别的音量(0.0–1.0) | + +**与 Box3 差异**: Box3 的 `Sound` 对象支持 `pause/resume/stop/setCurrentTime` 精细控制。Box3JS 的 `playSound/playMusic` 是 fire-and-forget 模式,无法暂停/恢复播放中的声音。`stopAll()` 可批量停止。 + +### 8.7 客户端存储 (storage) + +客户端独立的本地持久化存储,与服务端 storage 完全隔离: + +| Box3JS 客户端 API | 说明 | +| -------------------------------- | -------------------------------------------------------------- | +| `storage.getDataStorage(key)` | 获取具类型的数据存储空间(客户端本地),返回 `GameDataStorage` | +| `ds.set(key, value)` | 同步写入键值 | +| `ds.get(key)` | 同步读取,返回 value 或 null | +| `ds.update(key, fn)` | 原子更新 | +| `ds.increment(key, delta?)` | 原子增减(数值类型) | +| `ds.list(options)` | 分页列出 `{ pageSize?, ascending? }` | +| `ds.remove(key)` | 删除键 | +| `ds.keys()` | 返回所有键数组 | + +客户端存储位置: `config/box3/storage/-client/`,与服务端存储文件分离。 + +### 8.8 客户端数据库 (db) + +客户端本地 SQLite 数据库(需安装 `minecraft-sqlite-jdbc` 模组): + +| Box3JS 客户端 API | 说明 | +| --------------------------------------- | ------------------------------------------ | +| `db.isAvailable()` | 检查 SQLite 驱动是否可用 | +| `db.sql(sql, ...params)` | 执行 SQL 查询/更新,返回 `GameQueryResult` | +| `db.sql\`SELECT ... WHERE id = ${id}\`` | Tagged template 语法,自动参数化防注入 | + +**GameQueryResult**: `rows` (数组), `firstRow`, `columnNames`, `columnCount`, `rowCount`, `affectedRows`, `isQuery` + +每个项目独立客户端数据库文件 `config/box3/data/-client.db`。 + +### 8.9 客户端 HTTP (http) + +客户端 HTTP 请求,支持同步和异步模式: + +| Box3JS 客户端 API | 说明 | +| --------------------------- | ------------------------------------------------------------------------------------ | +| `http.fetch(url, options?)` | 发起 HTTP 请求,options 同服务端,额外支持 `async: true` + `onResponse/onError` 回调 | +| `Response` 对象 | `ok`, `status`, `headers`, `data`, `json()`, `text()`, `arrayBuffer()` | + +> **客户端特有**: 支持 `async: true` 异步模式,通过 `onResponse`/`onError` 回调接收结果,不阻塞渲染帧。 + +### 8.10 RemoteChannel (客户端) + +客户端侧的跨端通信已在 [§7.4 RemoteChannel](#74-remotechannel-跨端通信) 详述。客户端通过 `remoteChannel.sendServerEvent()` 向服务端发送事件,通过 `remoteChannel.onClientEvent()` 接收服务端事件。 + +### 8.11 客户端控制台 (console) + +| Box3JS API | 说明 | +| --------------------------- | ---------------- | +| `console.log(...args)` | 输出到客户端日志 | +| `console.warn(...args)` | 输出警告 | +| `console.error(...args)` | 输出错误 | +| `console.debug(...args)` | 输出调试信息 | +| `console.clear()` | 清空控制台 | +| `console.assert(cond, ...)` | 条件断言输出 | + +### 8.12 与 Box3 客户端 API 差距 + +以下 Box3 客户端能力 Box3JS **暂未实现**: + +| Box3 客户端功能 | 状态 | 说明 | +| -------------------- | ---- | ----------------------------------------------------------------------------------------- | +| 2D UI 控件体系 | ❌ | Box3 的盒子/文本/图片/输入框/滚动框等完整 UI 框架。Box3JS 仅有 drawText 叠加层 + 容器 GUI | +| `media` 录音/播放 | ❌ | MC 无内置录音 API | +| `navigator` 设备信息 | ❌ | MC 无对应 API | +| 3D 渲染开关 | ❌ | MC 渲染始终开启 | +| 客户端世界 API | ❌ | Box3 客户端可控制 3D 渲染开关等,Box3JS 无对应 | +| 自定义模型渲染 | ❌ | Box3 的 `entity.mesh` / `meshColor` / `meshScale` 等 | ## 9. Box3JS 独有 MC 扩展 这些 API 是 Box3JS 利用 Minecraft 原生能力提供的,在 Box3 平台**不存在**: ### 9.1 世界管理 + - `world.thunderDensity` — 雷暴强度 - `world.clearWeather()` — 清除天气 - `world.getGameRule/setGameRule` — 游戏规则 @@ -1110,6 +1244,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `world.onButtonPressed` — 按钮点击事件(石质/木质按钮) ### 9.2 实体管理 + - `entity.nameTag` — 名牌 - `entity.glowing` — 发光 - `entity.invulnerable` — 无敌 @@ -1128,6 +1263,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `entity.setText/setTextColor/setTextBackgroundColor` — 文本显示实体控制 ### 9.3 玩家管理 + - `player.opLevel` — OP 权限 - `player.gameMode` — 游戏模式 - `player.dimension` — 维度切换 @@ -1149,6 +1285,7 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `player.grantAdvancement/revokeAdvancement` — 成就授予/撤销 ### 9.4 系统 + - `world.addScoreboard/removeScoreboard/setScore/getScore/showScoreboard/hideScoreboard/listScores` — 记分板 - `world.showBossbar/removeBossbar` — Boss 血条 - `world.createTeam/removeTeam/joinTeam/leaveTeam/getTeamOf` — 队伍管理 @@ -1157,55 +1294,92 @@ Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` - `voxels.fillVoxel/countVoxel/getVoxelName/setSpawner` — 方块批量操作 ### 9.5 额外事件 + - `world.onEntityDamage` — 实体受伤(Pre 阶段) - `world.onMessage` — 跨脚本消息 - `world.onButtonPressed` — 按钮点击事件(支持石质/木质按钮长按检测) ### 9.6 数据库 -- `db.sql` — SQLite 数据库操作(支持 tagged template 和参数化查询) + +- `db.sql` — SQLite 数据库操作 - `GameQueryResult` — 查询结果(rows, firstRow, columnNames, rowCount, affectedRows, isQuery) - 每个项目独立数据库文件 `config/box3/data/.db` ### 9.7 HTTP 请求 + - `http.fetch(url, options?)` — 同步 HTTP 请求,支持 GET/POST/PUT/DELETE/PATCH/HEAD/OPTIONS - `options.responseType` — 自动解析响应体(`"json"` / `"text"` / `"arrayBuffer"`),结果见 `resp.data` - `options.maxBodySize` — 响应体大小限制,超出截断并标记 `resp.truncated` - `Response.getHeader(name)` — 获取单个响应头值 - `Response.errorMessage` — 请求失败时的错误信息 ---- +### 9.8 客户端扩展 + +Box3JS 客户端引擎提供的 Box3 平台**不存在**的能力: + +- `client.getFPS()` — 实时帧率监控 +- `client.getLookingAt()` — 准星目标检测(实体/方块 + 方向) +- `client.getPlayer()` — 本地玩家完整状态 +- `client.getServerInfo()` — 服务器连接信息 +- `ui.drawText(id, x, y, text)` — 屏幕坐标精确 HUD 绘制 +- `ui.removeDrawText(id)` / `ui.clearDrawTexts()` — HUD 文字管理 +- `ui.getScreenSize()` — 屏幕尺寸和 GUI 缩放 +- `input.onKeyPress(key, fn)` — 键盘快捷键注册(F1–F12 等) +- `input.onMouseClick(fn)` — 鼠标点击事件 +- `input.isKeyDown(key)` — 按键状态实时检测 +- `chat.onMessage(fn)` — 聊天消息拦截(可阻止广播) +- `gui.openGUI(config)` — 自定义容器 GUI(3–6 行,物品交互) +- `audio.playSound/playMusic/stopAll` — 客户端音效/音乐播放 +- `audio.getVolume/setVolume` — 音量类别精细控制 +- Client-side `storage` — 客户端独立持久化存储 +- Client-side `db` — 客户端本地 SQLite 数据库 +- Client-side `http.fetch(async: true)` — 非阻塞异步 HTTP 请求 ## 10. 总结 ### 10.1 实现统计 -| 类别 | Box3 API 总数 | 已实现 | 部分实现 | 未实现 | MC 独有扩展 | -|------|--------------|--------|---------|--------|-------------| -| GameWorld | ~80 | ~30 | ~6 | ~44 | 33 | -| GameEntity | ~65 | ~21 | ~1 | ~43 | 21 | -| GamePlayerEntity | ~72 | ~27 | ~4 | ~41 | 22 | -| GameVoxels | 14 | 14 | 0 | 0 | 4 | -| GameDataStorage | 8 | 8 | 7 (同步化) | 0 | 2 | -| Math 类型 | ~100 | ~100 | 0 | 0 | 0 | -| 数据库 (db) | N/A | — | — | — | 1 | -| 其他服务端 | ~30 | 0 | 0 | ~30 | 0 | -| **总计** | **~369** | **~200** | **~18** | **~158** | **~83** | - -> **2026-05 更新**: 本阶段实现约 54 个新 Box3 API(属性对齐 + math 补全 + 物理属性 + token + 回调签名 + World API 补全),Math 类型现已完全对齐。 +| 类别 | Box3 API 总数 | 已实现 | 部分实现 | 未实现 | MC 独有扩展 | +| ---------------- | ------------- | -------- | ---------- | -------- | ----------- | +| GameWorld | ~80 | ~30 | ~6 | ~44 | 33 | +| GameEntity | ~65 | ~21 | ~1 | ~43 | 21 | +| GamePlayerEntity | ~72 | ~27 | ~6 | ~39 | 22 | +| GameVoxels | 14 | 14 | 0 | 0 | 4 | +| GameDataStorage | 8 | 8 | 7 (同步化) | 0 | 2 | +| Math 类型 | ~100 | ~100 | 0 | 0 | 0 | +| RemoteChannel | 5 | 5 | 0 | 0 | 0 | +| HTTP | 11 | 11 | 3 (同步化) | 0 | 4 | +| 数据库 (db) | N/A | — | — | — | 1 | +| **服务端总计** | **~355** | **~216** | **~23** | **~126** | **~87** | +| 客户端 ui | ~15 | 7 | 0 | ~8 | 3 | +| 客户端 input | ~8 | 5 | 0 | ~3 | 3 | +| 客户端 audio | ~6 | 5 | 0 | ~1 | 0 | +| 客户端 chat | 3 | 3 | 0 | 0 | 1 | +| 客户端 gui | ~12 | 6 | 0 | ~6 | 0 | +| 客户端 client | 5 | 5 | 0 | 0 | 5 | +| 客户端 storage | 8 | 8 | 7 (同步化) | 0 | 0 | +| 客户端 db | N/A | — | — | — | 1 | +| 客户端 http | 11 | 11 | 0 | 0 | 2 | +| **客户端总计** | **~68** | **~50** | **~7** | **~18** | **~15** | + +> **2026-05 更新**: 本阶段新增客户端脚本引擎,实现了 ~50 个 Box3 客户端 API(ui/input/audio/chat/gui/client/storage/db/http),并通过 remoteChannel 实现完整的服务端↔客户端双向通信。 ### 10.2 核心差异模式 1. **事件回调签名**: Box3 传事件对象 → Box3JS 传展开参数;已为 onTick/onPlayerJoin/Leave/Respawn 添加 tick 参数 -2. **异步存储**: Box3 的 Promise 存储 → Box3JS 的同步本地文件存储 -3. **视觉/渲染 API**: Box3 有独立渲染引擎 → Box3JS 依赖 MC 原版渲染,无法控制雾/光照/雪花/粒子系统参数 +2. **异步存储**: Box3 的 Promise 存储 → Box3JS 的同步本地文件存储(服务端 + 客户端一致) +3. **视觉/渲染 API**: Box3 有独立渲染引擎 → Box3JS 依赖 MC 原版渲染,暂无法控制雾/光照/雪花参数(客户端引擎已可用,未来可扩展) 4. **物理 API**: 基本物理属性(collides/fixed/gravity/friction/mass/restitution)已对齐,高级物理(接触力/OBB/碰撞过滤)不支持 -5. **客户端 API**: Box3 有完整 UI/音频/媒体客户端 API → Box3JS 纯服务端,全部不可用 +5. **客户端 API**: Box3 有完整 2D UI 框架 → Box3JS 客户端提供 HUD 叠加层 + 容器 GUI,暂无交互式 UI 控件 6. **动画系统**: Box3 有关键帧动画 → Box3JS 无 -7. **事件令牌**: 已实现 GameEventHandlerToken,所有 world.onXxx() 返回 token,支持 cancel()/active() +7. **事件令牌**: 已实现 GameEventHandlerToken,所有 onXxx() 返回 token,支持 cancel()/active() +8. **双端架构**: Box3JS 服务端 + 客户端分离,通过 remoteChannel 双向通信,与 Box3 架构理念一致 ### 10.3 Box3JS 的独特优势 -Box3JS 虽然缺失大量 Box3 视觉/渲染/客户端 API,但提供了 Box3 平台**完全不具备**的 MC 原生能力: +Box3JS 虽然缺失部分 Box3 视觉/渲染/2D UI API,但提供了 Box3 平台**完全不具备**的能力: + +**服务端独有能力**: - **完整的原版方块系统** — 数百种方块类型、红石、容器、刷怪笼 - **生物 AI/寻路/战斗** — 全套 MC 生物行为控制 @@ -1216,14 +1390,31 @@ Box3JS 虽然缺失大量 Box3 视觉/渲染/客户端 API,但提供了 Box3 - **维度切换** — 下界/末地传送 - **游戏模式/OP 权限** — 权限管理 - **跨脚本通信** — 模块化脚本协作 +- **合成管理** — 配方禁用/恢复 +- **结构放置** — 数据包结构模板 + +**客户端独有能力**: + +- **HUD 精确绘制** — `ui.drawText` 屏幕坐标文字,FPS 监控 +- **键盘快捷键** — `input.onKeyPress` 注册自定义热键 +- **鼠标事件** — `input.onMouseClick` 检测点击类型和坐标 +- **聊天拦截** — `chat.onMessage` 拦截/过滤/扩展聊天消息 +- **容器 GUI** — `gui.openGUI` 自定义物品交互界面 +- **客户端音效** — `audio.playSound/playMusic` 双类别音频控制 +- **客户端存储** — 玩家本地的持久化键值存储(独立于服务端) +- **客户端数据库** — 本地 SQLite 缓存/查询 +- **异步 HTTP** — `http.fetch(async: true)` 非阻塞网络请求 +- **准星检测** — `client.getLookingAt()` 实时方块/实体目标 +- **RemoteChannel** — 服务端↔客户端双向事件通信 ### 10.4 迁移建议 从 Box3 平台迁移脚本到 Box3JS 时: -1. **视觉相关代码需重写** — 雾/光照/天气效果无法直接迁移,需使用 MC 原版机制代替 +1. **视觉相关代码需重写** — 雾/光照/天气效果暂无法直接迁移,需使用 MC 原版机制代替(客户端引擎已可用,未来可扩展) 2. **回调参数需调整** — 将事件对象访问改为参数列表访问 -3. **异步存储改为同步** — 移除 `await`,直接调用存储方法 +3. **异步存储改为同步** — 移除 `await`,直接调用存储方法(服务端和客户端均同步) 4. **实体外观/物理不可用** — mesh/粒子/物理属性需删除或用 MC 代替 -5. **客户端代码全废弃** — UI/音频/媒体代码无对应 -6. **可充分利用 MC 扩展** — 记分板/Bossbar/生物 AI/物品系统/药水效果等是 Box3 没有的强大功能 +5. **客户端代码需重写** — Box3 的 2D UI 控件体系(盒子/文本/图片/输入框)暂无可直接对应的 API,可用 `ui.drawText` HUD + `gui.openGUI` 容器代替;音频代码可迁移至 `audio` 全局对象;键盘事件可迁移至 `input` 全局对象 +6. **充分利用双端架构** — 将客户端逻辑放入 `src/client/app.ts`,通过 `remoteChannel` 与服务端通信,与 Box3 的客户端/服务端分离理念一致 +7. **可充分利用 MC 扩展** — 记分板/Bossbar/生物 AI/物品系统/药水效果/合成管理等是 Box3 没有的强大功能 diff --git a/Box3JS-NeoForge-1.21.1/docs/README_en.md b/Box3JS-NeoForge-1.21.1/docs/README_en.md deleted file mode 100644 index b16526e..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/README_en.md +++ /dev/null @@ -1,70 +0,0 @@ -# Box3JS Documentation - -Box3JS is a JavaScript/TypeScript scripting engine mod for Minecraft NeoForge 1.21.1. Write server-side gameplay scripts and client-side UI scripts without installing a JDK or compiling Java code. - -## Navigation - -### Getting Started - -Learn what Box3JS is, how to use it, and the principles behind it — from zero. - -| Doc | Content | -|------|------| -| [Quick Start](guide/getting-started_en.md) | Setup → first script → dev cycle → debugging → deployment | -| [Common Recipes](guide/recipes_en.md) | Feature templates: economy, teleport, shop, daily rewards, leaderboards, webhooks | -| [Architecture](guide/architecture_en.md) | Rhino engine, scope management, build pipeline, network communication | -| [JS vs Java](guide/js-vs-java_en.md) | Box3JS scripting vs native Java modding — pros, cons & when to choose | -| [FAQ](guide/faq_en.md) | Loading, build, runtime, database, HTTP, client, deployment | - -### Tutorials - -5 progressive tutorials, each 10–15 minutes with complete runnable code. - -| # | Tutorial | You'll learn | -|---|---------|-------------| -| 1 | [From Zero](tutorial/01-basics_en.md) | Create project → build → first script → chat commands → timers | -| 2 | [Players & Items](tutorial/02-player-items_en.md) | Teleport, flight, give items, enchantments, potion effects, game modes | -| 3 | [Events & Entities](tutorial/03-events-entities_en.md) | All event callbacks, spawn entities, AI control, patrol guards, collision | -| 4 | [Advanced Systems](tutorial/04-advanced-systems_en.md) | Scoreboards, BossBars, teams, world border, cross-script messaging | -| 5 | [Real Mini-Games](tutorial/05-examples_en.md) | Full PvP arena, particle effects, fireworks, wave spawning | -| 6 | [Client-Side Scripting](tutorial/06-client-scripting_en.md) | Keyboard input, screen UI, sound/music, local storage, SQLite, HTTP, remoteChannel | -| 📋 | [Tutorial Overview](tutorial/README_en.md) | Learning roadmap, prerequisites, pro tips | - -### API Reference - -Complete API docs organized by functional category. One document per global object/namespace. - -| Category | Doc | Globals | -|----------|-----|---------| -| **Server Overview** | [server](api/server_en.md) | Server runtime boundary, events, players/entities, blocks, data, cross-side communication | -| **World** | [world](api/world_en.md) | `world` — events, particles, fireworks, sound, scoreboards | -| **Entity** | [entity](api/entity_en.md) | `entity` — properties, AI, equipment, effects | -| **Player** | [player](api/player_en.md) | `player` — inventory, messages, flight, teleport | -| **Voxels** | [voxels](api/voxels_en.md) | `voxels` — block read/write, region fill | -| **Storage** | [storage](api/storage_en.md) | `storage` — JSON persistence | -| **Database** | [database](api/database_en.md) | `db` — SQLite database | -| **HTTP** | [http](api/http_en.md) | `http` — HTTP requests | -| **Client** | [client](api/client_en.md) | `audio` `client` `input` `ui` `chat` `gui` `remoteChannel` | -| **Registries** | [registries](api/registries_en.md) | `registries` — custom blocks/items/sounds | -| **Math** | [math](api/math_en.md) | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | -| **Commands** | [commands](api/commands_en.md) | `/box3script` CLI commands | -| **Task Lookup** | [API by Task](api/README_en.md) | Find APIs by "I want to..." | -| **Comparison** | [Box3 API Comparison](BOX3_API_COMPARISON.md) | Box3 platform API vs Box3JS implementation | - -### Version Info - -| Item | Version | -|------|---------| -| Minecraft | 1.21.1 | -| Mod Loader | NeoForge | -| Java | 21 | -| JS Engine | Mozilla Rhino 1.9.1 (ES5 compatible) | -| TypeScript | Compiled to ES5 via Babel | - -## Quick Links - -- **5-minute quickstart**: [Quick Start →](guide/getting-started_en.md) -- **I want to do X, which API?**: [API by Task →](api/README_en.md) -- **Why Box3JS over Java modding?**: [JS vs Java →](guide/js-vs-java_en.md) -- **How does Box3JS work internally?**: [Architecture →](guide/architecture_en.md) -- **Learn Box3JS scripting from zero**: [Tutorial 1 →](tutorial/01-basics_en.md) diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README.md b/Box3JS-NeoForge-1.21.1/docs/api/README.md index 54f0f0e..ae9b6a4 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/README.md @@ -1,6 +1,6 @@ # Box3JS API 参考 -Box3JS 是一个 Minecraft 模组,允许用 JavaScript/TypeScript 编写服务端脚本,并可选下发客户端脚本来实现本地 UI、输入和音效。每个脚本项目位于 `config/box3/script/<项目名>` 下。 +用 JavaScript/TypeScript 编写 Minecraft 服务端与客户端脚本。 ## 5 分钟快速开始 @@ -32,33 +32,42 @@ console.log("脚本已加载"); 每次修改后重新 `npm run build`,然后用 `/box3script reload mygame` 热重载。客户端逻辑放在 `src/client/app.ts`,构建后生成 `dist/client.js`,玩家安装 Box3JS 客户端 Mod 后会在加入服务器时自动接收。 -> **新手上路**: [快速开始指南](../guide/getting-started.md) | **原理深入**: [运行原理](../guide/architecture.md) | **JS vs Java**: [技术选型对比](../guide/js-vs-java.md) +::: tip 快速导航 +[快速开始指南](../guide/getting-started.md) | [运行原理](../guide/architecture.md) | [JS vs Java 对比](../guide/js-vs-java.md) +::: ## API 领域分类 Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服务端与客户端类型声明已拆分,`tsconfig.server.json` 不会包含客户端全局对象,`tsconfig.client.json` 也不会包含服务端 `world` / `voxels`。 -| 领域 | 运行环境 | 全局对象 | 说明 | -|------|---------|---------|------| -| **世界与实体** (服务端) | 服务端 | `world` `voxels` | 世界控制、方块操作、事件回调 | -| **玩家与数据** (服务端) | 服务端 | `entity` `player` `storage` `db` `http` | `entity`/`player` 来自回调或查询 | -| **客户端交互** (客户端) | 客户端 | `audio` `client` `input` `ui` `chat` `gui` | 需 Box3JS 客户端 Mod | -| **跨端通信** | 双端 | `remoteChannel` | 服务端↔客户端事件通信 | -| **注册与编译** | 编译时 | `registries` | 仅在 `/box3script compile` JAR 模式可用 | -| **数学与工具** | 双端 | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | 通过 `new` 构造 | -| **全局工具** | 双端 | `console` | 日志输出 | +| 领域 | 运行环境 | 全局对象 | 说明 | +| ----------------------- | -------- | --------------------------------------------------------------------------- | --------------------------------------- | +| **世界与实体** (服务端) | 服务端 | `world` `voxels` | 世界控制、方块操作、事件回调 | +| **玩家与数据** (服务端) | 服务端 | `entity` `player` `storage` `db` `http` | `entity`/`player` 来自回调或查询 | +| **客户端交互** (客户端) | 客户端 | `audio` `client` `input` `ui` `chat` `gui` | 需 Box3JS 客户端 Mod | +| **跨端通信** | 双端 | `remoteChannel` | 服务端↔客户端事件通信 | +| **注册与编译** | 编译时 | `registries` | 仅在 `/box3script compile` JAR 模式可用 | +| **数学与工具** | 双端 | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | 通过 `new` 构造 | +| **全局工具** | 双端 | `console` | 日志输出 | -> **服务端 API** 操作世界、实体、玩家、方块。脚本默认运行在服务端。 -> **客户端 API** 仅在安装了 Box3JS 客户端 Mod 时可用,用于 UI、输入、音效。 -> **注册 API** 仅在编译 JAR 模式下可用(`/box3script start` 解释模式中 `registries` 为 `undefined`)。 +::: info API 分类 +**服务端 API** 操作世界、实体、玩家、方块。脚本默认运行在服务端。**客户端 API** 仅在安装了 Box3JS 客户端 Mod 时可用,用于 UI、输入、音效。**注册 API** 仅在编译 JAR 模式下可用(`/box3script start` 解释模式中 `registries` 为 `undefined`)。 +::: ## 按运行环境阅读 -| 入口 | 适合场景 | 包含文档 | -|------|----------|----------| -| [服务端 API 总览](server.md) | 写玩法逻辑、事件、方块、实体、玩家、数据、服务端到客户端事件 | `world`、`entity`、`player`、`voxels`、`storage`、`db`、`http`、`registries` | -| [客户端 API 总览](client.md) | 写本地 UI、输入、音效、聊天辅助、本地数据、客户端到服务端事件 | `client`、`audio`、`input`、`ui`、`chat`、`gui`、`storage`、`db`、`http` | -| [共享工具](math.md) | 写双端都可用的数学、颜色、空间计算代码 | `GameVector3`、`GameBounds3`、`GameRGBColor`、`GameRGBAColor`、`GameQuaternion` | +| 入口 | 适合场景 | 包含文档 | +| ---------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [服务端 API 总览](server.md) | 写玩法逻辑、事件、方块、实体、玩家、数据、服务端到客户端事件 | `world`、`entity`、`player`、`voxels`、`storage`、`db`、`http`、`registries` | +| [客户端 API 总览](client.md) | 写本地 UI、输入、音效、聊天辅助、本地数据、客户端到服务端事件 | `client`、`audio`、`input`、`ui`、`chat`、`gui`、`storage`、`db`、`http` | +| [共享工具](math.md) | 写双端都可用的数学、颜色、空间计算代码 | `GameVector3`、`GameBounds3`、`GameRGBColor`、`GameRGBAColor`、`GameQuaternion` | + +## API 风格约定 + +- 所有 `onXxx(...)` 事件注册 API 都返回 `GameEventHandlerToken`,使用 `token.cancel()` 取消监听,使用 `token.active()` 检查是否仍有效。 +- 服务端 API 只出现在 `src/server/app.ts` 的类型环境中;客户端 API 只出现在 `src/client/app.ts` 的类型环境中。双端共享 API 为 `storage`、`db`、`http`、`remoteChannel`、`console` 和数学类型。 +- 跨端数据通过 `remoteChannel` 发送 JSON 可序列化对象:客户端使用 `sendServerEvent` / `onClientEvent`,服务端使用 `sendClientEvent` / `broadcastClientEvent` / `onServerEvent`。 +- 能用 `GameVector3` 的坐标 API 通常同时支持 `x, y, z` 重载;服务端方块坐标按整数处理。 ## 功能速查 — 我想... @@ -66,222 +75,225 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 ### 消息与聊天 -| 我想... | 用这个 | -|---------|--------| -| 全服广播 | `world.say("消息")` | -| 私密消息(只一人看到) | `player.directMessage("消息")` | -| 快捷栏上方文字 | `player.actionBar("消息")` | -| 屏幕中央大标题 | `player.title("标题", "副标题")` | -| 拦截/处理聊天 | `world.onChat((entity, msg) => { ... })` | +| 我想... | 用这个 | +| ---------------------- | ---------------------------------------- | +| 全服广播 | `world.say("消息")` | +| 私密消息(只一人看到) | `player.directMessage("消息")` | +| 快捷栏上方文字 | `player.actionBar("消息")` | +| 屏幕中央大标题 | `player.title("标题", "副标题")` | +| 拦截/处理聊天 | `world.onChat((entity, msg) => { ... })` | ### 玩家属性 -| 我想... | 用这个 | -|---------|--------| -| 获取/设置玩家位置 | `player.position` → `GameVector3` | -| 传送玩家 | `player.teleport(new GameVector3(x, y, z))` | -| 修改生命值 | `player.hp = 20` / `player.maxHp = 40` | -| 修改饱食度 | `player.food = 20` / `player.saturation = 10` | -| 切换游戏模式 | `player.gameMode = "creative"` | -| 切换飞行 | `player.canFly = true` / `player.flying = true` | -| 踢出玩家 | `player.kick("原因")` | -| 以玩家身份执行命令 | `player.runCommand("say hi")` | +| 我想... | 用这个 | +| ------------------ | ----------------------------------------------- | +| 获取/设置玩家位置 | `player.position` → `GameVector3` | +| 传送玩家 | `player.teleport(new GameVector3(x, y, z))` | +| 修改生命值 | `player.hp = 20` / `player.maxHp = 40` | +| 修改饱食度 | `player.food = 20` / `player.saturation = 10` | +| 切换游戏模式 | `player.gameMode = "creative"` | +| 切换飞行 | `player.canFly = true` / `player.flying = true` | +| 踢出玩家 | `player.kick("原因")` | +| 以玩家身份执行命令 | `player.runCommand("say hi")` | ### 物品与装备 -| 我想... | 用这个 | -|---------|--------| -| 给玩家普通物品 | `player.giveItem("minecraft:diamond", 1)` | -| 给带附魔的物品 | `player.giveEnchantedItem(...)` | -| 给带自定义名称的物品 | `player.giveNamedItem(...)` | -| 获取手持物品 | `player.getHeldItem()` | -| 清空背包 | `player.clearInventory()` | -| 设置实体装备 | `entity.setEquipment("head", "iron_helmet")` | +| 我想... | 用这个 | +| -------------------- | -------------------------------------------- | +| 给玩家普通物品 | `player.giveItem("minecraft:diamond", 1)` | +| 给带附魔的物品 | `player.giveEnchantedItem(...)` | +| 给带自定义名称的物品 | `player.giveNamedItem(...)` | +| 获取手持物品 | `player.getHeldItem()` | +| 清空背包 | `player.clearInventory()` | +| 设置实体装备 | `entity.setEquipment("head", "iron_helmet")` | ### 自定义注册表(方块/物品/音效) 🆕 -| 我想... | 用这个 | -|---------|--------| -| 注册自定义方块 | `registries/blocks.json`(编译时) | -| 注册自定义物品 | `registries/items.json`(编译时) | -| 注册自定义音效 | `registries/sounds.json`(编译时) | -| 注册创造标签页 | `registries/creativeTabs.json`(编译时) | -| 获取注册的方块 | `registries.getBlock("my_block")` | -| 获取注册的物品 | `registries.getItem("chocolate")` | -| 获取注册的音效 | `registries.getSound("victory_fanfare")` | -| 给予自定义方块/物品 | `player.giveItem(block.itemId, 1)` | -| 放置自定义方块 | `voxels.setVoxel(x, y, z, block.block)` | +| 我想... | 用这个 | +| ------------------------ | ----------------------------------------------- | +| 注册自定义方块 | `registries/blocks.json`(编译时) | +| 注册自定义物品 | `registries/items.json`(编译时) | +| 注册自定义音效 | `registries/sounds.json`(编译时) | +| 注册创造标签页 | `registries/creativeTabs.json`(编译时) | +| 获取注册的方块 | `registries.getBlock("my_block")` | +| 获取注册的物品 | `registries.getItem("chocolate")` | +| 获取注册的音效 | `registries.getSound("victory_fanfare")` | +| 给予自定义方块/物品 | `player.giveItem(block.itemId, 1)` | +| 放置自定义方块 | `voxels.setVoxel(x, y, z, block.block)` | | 播放自定义音效(服务端) | `world.playSound(sound.soundId, x, y, z, 1, 1)` | -| 播放自定义音效(客户端) | `audio.playSound("modId:soundId", 1.0, 1.0)` | +| 播放自定义音效(客户端) | `audio.playSound("modId:soundId", 1.0, 1.0)` | -> **仅服务端可用。** 客户端脚本中 `registries` 为 `undefined`。**仅在 `/box3script compile` 编译的 JAR 模式下可用。** 需客户端也安装该 JAR 以正确渲染纹理/模型。详见 [registries.md](registries.md) +::: warning +仅服务端可用。客户端脚本中 `registries` 为 `undefined`。仅在 `/box3script compile` 编译的 JAR 模式下可用。需客户端也安装该 JAR 以正确渲染纹理/模型。详见 [registries.md](registries.md) +::: ### 方块操作 -| 我想... | 用这个 | -|---------|--------| -| 读取某个位置的方块 | `voxels.getVoxel(x, y, z)` | -| 放置/替换方块 | `voxels.setVoxel(x, y, z, "minecraft:stone")` | -| 填充区域 | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | -| 监听方块破坏 | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | -| 监听方块放置 | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | +| 我想... | 用这个 | +| ------------------ | ----------------------------------------------------------- | +| 读取某个位置的方块 | `voxels.getVoxel(x, y, z)` | +| 放置/替换方块 | `voxels.setVoxel(x, y, z, "minecraft:stone")` | +| 填充区域 | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | +| 监听方块破坏 | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | +| 监听方块放置 | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | ### 实体操控 -| 我想... | 用这个 | -|---------|--------| -| 生成实体 | `world.spawnEntity("minecraft:zombie", pos)` | +| 我想... | 用这个 | +| -------------- | --------------------------------------------- | +| 生成实体 | `world.spawnEntity("minecraft:zombie", pos)` | | 带配置创建实体 | `world.createEntity({ type, position, ... })` | -| 设置实体名称 | `entity.setNameTag("§cBoss")` | -| 开关 AI | `entity.setAI(true)` | -| 实体导航 | `entity.navigateTo(x, y, z, speed)` | -| 设置攻击目标 | `entity.setTarget(otherEntity)` | -| 判断是否是玩家 | `entity.isPlayer()` | -| 获取实体类型 | `entity.entityType` | -| 获取实体标签 | `entity.tags()` / `entity.hasTag("boss")` | -| 查询附近实体 | `world.entitiesInRadius(pos, radius)` | -| 查询所有实体 | `world.querySelectorAll("*")` | +| 设置实体名称 | `entity.setNameTag("§cBoss")` | +| 开关 AI | `entity.setAI(true)` | +| 实体导航 | `entity.navigateTo(x, y, z, speed)` | +| 设置攻击目标 | `entity.setTarget(otherEntity)` | +| 判断是否是玩家 | `entity.isPlayer()` | +| 获取实体类型 | `entity.entityType` | +| 获取实体标签 | `entity.tags()` / `entity.hasTag("boss")` | +| 查询附近实体 | `world.entitiesInRadius(pos, radius)` | +| 查询所有实体 | `world.querySelectorAll("*")` | ### 客户端本地功能(需 Box3JS 客户端 Mod) -| 我想... | 用这个 | -|---------|--------| -| 客户端每帧执行 | `client.onTick(() => { ... })` | -| 检测按键按下 | `input.isKeyDown("space")` | -| 监听按键事件 | `input.onKeyPress("f", () => { ... })` | -| 播放客户端音效 | `audio.playSound("pling", 1.0, 1.0)` | -| 播放客户端音乐 | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | -| 停止所有声音 | `audio.stopAll()` | -| 获取/设置音量 | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | -| 快捷栏上方显示文字 | `ui.showOverlay("文字")` | -| 显示屏幕大标题 | `ui.showTitle("标题", "副标题")` | -| 发送聊天消息 | `chat.sendMessage("消息")` | -| 接收聊天消息 | `chat.onMessage((msg, sender, isSystem) => { ... })` | -| 发送服务端事件 | `remoteChannel.sendServerEvent({ ... })` | -| 接收服务端事件 | `remoteChannel.onClientEvent((event) => { ... })` | -| 客户端本地存储 | `storage.getDataStorage("key")` | +| 我想... | 用这个 | +| ------------------ | ----------------------------------------------------------------- | +| 客户端每帧执行 | `client.onTick(() => { ... })` | +| 检测按键按下 | `input.isKeyDown("space")` | +| 监听按键事件 | `input.onKeyPress("f", () => { ... })` | +| 播放客户端音效 | `audio.playSound("pling", 1.0, 1.0)` | +| 播放客户端音乐 | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | +| 停止所有声音 | `audio.stopAll()` | +| 获取/设置音量 | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | +| 快捷栏上方显示文字 | `ui.showOverlay("文字")` | +| 显示屏幕大标题 | `ui.showTitle("标题", "副标题")` | +| 发送聊天消息 | `chat.sendMessage("消息")` | +| 接收聊天消息 | `chat.onMessage((msg, sender, isSystem) => { ... })` | +| 发送服务端事件 | `remoteChannel.sendServerEvent({ ... })` | +| 接收服务端事件 | `remoteChannel.onClientEvent((event) => { ... })` | +| 客户端本地存储 | `storage.getDataStorage("key")` | +| 设置雾颜色 | `client.setFogColor(255, 100, 50)` | +| 设置雾距离 | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | +| 重置雾效果 | `client.resetFog()` | ### 视觉效果 -| 我想... | 用这个 | -|---------|--------| -| 粒子效果 | `world.spawnParticle("flame", x, y, z, ...)` | -| 圆形粒子圈 | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | -| 烟花 | `world.launchFirework(x, y, z, "red", "large_ball")` | -| 闪电 | `world.strikeLightning(x, y, z)` | -| 爆炸 | `world.explode(x, y, z, 4)` | -| 播放音效(全局) | `world.playSound("pling", pos, 1.0, 1.0)` | -| 播放音效(单人) | `player.playSound("pling", 1.0, 1.0)` | +| 我想... | 用这个 | +| ---------------- | --------------------------------------------------------- | +| 粒子效果 | `world.spawnParticle("flame", x, y, z, ...)` | +| 圆形粒子圈 | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | +| 烟花 | `world.launchFirework(x, y, z, "red", "large_ball")` | +| 闪电 | `world.strikeLightning(x, y, z)` | +| 爆炸 | `world.explode(x, y, z, 4)` | +| 播放音效(全局) | `world.playSound("pling", pos, 1.0, 1.0)` | +| 播放音效(单人) | `player.playSound("pling", 1.0, 1.0)` | ### 药水效果 -| 我想... | 用这个 | -|---------|--------| +| 我想... | 用这个 | +| ------------ | --------------------------------------------------------------------- | | 施加药水效果 | `entity.addEffect("minecraft:speed", duration, level, hideParticles)` | -| 清除所有效果 | `entity.clearEffects()` | +| 清除所有效果 | `entity.clearEffects()` | ### 事件系统 -| 我想... | 用这个 | -|---------|--------| -| 每 tick 执行 | `world.onTick((info) => { ... })` | -| 玩家加入时 | `world.onPlayerJoin((entity, tick) => { ... })` | -| 玩家离开时 | `world.onPlayerLeave((entity, tick) => { ... })` | -| 实体死亡时 | `world.onEntityDeath((entity, killer, tick) => { ... })` | -| 实体受伤时 | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | -| 右键实体时 | `world.onInteract((entity, target, tick) => { ... })` | -| 右键方块时 | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | -| 按钮按下时 | `world.onButtonPressed((entity, button, tick) => { ... })` | -| 玩家重生时 | `world.onPlayerRespawn((entity, tick) => { ... })` | -| 定时执行一次 | `world.setTimeout(() => { ... }, ticks)` | -| 定时循环执行 | `world.setInterval(() => { ... }, ticks)` | -| 取消事件监听 | `token.cancel()` | -| 检查事件是否活跃 | `token.active()` | +| 我想... | 用这个 | +| ---------------- | --------------------------------------------------------------------------- | +| 每 tick 执行 | `world.onTick((info) => { ... })` | +| 玩家加入时 | `world.onPlayerJoin((entity, tick) => { ... })` | +| 玩家离开时 | `world.onPlayerLeave((entity, tick) => { ... })` | +| 实体死亡时 | `world.onEntityDeath((entity, killer, tick) => { ... })` | +| 实体受伤时 | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | +| 右键实体时 | `world.onInteract((entity, target, tick) => { ... })` | +| 右键方块时 | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | +| 按钮按下时 | `world.onButtonPressed((entity, button, tick) => { ... })` | +| 玩家重生时 | `world.onPlayerRespawn((entity, tick) => { ... })` | +| 定时执行一次 | `setTimeout(() => { ... }, ticks)` | +| 定时循环执行 | `setInterval(() => { ... }, ticks)` | +| 取消事件监听 | `token.cancel()` | +| 检查事件是否活跃 | `token.active()` | ### 数据持久化 -| 我想... | 用这个 | -|---------|--------| +| 我想... | 用这个 | +| -------------- | ------------------------------- | | 读写 JSON 数据 | `storage.getDataStorage("key")` | -| SQL 查询 | `db.sql("SELECT ...")` | -| SQL 写入 | `db.sql("INSERT INTO ...")` | +| SQL 查询 | `db.sql("SELECT ...")` | +| SQL 写入 | `db.sql("INSERT INTO ...")` | ### 网络请求 -| 我想... | 用这个 | -|---------|--------| -| GET 请求 | `http.fetch("https://...")` | +| 我想... | 用这个 | +| --------- | ---------------------------------------------------- | +| GET 请求 | `http.fetch("https://...")` | | POST JSON | `http.fetch(url, { method: "POST", headers, body })` | -| 解析 JSON | `resp.json()` 或 `{ responseType: "json" }` | -| 读取文本 | `resp.text()` | -| 设置超时 | `http.fetch(url, { timeout: 5000 })` | +| 解析 JSON | `resp.json()` 或 `{ responseType: "json" }` | +| 读取文本 | `resp.text()` | +| 设置超时 | `http.fetch(url, { timeout: 5000 })` | ### 游戏系统 -| 我想... | 用这个 | -|---------|--------| -| 创建计分板 | `world.addScoreboard("name")` | -| 设置分数 | `world.setScore("玩家", "计分板", 10)` | -| 显示计分板 | `world.showScoreboard("sidebar", "name")` | -| 显示 BossBar | `world.showBossbar("id", "标题", 0.5, "red")` | -| 创建队伍 | `world.createTeam("teamName", "color")` | -| 加入队伍 | `world.joinTeam(entity, "teamName")` | -| 设置世界边界 | `world.borderSize = 500` | -| 缩圈 | `world.shrinkBorder(100, 60)` | -| 修改世界时间 | `world.time = 6000` | -| 设置天气 | `world.rainDensity = 0` / `world.clearWeather()` | -| 修改游戏规则 | `world.setGameRule("keepInventory", true)` | +| 我想... | 用这个 | +| ------------ | ------------------------------------------------ | +| 创建计分板 | `world.addScoreboard("name")` | +| 设置分数 | `world.setScore("玩家", "计分板", 10)` | +| 显示计分板 | `world.showScoreboard("sidebar", "name")` | +| 显示 BossBar | `world.showBossbar("id", "标题", 0.5, "red")` | +| 创建队伍 | `world.createTeam("teamName", "color")` | +| 加入队伍 | `world.joinTeam(entity, "teamName")` | +| 设置世界边界 | `world.borderSize = 500` | +| 缩圈 | `world.shrinkBorder(100, 60)` | +| 修改世界时间 | `world.time = 6000` | +| 设置天气 | `world.rainDensity = 0` / `world.clearWeather()` | +| 修改游戏规则 | `world.setGameRule("keepInventory", true)` | ### 数学工具 -| 我想... | 用这个 | -|---------|--------| -| 三维坐标 | `new GameVector3(x, y, z)` | -| 向量运算 | `v.add(other)`, `v.scale(n)`, `v.length()` | -| 包围盒 | `new GameBounds3(min, max)` | -| 颜色 | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | +| 我想... | 用这个 | +| -------- | ------------------------------------------------------------- | +| 三维坐标 | `new GameVector3(x, y, z)` | +| 向量运算 | `v.add(other)`, `v.scale(n)`, `v.length()` | +| 包围盒 | `new GameBounds3(min, max)` | +| 颜色 | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | ### 跨脚本通信 -| 我想... | 用这个 | -|---------|--------| -| 发送消息给其他脚本 | `world.sendMessage("projectName", data)` | +| 我想... | 用这个 | +| ------------------ | ------------------------------------------ | +| 发送消息给其他脚本 | `world.sendMessage("projectName", data)` | | 接收其他脚本的消息 | `world.onMessage((from, data) => { ... })` | ---- - ## 全局对象一览 -| 对象 | 类型 | 说明 | -|------|------|------| -| `world` | 服务端 | 世界控制,见 [world.md](world.md) | -| `voxels` | 服务端 | 方块操作,见 [voxels.md](voxels.md) | -| `entity` | 服务端值 | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | -| `player` | 服务端值 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | -| `storage` | 双端 | 数据持久化,见 [storage.md](storage.md) | -| `db` | 双端 | SQLite 数据库,见 [database.md](database.md) | -| `http` | 双端 | HTTP 请求,见 [http.md](http.md) | -| `audio` | 客户端 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | -| `client` | 客户端 | 客户端生命周期,见 [client.md](client.md) | -| `input` | 客户端 | 客户端键盘输入,见 [client.md](client.md) | -| `ui` | 客户端 | 客户端屏幕 UI,见 [client.md](client.md) | -| `chat` | 客户端 | 客户端聊天收发,见 [client.md](client.md) | -| `gui` | 客户端 | 自定义容器 GUI,见 [client.md](client.md) | -| `remoteChannel` | 双端 | 服务端↔客户端事件通信,见 [server.md](server.md) / [client.md](client.md) | -| `registries` | 服务端 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | -| `console` | 双端 | 控制台日志输出(`log`/`warn`/`error`/`debug`) | -| `GameVector3` | 双端 | 三维向量,见 [math.md](math.md) | -| `GameBounds3` | 双端 | 包围盒,见 [math.md](math.md) | -| `GameRGBColor` | 双端 | RGB 颜色,见 [math.md](math.md) | -| `GameRGBAColor` | 双端 | RGBA 颜色,见 [math.md](math.md) | -| `GameQuaternion` | 双端 | 四元数,见 [math.md](math.md) | +| 对象 | 类型 | 说明 | +| ---------------- | -------- | -------------------------------------------------------------------------- | +| `world` | 服务端 | 世界控制,见 [world.md](world.md) | +| `voxels` | 服务端 | 方块操作,见 [voxels.md](voxels.md) | +| `entity` | 服务端值 | 实体包装(回调参数或 `world.spawnEntity` 创建),见 [entity.md](entity.md) | +| `player` | 服务端值 | 玩家包装(通过 `entity.player` 获取),见 [player.md](player.md) | +| `storage` | 双端 | 数据持久化,见 [storage.md](storage.md) | +| `db` | 双端 | SQLite 数据库,见 [database.md](database.md) | +| `http` | 双端 | HTTP 请求,见 [http.md](http.md) | +| `audio` | 客户端 | 客户端音效、音乐、音量控制,见 [client.md](client.md) | +| `client` | 客户端 | 客户端生命周期,见 [client.md](client.md) | +| `input` | 客户端 | 客户端键盘输入,见 [client.md](client.md) | +| `ui` | 客户端 | 客户端屏幕 UI,见 [client.md](client.md) | +| `chat` | 客户端 | 客户端聊天收发,见 [client.md](client.md) | +| `gui` | 客户端 | 自定义容器 GUI,见 [client.md](client.md) | +| `remoteChannel` | 双端 | 服务端↔客户端事件通信,见 [server.md](server.md) / [client.md](client.md) | +| `registries` | 服务端 | 自定义方块/物品/音效(编译模式),见 [registries.md](registries.md) | +| `console` | 双端 | 控制台日志输出(`log`/`warn`/`error`/`debug`) | +| `GameVector3` | 双端 | 三维向量,见 [math.md](math.md) | +| `GameBounds3` | 双端 | 包围盒,见 [math.md](math.md) | +| `GameRGBColor` | 双端 | RGB 颜色,见 [math.md](math.md) | +| `GameRGBAColor` | 双端 | RGBA 颜色,见 [math.md](math.md) | +| `GameQuaternion` | 双端 | 四元数,见 [math.md](math.md) | ## API 标注说明 -| 标注 | 含义 | -|------|------| -| ✅ **Box3 API** | 源自 Box3 平台,命名和语义与 Box3 保持一致 | -| ⬆ **MC 扩展** | 非 Box3 原有,利用 Minecraft 特性新增的 API | +| 标注 | 含义 | +| --------------- | ------------------------------------------- | +| ✅ **Box3 API** | 源自 Box3 平台,命名和语义与 Box3 保持一致 | +| ⬆ **MC 扩展** | 非 Box3 原有,利用 Minecraft 特性新增的 API | ## 文档风格约定 @@ -297,26 +309,26 @@ Box3JS API 按运行环境分为服务端、客户端和双端共享三类。服 ## 详细文档索引 -| 文档 | 内容 | -|------|------| -| [server.md](server.md) | 服务端 API 总览:运行边界、全局对象、事件、玩家/实体、方块、数据、跨端通信 | -| [world.md](world.md) | 世界状态、事件回调、记分板、BossBar、队伍、边界、粒子、烟花、闪电、音效 | -| [entity.md](entity.md) | 实体属性、AI、装备、药水效果、寻路、标签、碰撞 | -| [player.md](player.md) | 背包、消息、飞行、游戏模式、传送、命令、经验值 | -| [voxels.md](voxels.md) | 方块读写、区域填充、刷怪笼 | -| [storage.md](storage.md) | 数据持久化存储 | -| [database.md](database.md) | SQLite 数据库 | -| [http.md](http.md) | HTTP 网络请求 | -| [client.md](client.md) | 客户端 API:生命周期、键盘输入、屏幕 UI、聊天、GUI、remoteChannel、客户端本地存储 | -| [registries.md](registries.md) | 自定义方块/物品/音效(blocks.json、items.json、sounds.json、creativeTabs.json) | -| [math.md](math.md) | GameVector3、GameBounds3、GameRGBColor、GameRGBAColor、GameQuaternion | -| [commands.md](commands.md) | `/box3script` 命令参考 | +| 文档 | 内容 | +| ------------------------------ | --------------------------------------------------------------------------------- | +| [server.md](server.md) | 服务端 API 总览:运行边界、全局对象、事件、玩家/实体、方块、数据、跨端通信 | +| [world.md](world.md) | 世界状态、事件回调、记分板、BossBar、队伍、边界、粒子、烟花、闪电、音效 | +| [entity.md](entity.md) | 实体属性、AI、装备、药水效果、寻路、标签、碰撞 | +| [player.md](player.md) | 背包、消息、飞行、游戏模式、传送、命令、经验值 | +| [voxels.md](voxels.md) | 方块读写、区域填充、刷怪笼 | +| [storage.md](storage.md) | 数据持久化存储 | +| [database.md](database.md) | SQLite 数据库 | +| [http.md](http.md) | HTTP 网络请求 | +| [client.md](client.md) | 客户端 API:生命周期、键盘输入、屏幕 UI、聊天、GUI、remoteChannel、客户端本地存储 | +| [registries.md](registries.md) | 自定义方块/物品/音效(blocks.json、items.json、sounds.json、creativeTabs.json) | +| [math.md](math.md) | GameVector3、GameBounds3、GameRGBColor、GameRGBAColor、GameQuaternion | +| [commands.md](commands.md) | `/box3script` 命令参考 | ## 文件模块 — TypeScript 构建管线 `/box3script create` 创建的项目自带完整的 TS 构建环境: -``` +```text config/box3/script/mygame/ ├── package.json ← esbuild + Babel + @babel/preset-typescript ├── tsconfig.base.json ← 公共 TS 编译选项 @@ -359,7 +371,7 @@ config/box3/script/mygame/ 开发调试完成后,将脚本编译为**独立 JAR 模组**,需与 Box3JS 一同部署在 NeoForge 服务器: -``` +```js /box3script compile <项目名> ``` @@ -369,30 +381,30 @@ config/box3/script/mygame/ ## Tick 换算 -| 时长 | Ticks | -|------|-------| -| 1 秒 | 20 | -| 5 秒 | 100 | -| 30 秒 | 600 | -| 1 分钟 | 1200 | -| 5 分钟 | 6000 | +| 时长 | Ticks | +| ------ | ----- | +| 1 秒 | 20 | +| 5 秒 | 100 | +| 30 秒 | 600 | +| 1 分钟 | 1200 | +| 5 分钟 | 6000 | ## 深入学习 -| 文档 | 内容 | -|------|------| -| [快速开始](../guide/getting-started.md) | 环境搭建、第一个脚本、开发循环、调试、发布 | -| [运行原理](../guide/architecture.md) | Rhino 引擎、作用域、事件回调、构建管线、网络通信 | -| [JS vs Java](../guide/js-vs-java.md) | Box3JS 脚本开发 vs 原生 Java 模组开发对比 | +| 文档 | 内容 | +| --------------------------------------- | ------------------------------------------------ | +| [快速开始](../guide/getting-started.md) | 环境搭建、第一个脚本、开发循环、调试、发布 | +| [运行原理](../guide/architecture.md) | Rhino 引擎、作用域、事件回调、构建管线、网络通信 | +| [JS vs Java](../guide/js-vs-java.md) | Box3JS 脚本开发 vs 原生 Java 模组开发对比 | ## 教程 从零开始学习 Box3JS 脚本开发,请阅读 `docs/tutorial/` 系列教程: -| 教程 | 内容 | -|------|------| -| [01-basics.md](../tutorial/01-basics.md) | 从零开始:第一个脚本、聊天命令、定时任务 | -| [02-player-items.md](../tutorial/02-player-items.md) | 玩家操控:传送、物品给予、药水效果、游戏模式 | -| [03-events-entities.md](../tutorial/03-events-entities.md) | 事件系统与实体操控:AI、战斗、巡逻 | -| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | 高级系统:计分板、BossBar、队伍、世界边界 | -| [05-examples.md](../tutorial/05-examples.md) | 实战示例:PvP 竞技场、特效、烟花、波次刷怪 | +| 教程 | 内容 | +| ------------------------------------------------------------ | -------------------------------------------- | +| [01-basics.md](../tutorial/01-basics.md) | 从零开始:第一个脚本、聊天命令、定时任务 | +| [02-player-items.md](../tutorial/02-player-items.md) | 玩家操控:传送、物品给予、药水效果、游戏模式 | +| [03-events-entities.md](../tutorial/03-events-entities.md) | 事件系统与实体操控:AI、战斗、巡逻 | +| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | 高级系统:计分板、BossBar、队伍、世界边界 | +| [05-examples.md](../tutorial/05-examples.md) | 实战示例:PvP 竞技场、特效、烟花、波次刷怪 | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md b/Box3JS-NeoForge-1.21.1/docs/api/README_en.md deleted file mode 100644 index fb8de02..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/api/README_en.md +++ /dev/null @@ -1,398 +0,0 @@ -# Box3JS API Reference - -Box3JS is a Minecraft mod that lets you write server-side scripts in JavaScript/TypeScript, with optional client scripts for local UI, input, and audio. Each script project lives under `config/box3/script/`. - -## 5-Minute Quick Start - -```bash -# 1. Create a project in-game -/box3script create mygame - -# 2. Install dependencies and build -cd config/box3/script/mygame -npm install && npm run build - -# 3. Start the script -/box3script start mygame -``` - -Open `src/server/app.ts` and write: - -```js -world.onChat((entity, message) => { - if (message === "!hello") { - entity.player.directMessage("Hello, " + entity.player.name + "!"); - return false; // suppress chat message - } - return true; -}); - -console.log("Script loaded"); -``` - -After each edit, re-run `npm run build`, then use `/box3script reload mygame` to hot-reload. Client logic goes in `src/client/app.ts`; after build it becomes `dist/client.js` and is sent automatically to players who have the Box3JS client mod installed. - -> **New here?** [Quick Start Guide](../guide/getting-started_en.md) | **How it works:** [Architecture](../guide/architecture_en.md) | **JS vs Java:** [Comparison](../guide/js-vs-java_en.md) - -## API Domain Map - -Box3JS APIs are split into server-side, client-side, and shared runtimes. The type declarations are separated: `tsconfig.server.json` does not include client globals, and `tsconfig.client.json` does not include server globals such as `world` / `voxels`. - -| Domain | Runtime | Globals | Description | -|--------|---------|---------|-------------| -| **World & Entities** (server) | Server | `world` `voxels` | World control, blocks, event callbacks | -| **Players & Data** (server) | Server | `entity` `player` `storage` `db` `http` | `entity`/`player` come from callbacks or queries | -| **Client Interaction** (client) | Client | `audio` `client` `input` `ui` `chat` `gui` | Requires Box3JS client mod | -| **Cross-Side** | Both | `remoteChannel` | Server↔Client event communication | -| **Registries** | Compile-time | `registries` | Only in `/box3script compile` JAR mode | -| **Math & Utilities** | Both | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | Constructed with `new` | -| **Global Tools** | Both | `console` | Log output | - -> **Server APIs** manipulate the world, entities, players, and blocks. Scripts run on the server by default. -> **Client APIs** are only available with the Box3JS client mod installed, for UI, input, and audio. -> **Registry APIs** are only available in compiled JAR mode (`registries` is `undefined` in interpreted mode). - -## Read By Runtime - -| Entry | Use it for | Includes | -|-------|------------|----------| -| [Server API Overview](server_en.md) | Gameplay logic, events, blocks, entities, players, data, server-to-client events | `world`, `entity`, `player`, `voxels`, `storage`, `db`, `http`, `registries` | -| [Client API Overview](client_en.md) | Local UI, input, audio, chat helpers, local data, client-to-server events | `client`, `audio`, `input`, `ui`, `chat`, `gui`, `storage`, `db`, `http` | -| [Shared Utilities](math_en.md) | Math, color, and spatial code usable on both sides | `GameVector3`, `GameBounds3`, `GameRGBColor`, `GameRGBAColor`, `GameQuaternion` | - -## Find by Task — I want to... - -Find APIs by what you want to do, not by which global object they live on. - -### Messages & Chat - -| I want to... | Use this | -|-------------|----------| -| Broadcast to server | `world.say("message")` | -| Send private message | `player.directMessage("message")` | -| Show action bar text | `player.actionBar("message")` | -| Show screen title | `player.title("Title", "Subtitle")` | -| Handle chat input | `world.onChat((entity, msg) => { ... })` | - -### Player Properties - -| I want to... | Use this | -|-------------|----------| -| Get/set player position | `player.position` → `GameVector3` | -| Teleport player | `player.teleport(new GameVector3(x, y, z))` | -| Change health | `player.hp = 20` / `player.maxHp = 40` | -| Change hunger | `player.food = 20` / `player.saturation = 10` | -| Switch game mode | `player.gameMode = "creative"` | -| Toggle flight | `player.canFly = true` / `player.flying = true` | -| Kick player | `player.kick("reason")` | -| Run command as player | `player.runCommand("say hi")` | - -### Items & Equipment - -| I want to... | Use this | -|-------------|----------| -| Give a basic item | `player.giveItem("minecraft:diamond", 1)` | -| Give enchanted item | `player.giveEnchantedItem(...)` | -| Give named item | `player.giveNamedItem(...)` | -| Get held item | `player.getHeldItem()` | -| Clear inventory | `player.clearInventory()` | -| Set entity equipment | `entity.setEquipment("head", "iron_helmet")` | - -### Custom Registries (Blocks, Items & Sounds) 🆕 - -| I want to... | Use this | -|-------------|----------| -| Register custom blocks | `registries/blocks.json` (at compile time) | -| Register custom items | `registries/items.json` (at compile time) | -| Register custom sounds | `registries/sounds.json` (at compile time) | -| Register creative tabs | `registries/creativeTabs.json` (at compile time) | -| Get a registered block | `registries.getBlock("my_block")` | -| Get a registered item | `registries.getItem("chocolate")` | -| Get a registered sound | `registries.getSound("victory_fanfare")` | -| Give a custom block/item | `player.giveItem(block.itemId, 1)` | -| Place a custom block | `voxels.setVoxel(x, y, z, block.block)` | -| Play a custom sound (server) | `world.playSound(sound.soundId, x, y, z, 1, 1)` | -| Play a custom sound (client) | `audio.playSound("modId:soundId", 1.0, 1.0)` | - -> **Server-side only.** `registries` is `undefined` in client scripts. **Only available in `/box3script compile` JAR mode.** Client must also install the JAR for textures/models. See [registries_en.md](registries_en.md) - -### Block Operations - -| I want to... | Use this | -|-------------|----------| -| Read a block | `voxels.getVoxel(x, y, z)` | -| Place/replace a block | `voxels.setVoxel(x, y, z, "minecraft:stone")` | -| Fill a region | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | -| Listen for block breaks | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | -| Listen for block placement | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | - -### Entity Manipulation - -| I want to... | Use this | -|-------------|----------| -| Spawn an entity | `world.spawnEntity("minecraft:zombie", pos)` | -| Create with config | `world.createEntity({ type, position, ... })` | -| Set entity name | `entity.setNameTag("§cBoss")` | -| Toggle AI | `entity.setAI(true)` | -| Navigate to position | `entity.navigateTo(x, y, z, speed)` | -| Set attack target | `entity.setTarget(otherEntity)` | -| Check if player | `entity.isPlayer()` | -| Get entity type | `entity.entityType` | -| Get entity tags | `entity.tags()` / `entity.hasTag("boss")` | -| Query nearby entities | `world.entitiesInRadius(pos, radius)` | -| Query all entities | `world.querySelectorAll("*")` | - -### Client-side Features (requires Box3JS client mod) - -| I want to... | Use this | -|--------------|----------| -| Run every client tick | `client.onTick(() => { ... })` | -| Check key held down | `input.isKeyDown("space")` | -| Listen for key press | `input.onKeyPress("f", () => { ... })` | -| Play sound effect | `audio.playSound("pling", 1.0, 1.0)` | -| Play music | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | -| Stop all sounds | `audio.stopAll()` | -| Get/set volume | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | -| Show action bar text | `ui.showOverlay("text")` | -| Show screen title | `ui.showTitle("Title", "Subtitle")` | -| Send chat message | `chat.sendMessage("message")` | -| Receive chat messages | `chat.onMessage((msg, sender, isSystem) => { ... })` | -| Send event to server | `remoteChannel.sendServerEvent({ ... })` | -| Receive event from server | `remoteChannel.onClientEvent((event) => { ... })` | -| Client-side local storage | `storage.getDataStorage("key")` | - -### Visual Effects - -| I want to... | Use this | -|-------------|----------| -| Spawn particles | `world.spawnParticle("flame", x, y, z, ...)` | -| Particle circle | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | -| Firework | `world.launchFirework(x, y, z, "red", "large_ball")` | -| Lightning | `world.strikeLightning(x, y, z)` | -| Explosion | `world.explode(x, y, z, power)` | -| Play sound (global) | `world.playSound("pling", pos, 1.0, 1.0)` | -| Play sound (per-player) | `player.playSound("pling", 1.0, 1.0)` | - -### Potion Effects - -| I want to... | Use this | -|-------------|----------| -| Apply an effect | `entity.addEffect("minecraft:speed", duration, level, hideParticles)` | -| Clear all effects | `entity.clearEffects()` | - -### Event System - -| I want to... | Use this | -|-------------|----------| -| Run every tick | `world.onTick((info) => { ... })` | -| On player join | `world.onPlayerJoin((entity, tick) => { ... })` | -| On player leave | `world.onPlayerLeave((entity, tick) => { ... })` | -| On entity death | `world.onEntityDeath((entity, killer, tick) => { ... })` | -| On entity damaged | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | -| On right-click entity | `world.onInteract((entity, target, tick) => { ... })` | -| On right-click block | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | -| On button pressed | `world.onButtonPressed((entity, button, tick) => { ... })` | -| On player respawn | `world.onPlayerRespawn((entity, tick) => { ... })` | -| Run once after delay | `world.setTimeout(() => { ... }, ticks)` | -| Run on interval | `world.setInterval(() => { ... }, ticks)` | -| Cancel event listener | `token.cancel()` | -| Check if active | `token.active()` | - -### Data Persistence - -| I want to... | Use this | -|-------------|----------| -| Read/write JSON data | `storage.getDataStorage("key")` | -| SQL query | `db.sql("SELECT ...")` | -| SQL write | `db.sql("INSERT INTO ...")` | - -### HTTP Requests - -| I want to... | Use this | -|-------------|----------| -| GET request | `http.fetch("https://...")` | -| POST JSON | `http.fetch(url, { method: "POST", headers, body })` | -| Parse JSON | `resp.json()` or `{ responseType: "json" }` | -| Read text | `resp.text()` | -| Set timeout | `http.fetch(url, { timeout: 5000 })` | - -### Game Systems - -| I want to... | Use this | -|-------------|----------| -| Create scoreboard | `world.addScoreboard("name")` | -| Set score | `world.setScore("player", "board", 10)` | -| Display scoreboard | `world.showScoreboard("sidebar", "name")` | -| Show BossBar | `world.showBossbar("id", "title", 0.5, "red")` | -| Create team | `world.createTeam("teamName", "color")` | -| Join team | `world.joinTeam(entity, "teamName")` | -| Set world border | `world.borderSize = 500` | -| Shrink border | `world.shrinkBorder(100, 60)` | -| Change time | `world.time = 6000` | -| Set weather | `world.rainDensity = 0` / `world.clearWeather()` | -| Change game rule | `world.setGameRule("keepInventory", true)` | - -### Math Tools - -| I want to... | Use this | -|-------------|----------| -| 3D coordinate | `new GameVector3(x, y, z)` | -| Vector math | `v.add(other)`, `v.scale(n)`, `v.length()` | -| Bounding box | `new GameBounds3(min, max)` | -| Color | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | - -### Cross-Script Messaging - -| I want to... | Use this | -|-------------|----------| -| Send to another script | `world.sendMessage("projectName", data)` | -| Receive from other scripts | `world.onMessage((from, data) => { ... })` | - ---- - -## Global Objects - -| Object | Type | Description | -|--------|------|-------------| -| `world` | Server | World control, see [world_en.md](world_en.md) | -| `voxels` | Server | Block operations, see [voxels_en.md](voxels_en.md) | -| `entity` | Server value | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity_en.md](entity_en.md) | -| `player` | Server value | Player wrapper (via `entity.player`), see [player_en.md](player_en.md) | -| `storage` | Both | Data persistence, see [storage_en.md](storage_en.md) | -| `db` | Both | SQLite database, see [database_en.md](database_en.md) | -| `http` | Both | HTTP requests, see [http_en.md](http_en.md) | -| `audio` | Client | Client sound, music, volume control, see [client_en.md](client_en.md) | -| `client` | Client | Client lifecycle, see [client_en.md](client_en.md) | -| `input` | Client | Client keyboard input, see [client_en.md](client_en.md) | -| `ui` | Client | Client screen UI, see [client_en.md](client_en.md) | -| `chat` | Client | Client chat send/receive, see [client_en.md](client_en.md) | -| `gui` | Client | Custom container GUI, see [client_en.md](client_en.md) | -| `remoteChannel` | Both | Server↔client event channel, see [server_en.md](server_en.md) / [client_en.md](client_en.md) | -| `registries` | Server | Custom blocks, items & sounds (compiled mode), see [registries_en.md](registries_en.md) | -| `console` | Both | Console logging (`log`/`warn`/`error`/`debug`) | -| `GameVector3` | Both | 3D vector, see [math_en.md](math_en.md) | -| `GameBounds3` | Both | Bounding box, see [math_en.md](math_en.md) | -| `GameRGBColor` | Both | RGB color, see [math_en.md](math_en.md) | -| `GameRGBAColor` | Both | RGBA color, see [math_en.md](math_en.md) | -| `GameQuaternion` | Both | Quaternion, see [math_en.md](math_en.md) | - -## API Legend - -| Label | Meaning | -|-------|---------| -| ✅ **Box3 API** | Originates from the Box3 platform; naming and semantics match Box3 | -| ⬆ **MC Extension** | Not in original Box3; added using Minecraft-specific features | - -## Documentation Style - -Each API document should follow this structure. Use the same style when adding future APIs: - -1. State the runtime at the top: server, client, or shared. -2. List globals and core concepts before method details. -3. Use `object.method(parameters)` for method headings. -4. Document parameters in tables with name, type, default, and meaning. -5. Prefer TypeScript/JavaScript examples and identify server or client context when needed. -6. For cross-side APIs, always state the direction: server → client, or client → server. -7. If docs and types disagree, treat `types/server/index.d.ts` and `types/client/index.d.ts` as the source of truth, then update the docs. - -## Detailed Document Index - -| Document | Content | -|----------|---------| -| [server_en.md](server_en.md) | Server API overview: runtime boundary, globals, events, players/entities, blocks, data, cross-side communication | -| [world_en.md](world_en.md) | World state, events, scoreboard, bossbar, teams, border, particles, fireworks, lightning, sounds | -| [entity_en.md](entity_en.md) | Entity properties, AI, equipment, potion effects, pathfinding, tags, collisions | -| [player_en.md](player_en.md) | Inventory, messaging, flight, game mode, teleport, commands, XP | -| [voxels_en.md](voxels_en.md) | Block read/write, region fill, spawner control | -| [storage_en.md](storage_en.md) | Persistent data storage | -| [database_en.md](database_en.md) | SQLite database API | -| [http_en.md](http_en.md) | HTTP request API | -| [client_en.md](client_en.md) | Client API: lifecycle, keyboard, screen UI, chat, GUI, remoteChannel, client-side storage | -| [registries_en.md](registries_en.md) | Custom blocks, items & sounds (blocks.json, items.json, sounds.json, creativeTabs.json) | -| [math_en.md](math_en.md) | GameVector3, GameBounds3, GameRGBColor, GameRGBAColor, GameQuaternion | -| [commands_en.md](commands_en.md) | `/box3script` command reference | - -## File Modules — TypeScript Build Pipeline - -Projects created with `/box3script create` come with a complete TS build environment: - -``` -config/box3/script/mygame/ -├── package.json ← esbuild + Babel + @babel/preset-typescript -├── tsconfig.base.json ← Shared TS compiler options -├── tsconfig.server.json ← Server-side TS config -├── tsconfig.client.json ← Client-side TS config -├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ -├── types/ -│ ├── shared.d.ts ← Shared types (server & client) -│ ├── server/ -│ │ ├── index.d.ts ← Server type entry point -│ │ ├── server.d.ts -│ │ ├── entity.d.ts -│ │ ├── player.d.ts -│ │ ├── world.d.ts -│ │ └── voxels.d.ts -│ └── client/ -│ ├── index.d.ts ← Client type entry point -│ ├── client.d.ts -│ ├── audio.d.ts -│ ├── input.d.ts -│ ├── ui.d.ts -│ ├── chat.d.ts -│ └── gui.d.ts -├── src/ -│ ├── server/ -│ │ ├── app.ts ← Server entry point -│ │ └── ... -│ └── client/ -│ ├── app.ts ← Client entry point -│ └── ... -└── dist/ - ├── server.js ← Server compiled output - ├── client.js ← Client compiled output - └── -.jar ← Standalone JAR (/box3script compile) -``` - -Run `npm run build` to build. Use `/box3script watch` to enable file watching for auto hot-reload. - -## Deployment - -When ready to distribute, compile your script into a **standalone JAR mod** that runs on any NeoForge server alongside Box3JS: - -``` -/box3script compile -``` - -Outputs `-.jar` (metadata read from `package.json`: name, displayName, version, description, author, license, homepage, logoFile). Drop it into `mods/` and start the server. - -See [full command reference →](commands_en.md#box3script-compile-project) - -## Tick Conversion - -| Duration | Ticks | -|----------|-------| -| 1 second | 20 | -| 5 seconds | 100 | -| 30 seconds | 600 | -| 1 minute | 1200 | -| 5 minutes | 6000 | - -## Deep Dive - -| Doc | Content | -|-----|---------| -| [Quick Start](../guide/getting-started_en.md) | Setup, first script, dev cycle, debugging, deployment | -| [Architecture](../guide/architecture_en.md) | Rhino engine, scopes, event callbacks, build pipeline, network | -| [JS vs Java](../guide/js-vs-java_en.md) | Box3JS scripting vs native Java modding comparison | - -## Tutorials - -Learn Box3JS from scratch with the tutorial series in `docs/tutorial/`: - -| Tutorial | Content | -|----------|---------| -| [01-basics.md](../tutorial/01-basics.md) | From zero: first script, chat commands, timers | -| [02-player-items.md](../tutorial/02-player-items.md) | Player controls: teleport, items, potion effects, game modes | -| [03-events-entities.md](../tutorial/03-events-entities.md) | Events & entities: AI, combat, patrols | -| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | Advanced: scoreboard, BossBar, teams, world border | -| [05-examples.md](../tutorial/05-examples.md) | Real-world: PvP arena, effects, fireworks, wave mobs | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client.md b/Box3JS-NeoForge-1.21.1/docs/api/client.md index c0ce264..56c528a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/client.md @@ -17,8 +17,9 @@ | `gui` | `GameGUI` | 自定义容器 GUI 界面 | | `remoteChannel` | `RemoteChannel` | 客户端 ↔ 服务端事件通信 | -> **前置条件:** 客户端必须安装 Box3JS mod,服务端必须启用该项目的客户端脚本并通过网络自动下发。 -> 客户端脚本放在 `src/client/` 目录下,服务端脚本放在 `src/server/` 目录下。客户端类型入口是 `types/client/index.d.ts`,不会包含服务端 `world` / `voxels` API。 +::: info 前置条件 +客户端必须安装 Box3JS mod,服务端必须启用该项目的客户端脚本并通过网络自动下发。客户端脚本放在 `src/client/` 目录下,服务端脚本放在 `src/server/` 目录下。客户端类型入口是 `types/client/index.d.ts`,不会包含服务端 `world` / `voxels` API。 +::: 客户端脚本不能直接修改服务端世界。需要改变方块、玩家、实体或计分板时,应发送事件给服务端: @@ -104,15 +105,19 @@ audio.setVolume("player", 0.8); ### client.onTick(callback) -注册客户端每 tick 回调(每秒 20 次)。无参数,无返回值。 +注册客户端每 tick 回调(每秒 20 次)。无参数,返回 `GameEventHandlerToken`,可用 `cancel()` 取消监听。 ```js -client.onTick(() => { +const token = client.onTick(() => { // 每帧更新逻辑 }); + +// token.cancel(); ``` -> **注意:** 服务端也有 `world.onTick()`,但参数为 `TickInfo` 对象。客户端 `client.onTick()` 无参数。 +::: info 注意 +服务端也有 `world.onTick()`,但参数为 `TickInfo` 对象。客户端 `client.onTick()` 无参数。 +::: ### client.getFPS() @@ -162,6 +167,70 @@ if (!info.isLocal) { } ``` +### 雾效控制 (Fog Control) + +Box3JS 客户端可以覆盖 Minecraft 的雾颜色和距离,实现类似 Box3 `world.fogColor` / `world.maxFog` 的效果。 + +### client.getFogColor() + +获取当前自定义雾颜色。未设置时返回 `null`。 + +```js +var color = client.getFogColor(); +if (color) { + console.log("Fog color: " + color.r + ", " + color.g + ", " + color.b); +} +``` + +### client.setFogColor(r, g, b) + +设置雾颜色(RGB 0-255)。 + +| 参数 | 类型 | 说明 | +|------|--------|------------| +| `r` | number | 红色 (0-255) | +| `g` | number | 绿色 (0-255) | +| `b` | number | 蓝色 (0-255) | + +```js +// 红色迷雾效果 +client.setFogColor(255, 50, 50); +``` + +### client.setFogStartDistance(distance) + +设置雾起始距离(方块)。低于此距离完全透明。 + +| 参数 | 类型 | 说明 | +|------------|--------|--------------------| +| `distance` | number | 雾起始距离(方块) | + +```js +// 雾从 10 个方块距离外开始 +client.setFogStartDistance(10); +``` + +### client.setFogEndDistance(distance) + +设置雾结束距离(方块),对应 Box3 的 `maxFog`。超过此距离完全被雾遮挡。 + +| 参数 | 类型 | 说明 | +|------------|--------|--------------------| +| `distance` | number | 雾结束距离(方块) | + +```js +// 50 格以外完全被雾遮挡 +client.setFogEndDistance(50); +``` + +### client.resetFog() + +重置雾效果为 Minecraft 默认值。 + +```js +client.resetFog(); +``` + ## input — 键盘输入 ### input.isKeyDown(key) @@ -240,7 +309,7 @@ token.cancel(); | 功能键 | `f1`–`f12` | | 方向键 | `up`, `down`, `left`, `right` | | 特殊键 | `space`, `enter`, `escape`, `tab`, `backspace`, `delete` | -| 修饰键 | `left_shift`, `right_shift`, `left_ctrl`, `right_ctrl`, `left_alt`, `right_alt` | +| 修饰键 | `shift`, `left_shift`, `right_shift`, `ctrl`, `left_ctrl`, `right_ctrl`, `alt`, `left_alt`, `right_alt` | ## ui — 屏幕 UI @@ -451,8 +520,9 @@ remoteChannel.onClientEvent((event) => { }); ``` -> 服务端对应 API 为 `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`。 -> 详见 `server.d.ts` 中的类型声明。 +::: info +服务端对应 API 为 `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`。详见 `server.d.ts` 中的类型声明。 +::: ## storage — 客户端存储 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands.md b/Box3JS-NeoForge-1.21.1/docs/api/commands.md index 6c89a4b..8403f50 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/commands.md @@ -8,13 +8,13 @@ 显示项目状态概览。 -``` +```js /box3script ``` 输出示例: -``` +```text ══ Box3JS Script Engine ══ Watch: ● Active Sandbox: ● 1 project(s) @@ -39,7 +39,7 @@ 创建新的 TypeScript 脚本项目。生成完整的 TS 脚手架,默认**禁用**。 -``` +```js /box3script create mygame ``` @@ -56,7 +56,7 @@ npm install && npm run build 启用并加载项目。**不带参数** = 启用全部。**带项目名** = 只启用指定项目。**`all`** = 显式启用全部。 -``` +```js /box3script start # 启用全部 /box3script start all # 启用全部(同无参数) /box3script start mygame # 只启用 mygame @@ -66,7 +66,7 @@ npm install && npm run build 禁用并卸载项目。**不带参数** = 禁用全部。**带项目名** = 只禁用指定项目。**`all`** = 显式禁用全部。 -``` +```js /box3script stop # 禁用全部 /box3script stop all # 禁用全部(同无参数) /box3script stop mygame # 只禁用 mygame @@ -76,7 +76,7 @@ npm install && npm run build 重载脚本。**不带参数** = 停止全部,重新加载所有已启用项目。**带项目名** = 重载指定项目。 -``` +```js /box3script reload # 重载全部已启用项目 /box3script reload mygame # 只重载 mygame ``` @@ -87,7 +87,7 @@ npm install && npm run build 开启/关闭文件监听。监听所有项目的 `dist/` 目录,`.js` 文件变化时自动重载对应项目。 -``` +```js /box3script watch # 切换 开/关 ``` @@ -95,7 +95,7 @@ npm install && npm run build 切换沙盒模式。开启后自动追踪该项目所有的方块/实体/世界状态变更,关闭时回滚并显示摘要。 -``` +```js /box3script sandbox mygame # 切换 开/关 ``` @@ -103,7 +103,7 @@ npm install && npm run build 典型工作流: -``` +```js /box3script sandbox mygame # 开启沙盒 /box3script start mygame # 启用项目 # ... 测试 ... @@ -112,19 +112,25 @@ npm install && npm run build /box3script sandbox mygame # 关闭沙盒 → 回滚全部修改 ``` -> **注意:** 沙盒仅追踪通过脚本 API 修改的方块(`setVoxel`/`setVoxelId`/`fillVoxel`),手动挖掘不受影响。 +::: warning +沙盒仅追踪通过脚本 API 修改的方块(`setVoxel`/`setVoxelId`/`fillVoxel`),手动挖掘不受影响。 +::: ### `/box3script compile ` 将脚本项目编译为**轻量独立 JAR 模组**(~50KB),依赖 Box3JS 模组提供 Rhino 运行时和 API 绑定。 -``` +```js /box3script compile mygame ``` -> **依赖:** 脚本 JAR 不包含 Rhino 或 Box3JS API 类,需将 Box3JS 模组(`box3js`)一同放入 `mods/`。 +::: warning 依赖 +脚本 JAR 不包含 Rhino 或 Box3JS API 类,需将 Box3JS 模组(`box3js`)一同放入 `mods/`。 +::: -> **自定义注册表:** 如果存在 `registries/blocks.json`、`items.json`、`sounds.json`、`creativeTabs.json` 和 `assets/`,编译时会自动注册方块/物品/音效,并将资源打包进 JAR。客户端也需安装该 JAR 才能正常渲染。详见 [registries.md](registries.md)。 +::: info 自定义注册表 +如果存在 `registries/blocks.json`、`items.json`、`sounds.json`、`creativeTabs.json` 和 `assets/`,编译时会自动注册方块/物品/音效,并将资源打包进 JAR。客户端也需安装该 JAR 才能正常渲染。详见 [registries.md](registries.md)。 +::: 编译时**从 `package.json` 读取以下字段**写入 `neoforge.mods.toml`: @@ -140,7 +146,9 @@ npm install && npm run build | `bugs.url` | `issueTrackerURL` | 问题反馈链接 | | `logoFile` | `logoFile` | 模组图标(项目中的 PNG 路径,打包为 `logo.png`) | -> **`logoFile` 使用说明:** 填写项目根目录下的 PNG 文件相对路径(如 `"logoFile": "logo.png"`),编译时自动打包为 JAR 根目录的 `logo.png`,无需在 `neoforge.mods.toml` 中手动配置。NeoForge 建议尺寸 128×128 或 256×256,仅支持 PNG 格式。不填则使用默认模组图标。 +::: tip logoFile 使用说明 +填写项目根目录下的 PNG 文件相对路径(如 `"logoFile": "logo.png"`),编译时自动打包为 JAR 根目录的 `logo.png`,无需在 `neoforge.mods.toml` 中手动配置。NeoForge 建议尺寸 128×128 或 256×256,仅支持 PNG 格式。不填则使用默认模组图标。 +::: 输出文件名格式:`dist/-.jar`。编译在后台线程运行,不阻塞服务器 tick,完成后聊天栏通知输出路径。 @@ -151,7 +159,7 @@ npm install && npm run build **输出 JAR 内容:** -``` +```text mygame-1.0.0.jar ├── META-INF/neoforge.mods.toml ← 模组元数据(依赖 box3js) ├── logo.png ← 模组图标(如有指定) @@ -168,7 +176,7 @@ mygame-1.0.0.jar **部署:** 将脚本 JAR 与 Box3JS 模组一起放入 `mods/`: -``` +```text mods/ ├── box3js-1.0.0.jar ← Box3JS 主模组 └── mygame-1.0.0.jar ← 编译的脚本模组 @@ -185,7 +193,9 @@ mods/ | 热重载 | 支持 | 不支持(JAR 重启才生效) | | 适用场景 | 开发调试 | 分发部署 | -> **注意:** 编译后的 JAR 是标准 NeoForge mod,由 NeoForge mod loader 管理,**不受** `/box3script start/stop/reload` 控制。多个编译 JAR 可同时放入 `mods/`,各自独立运行,互不干扰。 +::: warning +编译后的 JAR 是标准 NeoForge mod,由 NeoForge mod loader 管理,**不受** `/box3script start/stop/reload` 控制。多个编译 JAR 可同时放入 `mods/`,各自独立运行,互不干扰。 +::: ## 配置文件 @@ -200,7 +210,7 @@ mods/ ## 脚本目录结构 -``` +```text config/box3/ ├── scripts.json ← 项目开关配置 ├── script/ ← 脚本目录 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database.md b/Box3JS-NeoForge-1.21.1/docs/api/database.md index 2ff883e..6bb703c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/database.md @@ -2,7 +2,9 @@ Box3JS 通过全局 `db` 对象提供 SQLite 数据库能力,无需手动管理连接。 -> **运行环境:** 服务端和客户端都可用。服务端数据库位于 `config/box3/data/.db`;客户端数据库位于本地游戏目录的 `box3/client-db/.db`。两端数据库互不共享,需要同步数据时请使用 `remoteChannel`。 +::: info 运行环境 +服务端和客户端都可用。服务端数据库位于 `config/box3/data/.db`;客户端数据库位于本地游戏目录的 `box3/client-db/.db`。两端数据库互不共享,需要同步数据时请使用 `remoteChannel`。 +::: ## 依赖与降级行为 @@ -16,10 +18,14 @@ db API requires SQLite JDBC driver. Install the minecraft-sqlite-jdbc mod, then 安装 `minecraft-sqlite-jdbc` 并重启服务器后,`db` API 即可恢复可用。 -> **NeoForge 开发环境提示:** -> -> - 请将 `minecraft-sqlite-jdbc` 放到 `run/mods/`。 -> - 模组文件必须是 `.jar`(例如 `xxx.jar`),不要使用 `.zip`,否则不会被 NeoForge 加载。 +::: warning NeoForge 开发环境 +- 请将 `minecraft-sqlite-jdbc` 放到 `run/mods/`。 +- 模组文件必须是 `.jar`(例如 `xxx.jar`),不要使用 `.zip`,否则不会被 NeoForge 加载。 +::: + +## `db.isAvailable()` + +检查 SQLite JDBC 驱动是否可用。不可用时,`db.sql(...)` 会返回安全的空错误结果或显示清晰提示,脚本可用该方法提前降级。 ## `db.sql(sql, ...params)` @@ -275,15 +281,23 @@ db.sql( ); ``` -> **重要:只有值(值)用 `${}`,标识符(表名、列名)不能做绑定参数。** -> -> ```ts -> // ✅ 正确 — 表名硬编码在模板字符串中 -> db.sql`SELECT * FROM players WHERE name = ${name}`; -> -> // ❌ 错误 — 表名不能用占位符,会报 SQL syntax error -> db.sql`SELECT * FROM ${table} WHERE name = ${name}`; -> ``` +::: warning +只有值用 `${}`,标识符(表名、列名)不能做绑定参数。 +::: + +```ts +// ✅ 正确 — 表名硬编码在模板字符串中 +db.sql`SELECT * FROM players WHERE name = ${name}`; +``` + +::: danger 常见错误 +表名不能用占位符,会报 SQL syntax error: + +```js +// ❌ 错误 +db.sql`SELECT * FROM ${table} WHERE name = ${name}`; +``` +::: ## Rhino 兼容性注意事项 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md index 564e4f1..1ce6169 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -1,6 +1,6 @@ # entity — 实体 API -`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。通过 `world.spawnEntity()`、`world.createEntity()`、`world.querySelector()`、`world.searchBox()`、`world.entitiesInRadius()` 或事件回调参数获取。 +`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。 通过 `entity.player` 可获取该实体对应的 `player` 对象(仅当是玩家时非 null)。 @@ -415,7 +415,9 @@ entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); entity.setAttribute("minecraft:generic.armor", 10); ``` -> 注意:`maxHp` / `hp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性。仅当需要访问未封装的属性时才用 `setAttribute`。 +::: tip +`maxHp` / `hp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性。仅当需要访问未封装的属性时才用 `setAttribute`。 +::: ## 生命周期 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/http.md b/Box3JS-NeoForge-1.21.1/docs/api/http.md index 91c8eed..a1339f9 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/http.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/http.md @@ -1,8 +1,10 @@ # HTTP API -Box3JS 通过全局 `http` 对象提供 HTTP 请求能力,支持全部 HTTP 方法、超时、自定义请求头、自动解析、二进制上传,以及同步/异步两种调用方式。 +通过全局 `http` 对象发起 HTTP 请求。 -> **运行环境:** 服务端和客户端都可用。服务端同步请求会阻塞服务器 tick,避免在高频回调中执行长时间请求。客户端同步请求会阻塞客户端渲染/逻辑线程。**异步请求**(`async: true`)通过回调接收结果。 +::: info 运行环境 +服务端和客户端都可用。同步请求会阻塞当前线程,避免在高频回调中执行长时间请求。**异步请求**(`async: true`)通过回调接收结果。 +::: ## `http.fetch(url, options?)` @@ -27,9 +29,11 @@ Box3JS 通过全局 `http` 对象提供 HTTP 请求能力,支持全部 HTTP | `onResponse` | `function` | — | 异步请求成功回调,参数为 `GameHttpFetchResponse` | | `onError` | `function` | — | 异步请求失败回调,参数为错误信息字符串 | -> 设置 `responseType` 后,解析结果可直接通过 `resp.data` 获取,无需手动调 `resp.json()` 等。 -> -> 异步模式下 `fetch()` 返回 `null`,结果通过回调接收。 +::: info +设置 `responseType` 后,解析结果可直接通过 `resp.data` 获取,无需手动调 `resp.json()` 等。 + +异步模式下 `fetch()` 返回 `null`,结果通过回调接收。 +::: ## GameHttpFetchResponse diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md index 5d730f9..5344dce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -9,14 +9,14 @@ ### 构造 ```js -var v = new GameVector3(); // 零向量 (0, 0, 0) +var v = new GameVector3(); // 零向量 (0, 0, 0) var v = new GameVector3(x, y, z); // 指定坐标 ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| ----- | -------- | ------------------------- | | `v.x` | `number` | X 分量 (东西方向),可读写 | | `v.y` | `number` | Y 分量 (上下方向),可读写 | | `v.z` | `number` | Z 分量 (南北方向),可读写 | @@ -25,57 +25,57 @@ var v = new GameVector3(x, y, z); // 指定坐标 #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.set(x, y, z)` | `GameVector3` | 设置所有分量 | -| `v.copy(w)` | `GameVector3` | 从 `w` 复制所有分量 | -| `v.addEq(w)` | `GameVector3` | 原地加法:`v += w` | -| `v.subEq(w)` | `GameVector3` | 原地减法:`v -= w` | -| `v.mulEq(w)` | `GameVector3` | 原地逐分量乘法:`v.x *= w.x` … | -| `v.divEq(w)` | `GameVector3` | 原地逐分量除法,除以 0 跳过该分量 | -| `v.scaleEq(n)` | `GameVector3` | 原地标量乘法:`v.x *= n` … | -| `v.negEq()` | `GameVector3` | 原地取反:`v = -v` | +| 方法 | 返回值 | 说明 | +| ---------------- | ------------- | --------------------------------- | +| `v.set(x, y, z)` | `GameVector3` | 设置所有分量 | +| `v.copy(w)` | `GameVector3` | 从 `w` 复制所有分量 | +| `v.addEq(w)` | `GameVector3` | 原地加法:`v += w` | +| `v.subEq(w)` | `GameVector3` | 原地减法:`v -= w` | +| `v.mulEq(w)` | `GameVector3` | 原地逐分量乘法:`v.x *= w.x` … | +| `v.divEq(w)` | `GameVector3` | 原地逐分量除法,除以 0 跳过该分量 | +| `v.scaleEq(n)` | `GameVector3` | 原地标量乘法:`v.x *= n` … | +| `v.negEq()` | `GameVector3` | 原地取反:`v = -v` | #### 创建新向量 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.clone()` | `GameVector3` | 深拷贝,返回相同值的独立新向量 | -| `v.add(w)` | `GameVector3` | 向量加法:`v + w` | -| `v.sub(w)` | `GameVector3` | 向量减法:`v - w` | -| `v.mul(w)` | `GameVector3` | 逐分量乘法 | -| `v.div(w)` | `GameVector3` | 逐分量除法,除以 0 得 0 | -| `v.scale(n)` | `GameVector3` | 标量乘法:每个分量乘以 `n` | -| `v.cross(w)` | `GameVector3` | 叉积:`v × w` | -| `v.normalize()` | `GameVector3` | 单位化,零向量返回 `(0,0,0)` | -| `v.lerp(w, t)` | `GameVector3` | 线性插值:`t=0` 为自身,`t=1` 为 `w` | -| `v.towards(w)` | `GameVector3` | 指向 `w` 的方向向量 (已单位化) | -| `v.max(w)` | `GameVector3` | 逐分量取较大值 | -| `v.min(w)` | `GameVector3` | 逐分量取较小值 | -| `v.neg()` | `GameVector3` | 取反:`-v` | -| `v.moveTowards(target, maxDelta)` | `GameVector3` | 向目标移动不超过 `maxDelta` 距离 | -| `v.floor()` | `GameVector3` | 逐分量向下取整 | -| `v.ceil()` | `GameVector3` | 逐分量向上取整 | -| `v.clampLength(max)` | `GameVector3` | 限制长度至 `max`,超长则等比缩放 | +| 方法 | 返回值 | 说明 | +| --------------------------------- | ------------- | ------------------------------------ | +| `v.clone()` | `GameVector3` | 深拷贝,返回相同值的独立新向量 | +| `v.add(w)` | `GameVector3` | 向量加法:`v + w` | +| `v.sub(w)` | `GameVector3` | 向量减法:`v - w` | +| `v.mul(w)` | `GameVector3` | 逐分量乘法 | +| `v.div(w)` | `GameVector3` | 逐分量除法,除以 0 得 0 | +| `v.scale(n)` | `GameVector3` | 标量乘法:每个分量乘以 `n` | +| `v.cross(w)` | `GameVector3` | 叉积:`v × w` | +| `v.normalize()` | `GameVector3` | 单位化,零向量返回 `(0,0,0)` | +| `v.lerp(w, t)` | `GameVector3` | 线性插值:`t=0` 为自身,`t=1` 为 `w` | +| `v.towards(w)` | `GameVector3` | 指向 `w` 的方向向量 (已单位化) | +| `v.max(w)` | `GameVector3` | 逐分量取较大值 | +| `v.min(w)` | `GameVector3` | 逐分量取较小值 | +| `v.neg()` | `GameVector3` | 取反:`-v` | +| `v.moveTowards(target, maxDelta)` | `GameVector3` | 向目标移动不超过 `maxDelta` 距离 | +| `v.floor()` | `GameVector3` | 逐分量向下取整 | +| `v.ceil()` | `GameVector3` | 逐分量向上取整 | +| `v.clampLength(max)` | `GameVector3` | 限制长度至 `max`,超长则等比缩放 | #### 数值计算 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.dot(w)` | `number` | 点积 (内积):`v · w` | -| `v.mag()` | `number` | 向量长度 (模) | -| `v.sqrMag()` | `number` | 长度平方,比 `mag()` 更快 | -| `v.distance(w)` | `number` | 与 `w` 的欧几里得距离 | -| `v.angle(w)` | `number` | 与 `w` 的夹角 (弧度, 0–π) | +| 方法 | 返回值 | 说明 | +| ------------------ | -------- | --------------------------------------- | +| `v.dot(w)` | `number` | 点积 (内积):`v · w` | +| `v.mag()` | `number` | 向量长度 (模) | +| `v.sqrMag()` | `number` | 长度平方,比 `mag()` 更快 | +| `v.distance(w)` | `number` | 与 `w` 的欧几里得距离 | +| `v.angle(w)` | `number` | 与 `w` 的夹角 (弧度, 0–π) | | `v.sqrDistance(w)` | `number` | 与 `w` 的距离平方,比 `distance()` 更快 | #### 比较 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `v.equals(w)` | `boolean` | 近似相等,容差 1e-6 | -| `v.exactEquals(w)` | `boolean` | 精确相等,分量完全一致 | -| `v.isZero()` | `boolean` | 是否为 (接近) 零向量,容差 1e-6 | +| 方法 | 返回值 | 说明 | +| ------------------ | --------- | ------------------------------- | +| `v.equals(w)` | `boolean` | 近似相等,容差 1e-6 | +| `v.exactEquals(w)` | `boolean` | 精确相等,分量完全一致 | +| `v.isZero()` | `boolean` | 是否为 (接近) 零向量,容差 1e-6 | ```js var pos = new GameVector3(0, 100, 0); @@ -93,8 +93,8 @@ var angle = pos.angle(target); // 弧度 // 比较 var a = new GameVector3(1, 2, 3); var b = new GameVector3(1.0000001, 2.0000001, 3.0000001); -a.equals(b); // true (容差内) -a.exactEquals(b); // false +a.equals(b); // true (容差内) +a.exactEquals(b); // false // 传送实体 (LiveVec3) entity.position.set(0, 100, 0); @@ -117,8 +117,6 @@ var v = new GameVector3(1, 2, 3); v.toString(); // "GameVector3(1.0, 2.0, 3.0)" ``` ---- - ## GameBounds3 轴对齐包围盒 (AABB),由两个对角顶点 `lo` (最小角) 和 `hi` (最大角) 定义。 @@ -128,41 +126,41 @@ v.toString(); // "GameVector3(1.0, 2.0, 3.0)" ```js var bounds = new GameBounds3( new GameVector3(-1, 0, -1), // lo (最小角) - new GameVector3(1, 2, 1), // hi (最大角) + new GameVector3(1, 2, 1), // hi (最大角) ); ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| ----------- | ------------- | ----------------------------------- | | `bounds.lo` | `GameVector3` | 最小角 (三个分量均为最小值),可读写 | | `bounds.hi` | `GameVector3` | 最大角 (三个分量均为最大值),可读写 | ### 实例方法 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | 原地设置所有边界,返回自身 | -| `bounds.copy(b)` | `GameBounds3` | 原地复制 `b` 的值,返回自身 | -| `bounds.intersects(other)` | `boolean` | 是否与 `other` 相交 | -| `bounds.intersect(other)` | `GameBounds3 \| null` | 计算交集包围盒,不相交返回 `null` | -| `bounds.contains(v)` | `boolean` | 点 `v` 是否在包围盒内 (含边界) | -| `bounds.containsBounds(b)` | `boolean` | 是否完全包含另一个包围盒 `b` | -| `bounds.center()` | `GameVector3` | 包围盒中心点 | -| `bounds.size()` | `GameVector3` | 包围盒尺寸 (宽, 高, 深) | -| `bounds.expand(delta)` | `GameBounds3` | 各面向外扩展 `delta`,返回新包围盒 | -| `bounds.expandEq(delta)` | `GameBounds3` | 原地各面向外扩展 `delta`,返回自身 | -| `bounds.growToInclude(v)` | `GameBounds3` | 原地扩展以包含点 `v`,返回自身 | -| `bounds.closestPoint(v)` | `GameVector3` | 包围盒上离点 `v` 最近的点 | -| `bounds.move(offset)` | `GameBounds3` | 平移 `offset`,返回新包围盒 | -| `bounds.moveEq(offset)` | `GameBounds3` | 原地平移 `offset`,返回自身 | +| 方法 | 返回值 | 说明 | +| ------------------------------------------ | --------------------- | ---------------------------------- | +| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | 原地设置所有边界,返回自身 | +| `bounds.copy(b)` | `GameBounds3` | 原地复制 `b` 的值,返回自身 | +| `bounds.intersects(other)` | `boolean` | 是否与 `other` 相交 | +| `bounds.intersect(other)` | `GameBounds3 \| null` | 计算交集包围盒,不相交返回 `null` | +| `bounds.contains(v)` | `boolean` | 点 `v` 是否在包围盒内 (含边界) | +| `bounds.containsBounds(b)` | `boolean` | 是否完全包含另一个包围盒 `b` | +| `bounds.center()` | `GameVector3` | 包围盒中心点 | +| `bounds.size()` | `GameVector3` | 包围盒尺寸 (宽, 高, 深) | +| `bounds.expand(delta)` | `GameBounds3` | 各面向外扩展 `delta`,返回新包围盒 | +| `bounds.expandEq(delta)` | `GameBounds3` | 原地各面向外扩展 `delta`,返回自身 | +| `bounds.growToInclude(v)` | `GameBounds3` | 原地扩展以包含点 `v`,返回自身 | +| `bounds.closestPoint(v)` | `GameVector3` | 包围盒上离点 `v` 最近的点 | +| `bounds.move(offset)` | `GameBounds3` | 平移 `offset`,返回新包围盒 | +| `bounds.moveEq(offset)` | `GameBounds3` | 原地平移 `offset`,返回自身 | ### 静态方法 ```js // 从 GameVector3 数组创建最小包围盒 -var points = [new GameVector3(0,0,0), new GameVector3(5,10,3)]; +var points = [new GameVector3(0, 0, 0), new GameVector3(5, 10, 3)]; var box = GameBounds3.fromPoints(points); // 返回 GameBounds3 或 null ``` @@ -182,8 +180,6 @@ if (bounds.contains(player.position)) { } ``` ---- - ## GameRGBColor RGB 颜色,三个通道范围 0.0–1.0。 @@ -191,15 +187,15 @@ RGB 颜色,三个通道范围 0.0–1.0。 ### 构造 ```js -var red = new GameRGBColor(1, 0, 0); -var blue = new GameRGBColor(0, 0, 1); -var gray = new GameRGBColor(0.5, 0.5, 0.5); +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| --------- | -------- | ---------------------- | | `color.r` | `number` | 红色通道 (0–1),可读写 | | `color.g` | `number` | 绿色通道 (0–1),可读写 | | `color.b` | `number` | 蓝色通道 (0–1),可读写 | @@ -208,29 +204,29 @@ var gray = new GameRGBColor(0.5, 0.5, 0.5); #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.set(r, g, b)` | `GameRGBColor` | 设置所有通道 | -| `c.copy(o)` | `GameRGBColor` | 从另一个颜色复制所有通道 | -| `c.addEq(o)` | `GameRGBColor` | 原地加法:`c += o` | -| `c.subEq(o)` | `GameRGBColor` | 原地减法:`c -= o` | -| `c.mulEq(o)` | `GameRGBColor` | 原地逐通道乘法 | -| `c.divEq(o)` | `GameRGBColor` | 原地逐通道除法,除以 0 跳过该通道 | -| `c.scaleEq(n)` | `GameRGBColor` | 原地标量乘法:每个通道乘以 `n` | +| 方法 | 返回值 | 说明 | +| ---------------- | -------------- | --------------------------------- | +| `c.set(r, g, b)` | `GameRGBColor` | 设置所有通道 | +| `c.copy(o)` | `GameRGBColor` | 从另一个颜色复制所有通道 | +| `c.addEq(o)` | `GameRGBColor` | 原地加法:`c += o` | +| `c.subEq(o)` | `GameRGBColor` | 原地减法:`c -= o` | +| `c.mulEq(o)` | `GameRGBColor` | 原地逐通道乘法 | +| `c.divEq(o)` | `GameRGBColor` | 原地逐通道除法,除以 0 跳过该通道 | +| `c.scaleEq(n)` | `GameRGBColor` | 原地标量乘法:每个通道乘以 `n` | #### 创建新颜色 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.clone()` | `GameRGBColor` | 深拷贝 | -| `c.add(o)` | `GameRGBColor` | 逐通道加法 | -| `c.sub(o)` | `GameRGBColor` | 逐通道减法 | -| `c.mul(o)` | `GameRGBColor` | 逐通道乘法 | -| `c.div(o)` | `GameRGBColor` | 逐通道除法,除以 0 得 0 | +| 方法 | 返回值 | 说明 | +| -------------- | -------------- | ------------------------------------ | +| `c.clone()` | `GameRGBColor` | 深拷贝 | +| `c.add(o)` | `GameRGBColor` | 逐通道加法 | +| `c.sub(o)` | `GameRGBColor` | 逐通道减法 | +| `c.mul(o)` | `GameRGBColor` | 逐通道乘法 | +| `c.div(o)` | `GameRGBColor` | 逐通道除法,除以 0 得 0 | | `c.lerp(o, t)` | `GameRGBColor` | 线性插值:`t=0` 为自身,`t=1` 为 `o` | -| `c.scale(n)` | `GameRGBColor` | 标量乘法:每个通道乘以 `n` | -| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | -| `c.toRGBA()` | `string` | 转为 CSS 格式:`"rgba(r,g,b,1.0)"` | +| `c.scale(n)` | `GameRGBColor` | 标量乘法:每个通道乘以 `n` | +| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | +| `c.toRGBA()` | `string` | 转为 CSS 格式:`"rgba(r,g,b,1.0)"` | ### 静态方法 @@ -244,8 +240,6 @@ var randomColor = GameRGBColor.random(); // 每个通道 0–1 随机值 new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" ``` ---- - ## GameRGBAColor 带 Alpha 通道的颜色,四个分量范围 0.0–1.0。 @@ -254,45 +248,45 @@ new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" ```js var semiRed = new GameRGBAColor(1, 0, 0, 0.5); -var opaque = new GameRGBAColor(0, 1, 0, 1.0); +var opaque = new GameRGBAColor(0, 1, 0, 1.0); ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| -| `color.r` | `number` | 红色通道 (0–1),可读写 | -| `color.g` | `number` | 绿色通道 (0–1),可读写 | -| `color.b` | `number` | 蓝色通道 (0–1),可读写 | +| 属性 | 类型 | 说明 | +| --------- | -------- | ---------------------------- | +| `color.r` | `number` | 红色通道 (0–1),可读写 | +| `color.g` | `number` | 绿色通道 (0–1),可读写 | +| `color.b` | `number` | 蓝色通道 (0–1),可读写 | | `color.a` | `number` | Alpha 不透明度 (0–1),可读写 | ### 实例方法 #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.set(r, g, b, a)` | `GameRGBAColor` | 设置所有四个通道 | -| `c.copy(o)` | `GameRGBAColor` | 从另一个 RGBA 颜色复制所有通道 | -| `c.addEq(o)` | `GameRGBAColor` | 原地加法 | -| `c.subEq(o)` | `GameRGBAColor` | 原地减法 | -| `c.mulEq(o)` | `GameRGBAColor` | 原地逐通道乘法 | -| `c.divEq(o)` | `GameRGBAColor` | 原地逐通道除法,除以 0 跳过该通道 | -| `c.scaleEq(n)` | `GameRGBAColor` | 原地标量乘法:每个通道乘以 `n` | +| 方法 | 返回值 | 说明 | +| ------------------- | --------------- | --------------------------------- | +| `c.set(r, g, b, a)` | `GameRGBAColor` | 设置所有四个通道 | +| `c.copy(o)` | `GameRGBAColor` | 从另一个 RGBA 颜色复制所有通道 | +| `c.addEq(o)` | `GameRGBAColor` | 原地加法 | +| `c.subEq(o)` | `GameRGBAColor` | 原地减法 | +| `c.mulEq(o)` | `GameRGBAColor` | 原地逐通道乘法 | +| `c.divEq(o)` | `GameRGBAColor` | 原地逐通道除法,除以 0 跳过该通道 | +| `c.scaleEq(n)` | `GameRGBAColor` | 原地标量乘法:每个通道乘以 `n` | #### 创建新颜色 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `c.clone()` | `GameRGBAColor` | 深拷贝 | -| `c.add(o)` | `GameRGBAColor` | 逐通道加法 | -| `c.sub(o)` | `GameRGBAColor` | 逐通道减法 | -| `c.mul(o)` | `GameRGBAColor` | 逐通道乘法 | -| `c.div(o)` | `GameRGBAColor` | 逐通道除法,除以 0 得 0 | -| `c.lerp(o, t)` | `GameRGBAColor` | 线性插值 | -| `c.scale(n)` | `GameRGBAColor` | 标量乘法:每个通道乘以 `n` | -| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | -| `c.blendEq(rgb)` | `GameRGBColor` | Alpha 混合到 RGB 背景上,返回最终 RGB | +| 方法 | 返回值 | 说明 | +| ---------------- | --------------- | ------------------------------------- | +| `c.clone()` | `GameRGBAColor` | 深拷贝 | +| `c.add(o)` | `GameRGBAColor` | 逐通道加法 | +| `c.sub(o)` | `GameRGBAColor` | 逐通道减法 | +| `c.mul(o)` | `GameRGBAColor` | 逐通道乘法 | +| `c.div(o)` | `GameRGBAColor` | 逐通道除法,除以 0 得 0 | +| `c.lerp(o, t)` | `GameRGBAColor` | 线性插值 | +| `c.scale(n)` | `GameRGBAColor` | 标量乘法:每个通道乘以 `n` | +| `c.equals(o)` | `boolean` | 近似相等,容差 1e-6 | +| `c.blendEq(rgb)` | `GameRGBColor` | Alpha 混合到 RGB 背景上,返回最终 RGB | ### toString @@ -303,12 +297,10 @@ new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5 ```js // Alpha 混合 var fg = new GameRGBAColor(1, 0, 0, 0.5); // 半透明红 -var bg = new GameRGBColor(1, 1, 1); // 白色背景 -var result = fg.blendEq(bg); // 得到混合后的 RGB 颜色 +var bg = new GameRGBColor(1, 1, 1); // 白色背景 +var result = fg.blendEq(bg); // 得到混合后的 RGB 颜色 ``` ---- - ## GameQuaternion 四元数,用于 3D 旋转。单位四元数 (模长=1) 表示纯旋转。 @@ -316,60 +308,60 @@ var result = fg.blendEq(bg); // 得到混合后的 RGB 颜色 ### 构造 ```js -var q = new GameQuaternion(); // 单位四元数 (1, 0, 0, 0) -var q = new GameQuaternion(w, x, y, z); // 指定分量 +var q = new GameQuaternion(); // 单位四元数 (1, 0, 0, 0) +var q = new GameQuaternion(w, x, y, z); // 指定分量 ``` ### 属性 -| 属性 | 类型 | 说明 | -|------|------|------| +| 属性 | 类型 | 说明 | +| ----- | -------- | ----------------------- | | `q.w` | `number` | 实部 (标量分量),可读写 | -| `q.x` | `number` | 虚部 X 分量,可读写 | -| `q.y` | `number` | 虚部 Y 分量,可读写 | -| `q.z` | `number` | 虚部 Z 分量,可读写 | +| `q.x` | `number` | 虚部 X 分量,可读写 | +| `q.y` | `number` | 虚部 Y 分量,可读写 | +| `q.z` | `number` | 虚部 Z 分量,可读写 | ### 实例方法 #### 原地修改 (返回 this) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.set(w, x, y, z)` | `GameQuaternion` | 设置所有分量 | -| `q.copy(p)` | `GameQuaternion` | 从 `p` 复制所有分量 | +| 方法 | 返回值 | 说明 | +| ------------------- | ---------------- | ------------------- | +| `q.set(w, x, y, z)` | `GameQuaternion` | 设置所有分量 | +| `q.copy(p)` | `GameQuaternion` | 从 `p` 复制所有分量 | #### 创建新四元数 (不修改自身) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.clone()` | `GameQuaternion` | 深拷贝 | -| `q.add(p)` | `GameQuaternion` | 逐分量加法 | -| `q.sub(p)` | `GameQuaternion` | 逐分量减法 | -| `q.mul(p)` | `GameQuaternion` | 汉密尔顿积:`q × p` (不可交换) | -| `q.div(p)` | `GameQuaternion` | 除法:`q × p⁻¹` | -| `q.inv()` | `GameQuaternion` | 共轭 (对单位四元数等价于逆) | -| `q.normalize()` | `GameQuaternion` | 单位化,返回模长为 1 的新四元数 | +| 方法 | 返回值 | 说明 | +| --------------- | ---------------- | ---------------------------------------- | +| `q.clone()` | `GameQuaternion` | 深拷贝 | +| `q.add(p)` | `GameQuaternion` | 逐分量加法 | +| `q.sub(p)` | `GameQuaternion` | 逐分量减法 | +| `q.mul(p)` | `GameQuaternion` | 汉密尔顿积:`q × p` (不可交换) | +| `q.div(p)` | `GameQuaternion` | 除法:`q × p⁻¹` | +| `q.inv()` | `GameQuaternion` | 共轭 (对单位四元数等价于逆) | +| `q.normalize()` | `GameQuaternion` | 单位化,返回模长为 1 的新四元数 | | `q.slerp(p, t)` | `GameQuaternion` | 球面线性插值:`t=0` 为自身,`t=1` 为 `p` | #### 数值计算 -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.dot(p)` | `number` | 点积 | -| `q.mag()` | `number` | 模长 (范数) | -| `q.sqrMag()` | `number` | 模长平方 | -| `q.angle(p)` | `number` | 与 `p` 的角度差 (弧度) | -| `q.equals(p)` | `boolean` | 近似相等,容差 1e-6 | +| 方法 | 返回值 | 说明 | +| ------------- | --------- | ---------------------- | +| `q.dot(p)` | `number` | 点积 | +| `q.mag()` | `number` | 模长 (范数) | +| `q.sqrMag()` | `number` | 模长平方 | +| `q.angle(p)` | `number` | 与 `p` 的角度差 (弧度) | +| `q.equals(p)` | `boolean` | 近似相等,容差 1e-6 | #### 旋转操作 (绕自身坐标系旋转,返回新四元数) -| 方法 | 返回值 | 说明 | -|------|--------|------| -| `q.rotateX(rad)` | `GameQuaternion` | 绕 X 轴旋转 | -| `q.rotateY(rad)` | `GameQuaternion` | 绕 Y 轴旋转 | -| `q.rotateZ(rad)` | `GameQuaternion` | 绕 Z 轴旋转 | -| `q.rotateVector(v)` | `GameVector3` | 用此四元数旋转向量 `v` | -| `q.toEuler()` | `GameVector3` | 转为欧拉角 (YZX 顺序),返回 `(x, y, z)` 弧度 | +| 方法 | 返回值 | 说明 | +| ------------------- | ---------------- | -------------------------------------------- | +| `q.rotateX(rad)` | `GameQuaternion` | 绕 X 轴旋转 | +| `q.rotateY(rad)` | `GameQuaternion` | 绕 Y 轴旋转 | +| `q.rotateZ(rad)` | `GameQuaternion` | 绕 Z 轴旋转 | +| `q.rotateVector(v)` | `GameVector3` | 用此四元数旋转向量 `v` | +| `q.toEuler()` | `GameVector3` | 转为欧拉角 (YZX 顺序),返回 `(x, y, z)` 弧度 | #### 轴角分解 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md deleted file mode 100644 index efdff2c..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md +++ /dev/null @@ -1,413 +0,0 @@ -# Math Types - - The following data types are globally available in JS. - -## GameVector3 - -A 3D vector with double-precision components. Used for position, direction, velocity, etc. - -### Constructor - -```js -var v = new GameVector3(); // Zero vector (0, 0, 0) -var v = new GameVector3(x, y, z); // Specified coordinates -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `v.x` | `number` | X component (east/west), read/write | -| `v.y` | `number` | Y component (up/down), read/write | -| `v.z` | `number` | Z component (north/south), read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.set(x, y, z)` | `GameVector3` | Set all components | -| `v.copy(w)` | `GameVector3` | Copy all components from `w` | -| `v.addEq(w)` | `GameVector3` | In-place addition: `v += w` | -| `v.subEq(w)` | `GameVector3` | In-place subtraction: `v -= w` | -| `v.mulEq(w)` | `GameVector3` | In-place component-wise multiplication | -| `v.divEq(w)` | `GameVector3` | In-place component-wise division; divide-by-zero skips that component | -| `v.scaleEq(n)` | `GameVector3` | In-place scalar multiplication: `v.x *= n` … | -| `v.negEq()` | `GameVector3` | In-place negation: `v = -v` | - -#### Creating New Vectors (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.clone()` | `GameVector3` | Deep copy — independent vector with same values | -| `v.add(w)` | `GameVector3` | Vector addition: `v + w` | -| `v.sub(w)` | `GameVector3` | Vector subtraction: `v - w` | -| `v.mul(w)` | `GameVector3` | Component-wise multiplication | -| `v.div(w)` | `GameVector3` | Component-wise division; divide-by-zero → 0 | -| `v.scale(n)` | `GameVector3` | Scalar multiplication: each component × `n` | -| `v.cross(w)` | `GameVector3` | Cross product: `v × w` | -| `v.normalize()` | `GameVector3` | Unit vector; zero vector returns `(0,0,0)` | -| `v.lerp(w, t)` | `GameVector3` | Linear interpolation: `t=0` → this, `t=1` → `w` | -| `v.towards(w)` | `GameVector3` | Direction vector pointing toward `w` (normalized) | -| `v.max(w)` | `GameVector3` | Component-wise maximum | -| `v.min(w)` | `GameVector3` | Component-wise minimum | -| `v.neg()` | `GameVector3` | Negation: `-v` | -| `v.moveTowards(target, maxDelta)` | `GameVector3` | Move toward target by at most `maxDelta` distance | -| `v.floor()` | `GameVector3` | Component-wise floor | -| `v.ceil()` | `GameVector3` | Component-wise ceiling | -| `v.clampLength(max)` | `GameVector3` | Clamp magnitude to `max`, scale down proportionally if exceeded | - -#### Numeric Computations - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.dot(w)` | `number` | Dot (inner) product: `v · w` | -| `v.mag()` | `number` | Magnitude (length) | -| `v.sqrMag()` | `number` | Squared magnitude — faster than `mag()` | -| `v.distance(w)` | `number` | Euclidean distance to `w` | -| `v.angle(w)` | `number` | Angle between `v` and `w` (radians, 0–π) | -| `v.sqrDistance(w)` | `number` | Squared distance to `w` — faster than `distance()` | - -#### Comparison - -| Method | Returns | Description | -|--------|---------|-------------| -| `v.equals(w)` | `boolean` | Approximate equality, tolerance 1e-6 | -| `v.exactEquals(w)` | `boolean` | Exact equality — components strictly equal | -| `v.isZero()` | `boolean` | Whether this is (approximately) a zero vector, tolerance 1e-6 | - -```js -var pos = new GameVector3(0, 100, 0); -var target = new GameVector3(10, 100, 10); - -// Distance -var dist = pos.distance(target); // ~14.14 - -// Direction vector -var dir = target.sub(pos).normalize(); - -// Angle -var angle = pos.angle(target); // radians - -// Comparison -var a = new GameVector3(1, 2, 3); -var b = new GameVector3(1.0000001, 2.0000001, 3.0000001); -a.equals(b); // true (within tolerance) -a.exactEquals(b); // false - -// Teleport entity (LiveVec3) -entity.position.set(0, 100, 0); -``` - -### Static Methods - -```js -// Spherical coordinates → vector -var v = GameVector3.fromPolar(mag, phi, theta); -// mag: radius -// phi: azimuth angle (radians, horizontal rotation around Y) -// theta: elevation angle (radians, from horizontal plane) -``` - -### toString - -```js -var v = new GameVector3(1, 2, 3); -v.toString(); // "GameVector3(1.0, 2.0, 3.0)" -``` - ---- - -## GameBounds3 - -Axis-aligned bounding box (AABB), defined by two opposing corners: `lo` (minimum corner) and `hi` (maximum corner). - -### Constructor - -```js -var bounds = new GameBounds3( - new GameVector3(-1, 0, -1), // lo (min corner) - new GameVector3(1, 2, 1), // hi (max corner) -); -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `bounds.lo` | `GameVector3` | Minimum corner, read/write | -| `bounds.hi` | `GameVector3` | Maximum corner, read/write | - -### Instance Methods - -| Method | Returns | Description | -|--------|---------|-------------| -| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | Set all boundaries in-place, returns this | -| `bounds.copy(b)` | `GameBounds3` | Copy values from `b` in-place, returns this | -| `bounds.intersects(other)` | `boolean` | Whether this intersects `other` | -| `bounds.intersect(other)` | `GameBounds3 \| null` | Intersection bounds, or `null` if no overlap | -| `bounds.contains(v)` | `boolean` | Whether point `v` is inside (inclusive) | -| `bounds.containsBounds(b)` | `boolean` | Whether this fully contains `b` | -| `bounds.center()` | `GameVector3` | Center point of the bounds | -| `bounds.size()` | `GameVector3` | Size of the bounds (width, height, depth) | -| `bounds.expand(delta)` | `GameBounds3` | Expand all faces outward by `delta`, returns new bounds | -| `bounds.expandEq(delta)` | `GameBounds3` | In-place expand all faces outward by `delta`, returns this | -| `bounds.growToInclude(v)` | `GameBounds3` | In-place grow to include point `v`, returns this | -| `bounds.closestPoint(v)` | `GameVector3` | Closest point on the bounds to point `v` | -| `bounds.move(offset)` | `GameBounds3` | Translate by `offset`, returns new bounds | -| `bounds.moveEq(offset)` | `GameBounds3` | In-place translate by `offset`, returns this | - -### Static Methods - -```js -// Create minimal bounds from an array of GameVector3 -var points = [new GameVector3(0,0,0), new GameVector3(5,10,3)]; -var box = GameBounds3.fromPoints(points); // returns GameBounds3 or null -``` - -### toString - -```js -bounds.toString(); // "GameBounds3(GameVector3(-1.0, 0.0, -1.0), GameVector3(1.0, 2.0, 1.0))" -``` - -```js -// Query entities within bounds -var entities = world.searchBox(bounds); - -// Check if point is inside -if (bounds.contains(player.position)) { - // Player is inside the area -} -``` - ---- - -## GameRGBColor - -An RGB color with three channels ranging from 0.0 to 1.0. - -### Constructor - -```js -var red = new GameRGBColor(1, 0, 0); -var blue = new GameRGBColor(0, 0, 1); -var gray = new GameRGBColor(0.5, 0.5, 0.5); -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `color.r` | `number` | Red channel (0–1), read/write | -| `color.g` | `number` | Green channel (0–1), read/write | -| `color.b` | `number` | Blue channel (0–1), read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.set(r, g, b)` | `GameRGBColor` | Set all channels | -| `c.copy(o)` | `GameRGBColor` | Copy all channels from another color | -| `c.addEq(o)` | `GameRGBColor` | In-place addition: `c += o` | -| `c.subEq(o)` | `GameRGBColor` | In-place subtraction: `c -= o` | -| `c.mulEq(o)` | `GameRGBColor` | In-place channel-wise multiplication | -| `c.divEq(o)` | `GameRGBColor` | In-place channel-wise division; divide-by-zero skips | -| `c.scaleEq(n)` | `GameRGBColor` | In-place scalar multiplication: each channel × `n` | - -#### Creating New Colors (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.clone()` | `GameRGBColor` | Deep copy | -| `c.add(o)` | `GameRGBColor` | Channel-wise addition | -| `c.sub(o)` | `GameRGBColor` | Channel-wise subtraction | -| `c.mul(o)` | `GameRGBColor` | Channel-wise multiplication | -| `c.div(o)` | `GameRGBColor` | Channel-wise division; divide-by-zero → 0 | -| `c.lerp(o, t)` | `GameRGBColor` | Linear interpolation: `t=0` → this, `t=1` → `o` | -| `c.scale(n)` | `GameRGBColor` | Scalar multiplication: each channel × `n` | -| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | -| `c.toRGBA()` | `string` | CSS format string: `"rgba(r,g,b,1.0)"` | - -### Static Methods - -```js -var randomColor = GameRGBColor.random(); // Each channel 0–1 random -``` - -### toString - -```js -new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" -``` - ---- - -## GameRGBAColor - -An RGBA color with four channels ranging from 0.0 to 1.0. - -### Constructor - -```js -var semiRed = new GameRGBAColor(1, 0, 0, 0.5); -var opaque = new GameRGBAColor(0, 1, 0, 1.0); -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `color.r` | `number` | Red channel (0–1), read/write | -| `color.g` | `number` | Green channel (0–1), read/write | -| `color.b` | `number` | Blue channel (0–1), read/write | -| `color.a` | `number` | Alpha opacity (0–1), read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.set(r, g, b, a)` | `GameRGBAColor` | Set all four channels | -| `c.copy(o)` | `GameRGBAColor` | Copy all channels from another RGBA color | -| `c.addEq(o)` | `GameRGBAColor` | In-place addition | -| `c.subEq(o)` | `GameRGBAColor` | In-place subtraction | -| `c.mulEq(o)` | `GameRGBAColor` | In-place channel-wise multiplication | -| `c.divEq(o)` | `GameRGBAColor` | In-place channel-wise division; divide-by-zero skips | -| `c.scaleEq(n)` | `GameRGBAColor` | In-place scalar multiplication: each channel × `n` | - -#### Creating New Colors (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `c.clone()` | `GameRGBAColor` | Deep copy | -| `c.add(o)` | `GameRGBAColor` | Channel-wise addition | -| `c.sub(o)` | `GameRGBAColor` | Channel-wise subtraction | -| `c.mul(o)` | `GameRGBAColor` | Channel-wise multiplication | -| `c.div(o)` | `GameRGBAColor` | Channel-wise division; divide-by-zero → 0 | -| `c.lerp(o, t)` | `GameRGBAColor` | Linear interpolation | -| `c.scale(n)` | `GameRGBAColor` | Scalar multiplication: each channel × `n` | -| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | -| `c.blendEq(rgb)` | `GameRGBColor` | Alpha-blend onto an RGB background, returns displayed RGB | - -### toString - -```js -new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5)" -``` - -```js -// Alpha blending -var fg = new GameRGBAColor(1, 0, 0, 0.5); // Semi-transparent red -var bg = new GameRGBColor(1, 1, 1); // White background -var result = fg.blendEq(bg); // Blended RGB color -``` - ---- - -## GameQuaternion - -A quaternion used for 3D rotation. Unit quaternions (magnitude=1) represent pure rotations. - -### Constructor - -```js -var q = new GameQuaternion(); // Identity (1, 0, 0, 0) -var q = new GameQuaternion(w, x, y, z); // Specified components -``` - -### Properties - -| Property | Type | Description | -|----------|------|-------------| -| `q.w` | `number` | Real (scalar) component, read/write | -| `q.x` | `number` | Imaginary X component, read/write | -| `q.y` | `number` | Imaginary Y component, read/write | -| `q.z` | `number` | Imaginary Z component, read/write | - -### Instance Methods - -#### Mutating (return this) - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.set(w, x, y, z)` | `GameQuaternion` | Set all components | -| `q.copy(p)` | `GameQuaternion` | Copy all components from `p` | - -#### Creating New Quaternions (does not mutate) - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.clone()` | `GameQuaternion` | Deep copy | -| `q.add(p)` | `GameQuaternion` | Component-wise addition | -| `q.sub(p)` | `GameQuaternion` | Component-wise subtraction | -| `q.mul(p)` | `GameQuaternion` | Hamilton product: `q × p` (NOT commutative) | -| `q.div(p)` | `GameQuaternion` | Division: `q × p⁻¹` | -| `q.inv()` | `GameQuaternion` | Conjugate (equals inverse for unit quaternions) | -| `q.normalize()` | `GameQuaternion` | Normalize, returns unit quaternion | - -#### Interpolation - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.slerp(p, t)` | `GameQuaternion` | Spherical linear interpolation: `t=0` → this, `t=1` → `p` | - -#### Numeric Computations - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.dot(p)` | `number` | Dot product | -| `q.mag()` | `number` | Magnitude (norm) | -| `q.sqrMag()` | `number` | Squared magnitude | -| `q.angle(p)` | `number` | Angular difference from `p` (radians) | -| `q.equals(p)` | `boolean` | Approximate equality, tolerance 1e-6 | - -#### Rotation Operations (rotate around local axes, returns new quaternion) - -| Method | Returns | Description | -|--------|---------|-------------| -| `q.rotateX(rad)` | `GameQuaternion` | Rotate around X axis | -| `q.rotateY(rad)` | `GameQuaternion` | Rotate around Y axis | -| `q.rotateZ(rad)` | `GameQuaternion` | Rotate around Z axis | -| `q.rotateVector(v)` | `GameVector3` | Rotate vector `v` by this quaternion | -| `q.toEuler()` | `GameVector3` | Convert to Euler angles (YZX order), returns `(x, y, z)` in radians | - -#### Axis-Angle Decomposition - -```js -var result = q.getAxisAngle(); -// result.angle — rotation angle (radians) -// result.axis — rotation axis (unit GameVector3) -``` - -### Static Methods - -```js -// Create from axis-angle representation -var q1 = GameQuaternion.fromAxisAngle(axis, rad); -// axis: GameVector3 (auto-normalized) -// rad: rotation angle (radians) - -// Create from Euler angles (YZX rotation order: Y → Z → X) -var q2 = GameQuaternion.fromEuler(x, y, z); -// x, y, z: rotation around each axis in radians - -// Shortest-arc quaternion rotating from vector a to b -var q3 = GameQuaternion.rotationBetween(fromVec, toVec); - -// Create quaternion from look-at direction (from → to) -var q4 = GameQuaternion.lookAt(from, to, up); -// from: GameVector3 — observer position -// to: GameVector3 — target point -// up: GameVector3 — up direction (default (0,1,0)) -``` - -### toString - -```js -q.toString(); // "GameQuaternion(0.707, 0.0, 0.707, 0.0)" -``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player.md b/Box3JS-NeoForge-1.21.1/docs/api/player.md index 649f38e..d9bc281 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -1,6 +1,6 @@ # player — 玩家 API -`player` 对象通过 `entity.player` 获取,代表登录的玩家。它拥有 `entity` 的全部属性和方法(如 `hp`、`position`、`tags()` 等),并额外提供玩家专属功能:背包、经验、飞行、消息、传送等。 +`player` 通过 `entity.player` 获取,拥有 `entity` 的全部属性并额外提供背包、经验、飞行、消息、传送等玩家专属功能。 ```js world.onPlayerJoin(function(entity, tick) { @@ -57,6 +57,22 @@ console.log("玩家缩放: " + player.scale); ## 移动 +### player.position + +只读引用。玩家当前世界坐标,可通过 `player.position.set(x, y, z)` 修改向量值;如需传送玩家,优先使用 `player.teleport(pos)`。 + +### player.velocity + +只读引用。玩家当前速度向量,可通过 `.set()` 修改。 + +### player.bounds + +只读。玩家包围盒半尺寸。 + +### player.onGround + +只读。玩家当前是否站在方块上。 + ### player.walkSpeed diff --git a/Box3JS-NeoForge-1.21.1/docs/api/registries.md b/Box3JS-NeoForge-1.21.1/docs/api/registries.md index 2fc848d..899dc2f 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/registries.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/registries.md @@ -1,16 +1,14 @@ # 自定义注册(registries API) -> **仅服务端可用。** 客户端中 `registries` 为 `undefined`。客户端代码请直接使用 ResourceLocation 字符串(如 `audio.playSound("colorzone:victory_fanfare", 1.0, 1.0)`)。 -> -> **仅在编译 JAR 模式(`/box3script compile`)下可用。** 解释模式(`/box3script start`)中 `registries` 为 `undefined`。 -> -> **需要服务端和客户端都安装编译后的 JAR** 才能正确渲染方块纹理/模型。客户端没有 JAR 的话,方块会显示为紫黑缺失方块。 +::: warning +仅编译 JAR 模式(`/box3script compile`)**服务端**可用。客户端及解释模式中 `registries` 为 `undefined`。JAR 需双端安装才能渲染。 +::: 方块、物品和音效事件在 JSON 配置文件中声明,编译时生成 `DeferredRegister` 代码注入 `@Mod` 类。资源文件从项目 `assets/` 目录打包进 JAR。 ## 项目布局 -``` +```text mygame/ ├── registries/ │ ├── blocks.json ← 方块定义 @@ -137,7 +135,9 @@ mygame/ } ``` -> **注意:** `creativeTab` 图标会自动从物品中查找(优先物品,其次方块)。如果 `creativeTabs.json` 的 `icon` 匹配某个物品 key,会使用该物品作为图标。 +::: tip +`creativeTab` 图标会自动从物品中查找(优先物品,其次方块)。如果 `creativeTabs.json` 的 `icon` 匹配某个物品 key,会使用该物品作为图标。 +::: ### 装备类型(工具 & 盔甲) @@ -198,7 +198,9 @@ mygame/ } ``` -> **注意:** 装备的 `maxStackSize` 固定为 1(不可堆叠),无需手动设置。`nutrition`/`saturation`/`alwaysEdible` 仅用于 `"food"` 类型。 +::: tip +装备的 `maxStackSize` 固定为 1(不可堆叠),无需手动设置。`nutrition`/`saturation`/`alwaysEdible` 仅用于 `"food"` 类型。 +::: ## sounds.json @@ -234,7 +236,7 @@ mygame/ 对应文件: -``` +```js assets/sounds/victory_fanfare.ogg assets/sounds/skill_cast.ogg assets/sounds/background_music.ogg @@ -266,7 +268,7 @@ audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); 与 Minecraft 资源包结构一致: -``` +```text assets// ├── blockstates/.json ← 自动生成,可自定义覆盖 ├── models/block/.json ← 自动生成,可自定义覆盖 @@ -281,7 +283,9 @@ assets// └── item/.png ``` -> **注意:** `` 来自 `package.json` 的 `name` 字段(从第二个 `/` 后取,如 `@scope/mygame` → `mygame`)。 +::: tip +`` 来自 `package.json` 的 `name` 字段(从第二个 `/` 后取,如 `@scope/mygame` → `mygame`)。 +::: 编译时自动将 `assets/` 打包为 `assets//`。 @@ -289,7 +293,7 @@ assets// 语言文件需要在 `assets/lang/` 目录下**手动创建**,不会被自动生成。至少应提供 `en_us.json` 和 `zh_cn.json`,也可添加更多语言: -``` +```text mygame/ └── assets/ └── lang/ diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server.md b/Box3JS-NeoForge-1.21.1/docs/api/server.md index 658dc09..2380190 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/server.md @@ -2,7 +2,9 @@ 服务端脚本运行在 Minecraft 服务器线程上,入口文件是 `src/server/app.ts`,构建产物是 `dist/server.js`。服务端 API 负责世界状态、实体和玩家、方块读写、事件回调、持久化数据、网络请求以及服务端到客户端的事件下发。 -> 客户端 UI、键盘输入、本地音效和本地 GUI 不在服务端 API 中。相关能力见 [client.md](client.md)。 +::: info +客户端 UI、键盘输入、本地音效和本地 GUI 不在服务端 API 中。相关能力见 [client.md](client.md)。 +::: ## 服务端全局对象 @@ -60,7 +62,7 @@ remoteChannel.sendClientEvent(entity, { | 实体交互 | `world.onInteract(handler)` | | 方块交互 | `world.onBlockActivate(handler)` | | 方块破坏/放置 | `world.onVoxelDestroy(handler)` / `world.onBlockPlace(handler)` | -| 定时器 | `world.setTimeout(fn, ticks)` / `world.setInterval(fn, ticks)` | +| 定时器 | `setTimeout(fn, ticks)` / `setInterval(fn, ticks)` | ```ts world.onChat((entity, message) => { @@ -157,14 +159,10 @@ globalConfig.set("season", "spring"); ## 跨端通信 -服务端使用 `remoteChannel` 与客户端脚本通信。发送前建议检查玩家是否安装了 Box3JS 客户端: +服务端使用 `remoteChannel` 与客户端脚本通信。数据包为可选的(optional),未安装 Box3JS 的客户端会自动忽略,无需手动检测: ```ts world.onPlayerJoin((entity) => { - if (!entity.hasBox3JSClient()) { - return; - } - remoteChannel.sendClientEvent(entity, { type: "welcome", text: "欢迎来到服务器", @@ -172,6 +170,15 @@ world.onPlayerJoin((entity) => { }); ``` +向所有玩家广播客户端事件: + +```ts +remoteChannel.broadcastClientEvent({ + type: "serverNotice", + text: "服务器事件已触发", +}); +``` + 接收客户端事件: ```ts @@ -227,4 +234,3 @@ if (registries) { - 对所有长期事件监听保存 token,需要关闭玩法或重载模块时调用 `cancel()`。 - 大范围 `voxels.fillVoxel()`、大量实体生成、同步 HTTP 请求都应控制频率,避免卡住服务器 tick。 - 共享数据优先明确命名空间,例如 `storage.getDataStorage("arena/scores")` 或 `storage.getGroupStorage("global/season")`。 - diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage.md b/Box3JS-NeoForge-1.21.1/docs/api/storage.md index a69057a..10df6bf 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/storage.md @@ -2,10 +2,16 @@ `storage` 提供 JSON 文件持久化存储,带内存缓存加速读写。 -> **运行环境:** 服务端和客户端都可用。服务端数据保存在 `config/box3/storage/<项目名>/`;客户端数据保存在本地游戏目录的 `box3/client-storage/<项目名>/`。每个项目自动拥有独立命名空间。 +::: info 运行环境 +服务端和客户端都可用。服务端数据保存在 `config/box3/storage/<项目名>/`;客户端数据保存在本地游戏目录的 `box3/client-storage/<项目名>/`。每个项目自动拥有独立命名空间。 +::: ## 获取存储实例 +### storage.key + +只读。根 `storage` 对象始终返回空字符串;具体命名空间请读取 `store.key`。 + ### storage.getDataStorage(name) 获取或创建一个命名存储。同名存储返回同一实例。 @@ -14,7 +20,9 @@ 获取**跨项目共享**存储。所有项目通过同一 `name` 访问同一份数据(底层使用 `__shared__/` 命名空间)。适合做全服排行榜、全局配置等。 -> 仅服务端可用。客户端本地存储只提供 `getDataStorage(name)`。 +::: warning +仅服务端可用。客户端本地存储只提供 `getDataStorage(name)`。 +::: ```js var store = storage.getDataStorage("leaderboard"); @@ -41,7 +49,9 @@ var winner = store.get("lastWinner"); // "Steve" (string) var cfg = store.get("config"); // {difficulty: "hard", ...} (object) ``` -> **注意:** 当数据从磁盘重新加载后,复杂对象会以普通 JSON 对象形式返回(例如 `Map` 风格对象),请避免依赖原始 JS 原型方法。 +::: warning 注意 +当数据从磁盘重新加载后,复杂对象会以普通 JSON 对象形式返回(例如 `Map` 风格对象),请避免依赖原始 JS 原型方法。 +::: ### store.keys() diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index 8eee60e..a668a2d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -1,6 +1,6 @@ # world — 世界 API -`world` 是全局单例,代表 Minecraft 服务端的世界状态。控制天气、时间、游戏规则、实体生成,注册事件回调,管理记分板/Bossbar/队伍,以及发射粒子、烟花、闪电等视觉效果。 +`world` 是全局单例,代表 Minecraft 服务端的世界状态。 ## 世界属性 @@ -367,34 +367,28 @@ world.say("§6[公告] §f比赛即将开始!"); ## 计时器 -### world.setTimeout(handler, ticks) +`setTimeout` 和 `setInterval` 是全局函数(不在 `world` 上)。Rhino 引擎不提供浏览器内置的定时器,由 Box3JS 提供。返回 `GameEventHandlerToken`,调用 `.cancel()` 取消。 -⬆ MC 扩展 | 延迟 `ticks` 后执行一次,返回 timer ID。 +### setTimeout(handler, ticks) -### world.setInterval(handler, ticks) +延迟 `ticks` 后执行一次。 -⬆ MC 扩展 | 每 `ticks` 重复执行,返回 timer ID。 +### setInterval(handler, ticks) -### world.clearTimeout(id) - -⬆ MC 扩展 | 取消 timeout。 - -### world.clearInterval(id) - -⬆ MC 扩展 | 取消 interval。 +每 `ticks` 重复执行。 ```js -var tid = world.setTimeout(() => { +var token = setTimeout(() => { world.say("3 秒后执行"); }, 60); // 60 ticks = 3 秒 -var iid = world.setInterval(() => { +var interval = setInterval(() => { world.say("每 10 秒执行一次"); }, 200); // 200 ticks = 10 秒 // 取消 -world.clearTimeout(tid); -world.clearInterval(iid); +token.cancel(); +interval.cancel(); ``` ## 记分板 @@ -467,12 +461,12 @@ world.removeScoreboard("kills"); ```js // 创建一个 3 分钟倒计时血条 var totalTicks = 3600; -var iid = world.setInterval(() => { +var iid = setInterval(() => { totalTicks -= 20; var remain = totalTicks / 3600; if (remain <= 0) { world.removeBossbar("timer"); - world.clearInterval(iid); + iid.cancel(); } else { world.showBossbar( "timer", @@ -550,7 +544,7 @@ world.borderSize = 500; world.setBorderDamage(2); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.shrinkBorder(100, 120); // 2 分钟缩到 100 }, 600); // 30 秒后开始 ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/en/README.md b/Box3JS-NeoForge-1.21.1/docs/en/README.md new file mode 100644 index 0000000..499ecfa --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/README.md @@ -0,0 +1,77 @@ +--- +layout: home + +hero: + name: "Box3JS" + text: "Minecraft JS/TS Scripting Engine" + tagline: Same programming experience as Box3 — build mini-games in Minecraft with JS/TS + actions: + - theme: brand + text: Get Started + link: /en/guide/getting-started + +features: + - icon: 🎮 + title: Server & Client Dual-Side Scripting + details: "Server: world manipulation, entities, recipes. Client: keyboard input, screen UI, audio, SQLite storage, HTTP requests." + - icon: 📦 + title: TypeScript-First + details: DTS type definitions for all 17 global objects. Built-in esbuild + Babel pipeline transpiles modern TS to Rhino-compatible ES5. + - icon: 🔄 + title: Hot Reload + details: Script changes take effect instantly — no server restart needed. File watcher auto-reloads on save. + - icon: 🌐 + title: Bidirectional Communication + details: remoteChannel for server↔client event messaging. Server broadcasts to all players, clients reply independently. + - icon: 🗄️ + title: Dual-Side Storage & Database + details: JSON file persistence and SQLite on both server and client. Paginated queries, atomic updates, counters, tagged templates. + - icon: 🧩 + title: Custom Blocks & Items + details: Block textures, item models, equipment, sounds, creative tabs — all configured via JSON registries (standalone/JAR mode). + - icon: 📚 + title: Comprehensive Docs + details: 50+ pages covering API reference, progressive tutorials, recipes, architecture deep-dive, and FAQ — bilingual CN/EN. + - icon: 🚀 + title: Standalone JAR Mode + details: Compile script projects into independent JAR mods with zero runtime dependencies — drop into mods folder and go. + +--- + +## Quick Start + +```bash +# In-game: create a new project +/box3script create mygame + +# Build & watch +cd config/box3/script/mygame +npm install +npm run build -- --watch + +# TypeScript type check +npm run check +``` + +```ts +// src/server/app.ts — your first script +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage(`Hello, ${entity.player.name}!`); + return false; + } + return true; +}); +``` + +[View Full Docs →](/en/guide/getting-started) + +## Version Info + +| Component | Version | +|-----------|---------| +| Minecraft | 1.21.1 | +| Mod Loader | NeoForge | +| Java | 21 | +| JS Engine | Mozilla Rhino 1.9.1 (ES5) | +| TypeScript | Via Babel → ES5 | diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/README.md b/Box3JS-NeoForge-1.21.1/docs/en/api/README.md new file mode 100644 index 0000000..cbcb548 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/README.md @@ -0,0 +1,413 @@ +--- +--- + +# Box3JS API Reference + +Write Minecraft server and client scripts in JavaScript/TypeScript. + +## 5-Minute Quick Start + +```bash +# 1. Create a project in-game +/box3script create mygame + +# 2. Install dependencies and build +cd config/box3/script/mygame +npm install && npm run build + +# 3. Start the script +/box3script start mygame +``` + +Open `src/server/app.ts` and write: + +```js +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage("Hello, " + entity.player.name + "!"); + return false; // suppress chat message + } + return true; +}); + +console.log("Script loaded"); +``` + +After each edit, re-run `npm run build`, then use `/box3script reload mygame` to hot-reload. Client logic goes in `src/client/app.ts`; after build it becomes `dist/client.js` and is sent automatically to players who have the Box3JS client mod installed. + +::: tip Quick Navigation +[Quick Start Guide](../guide/getting-started.md) | [Architecture](../guide/architecture.md) | [JS vs Java Comparison](../guide/js-vs-java.md) +::: + +## API Domain Map + +Box3JS APIs are split into server-side, client-side, and shared runtimes. The type declarations are separated: `tsconfig.server.json` does not include client globals, and `tsconfig.client.json` does not include server globals such as `world` / `voxels`. + +| Domain | Runtime | Globals | Description | +| ------------------------------- | ------------ | --------------------------------------------------------------------------- | ------------------------------------------------ | +| **World & Entities** (server) | Server | `world` `voxels` | World control, blocks, event callbacks | +| **Players & Data** (server) | Server | `entity` `player` `storage` `db` `http` | `entity`/`player` come from callbacks or queries | +| **Client Interaction** (client) | Client | `audio` `client` `input` `ui` `chat` `gui` | Requires Box3JS client mod | +| **Cross-Side** | Both | `remoteChannel` | Server↔Client event communication | +| **Registries** | Compile-time | `registries` | Only in `/box3script compile` JAR mode | +| **Math & Utilities** | Both | `GameVector3` `GameBounds3` `GameRGBColor` `GameRGBAColor` `GameQuaternion` | Constructed with `new` | +| **Global Tools** | Both | `console` | Log output | + +::: info API Classification +**Server APIs** manipulate the world, entities, players, and blocks. Scripts run on the server by default. **Client APIs** are only available with the Box3JS client mod installed, for UI, input, and audio. **Registry APIs** are only available in compiled JAR mode (`registries` is `undefined` in interpreted mode). +::: + +## Read By Runtime + +| Entry | Use it for | Includes | +| ----------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| [Server API Overview](server.md) | Gameplay logic, events, blocks, entities, players, data, server-to-client events | `world`, `entity`, `player`, `voxels`, `storage`, `db`, `http`, `registries` | +| [Client API Overview](client.md) | Local UI, input, audio, chat helpers, local data, client-to-server events | `client`, `audio`, `input`, `ui`, `chat`, `gui`, `storage`, `db`, `http` | +| [Shared Utilities](math.md) | Math, color, and spatial code usable on both sides | `GameVector3`, `GameBounds3`, `GameRGBColor`, `GameRGBAColor`, `GameQuaternion` | + +## API Style Rules + +- Every `onXxx(...)` event registration API returns a `GameEventHandlerToken`; call `token.cancel()` to unsubscribe and `token.active()` to check whether it is still live. +- Server APIs are only typed in `src/server/app.ts`; client APIs are only typed in `src/client/app.ts`. Shared APIs are `storage`, `db`, `http`, `remoteChannel`, `console`, and the math classes. +- Cross-side data travels through `remoteChannel` as JSON-serializable objects: clients use `sendServerEvent` / `onClientEvent`; servers use `sendClientEvent` / `broadcastClientEvent` / `onServerEvent`. +- Coordinate APIs that accept `GameVector3` usually also support an `x, y, z` overload; server block coordinates are handled as integers. + +## Find by Task — I want to... + +Find APIs by what you want to do, not by which global object they live on. + +### Messages & Chat + +| I want to... | Use this | +| -------------------- | ---------------------------------------- | +| Broadcast to server | `world.say("message")` | +| Send private message | `player.directMessage("message")` | +| Show action bar text | `player.actionBar("message")` | +| Show screen title | `player.title("Title", "Subtitle")` | +| Handle chat input | `world.onChat((entity, msg) => { ... })` | + +### Player Properties + +| I want to... | Use this | +| ----------------------- | ----------------------------------------------- | +| Get/set player position | `player.position` → `GameVector3` | +| Teleport player | `player.teleport(new GameVector3(x, y, z))` | +| Change health | `player.hp = 20` / `player.maxHp = 40` | +| Change hunger | `player.food = 20` / `player.saturation = 10` | +| Switch game mode | `player.gameMode = "creative"` | +| Toggle flight | `player.canFly = true` / `player.flying = true` | +| Kick player | `player.kick("reason")` | +| Run command as player | `player.runCommand("say hi")` | + +### Items & Equipment + +| I want to... | Use this | +| -------------------- | -------------------------------------------- | +| Give a basic item | `player.giveItem("minecraft:diamond", 1)` | +| Give enchanted item | `player.giveEnchantedItem(...)` | +| Give named item | `player.giveNamedItem(...)` | +| Get held item | `player.getHeldItem()` | +| Clear inventory | `player.clearInventory()` | +| Set entity equipment | `entity.setEquipment("head", "iron_helmet")` | + +### Custom Registries (Blocks, Items & Sounds) 🆕 + +| I want to... | Use this | +| ---------------------------- | ------------------------------------------------ | +| Register custom blocks | `registries/blocks.json` (at compile time) | +| Register custom items | `registries/items.json` (at compile time) | +| Register custom sounds | `registries/sounds.json` (at compile time) | +| Register creative tabs | `registries/creativeTabs.json` (at compile time) | +| Get a registered block | `registries.getBlock("my_block")` | +| Get a registered item | `registries.getItem("chocolate")` | +| Get a registered sound | `registries.getSound("victory_fanfare")` | +| Give a custom block/item | `player.giveItem(block.itemId, 1)` | +| Place a custom block | `voxels.setVoxel(x, y, z, block.block)` | +| Play a custom sound (server) | `world.playSound(sound.soundId, x, y, z, 1, 1)` | +| Play a custom sound (client) | `audio.playSound("modId:soundId", 1.0, 1.0)` | + +::: warning +Server-side only. `registries` is `undefined` in client scripts. Only available in `/box3script compile` JAR mode. Client must also install the JAR for textures/models. See [registries.md](registries.md) +::: + +### Block Operations + +| I want to... | Use this | +| -------------------------- | ----------------------------------------------------------- | +| Read a block | `voxels.getVoxel(x, y, z)` | +| Place/replace a block | `voxels.setVoxel(x, y, z, "minecraft:stone")` | +| Fill a region | `voxels.fillVoxel(x1,y1,z1, x2,y2,z2, "stone")` | +| Listen for block breaks | `world.onVoxelDestroy((entity, x, y, z, voxel) => { ... })` | +| Listen for block placement | `world.onBlockPlace((entity, x, y, z, voxel) => { ... })` | + +### Entity Manipulation + +| I want to... | Use this | +| --------------------- | --------------------------------------------- | +| Spawn an entity | `world.spawnEntity("minecraft:zombie", pos)` | +| Create with config | `world.createEntity({ type, position, ... })` | +| Set entity name | `entity.setNameTag("§cBoss")` | +| Toggle AI | `entity.setAI(true)` | +| Navigate to position | `entity.navigateTo(x, y, z, speed)` | +| Set attack target | `entity.setTarget(otherEntity)` | +| Check if player | `entity.isPlayer()` | +| Get entity type | `entity.entityType` | +| Get entity tags | `entity.tags()` / `entity.hasTag("boss")` | +| Query nearby entities | `world.entitiesInRadius(pos, radius)` | +| Query all entities | `world.querySelectorAll("*")` | + +### Client-side Features (requires Box3JS client mod) + +| I want to... | Use this | +| ------------------------- | ----------------------------------------------------------------- | +| Run every client tick | `client.onTick(() => { ... })` | +| Check key held down | `input.isKeyDown("space")` | +| Listen for key press | `input.onKeyPress("f", () => { ... })` | +| Play sound effect | `audio.playSound("pling", 1.0, 1.0)` | +| Play music | `audio.playMusic("minecraft:music.game", 0.5, 1.0)` | +| Stop all sounds | `audio.stopAll()` | +| Get/set volume | `audio.getVolume("music")` / `audio.setVolume("player", 0.8)` | +| Show action bar text | `ui.showOverlay("text")` | +| Show screen title | `ui.showTitle("Title", "Subtitle")` | +| Send chat message | `chat.sendMessage("message")` | +| Receive chat messages | `chat.onMessage((msg, sender, isSystem) => { ... })` | +| Send event to server | `remoteChannel.sendServerEvent({ ... })` | +| Receive event from server | `remoteChannel.onClientEvent((event) => { ... })` | +| Client-side local storage | `storage.getDataStorage("key")` | +| Set fog colour | `client.setFogColor(255, 100, 50)` | +| Set fog distance | `client.setFogStartDistance(10)` / `client.setFogEndDistance(50)` | +| Reset fog | `client.resetFog()` | + +### Visual Effects + +| I want to... | Use this | +| ----------------------- | --------------------------------------------------------- | +| Spawn particles | `world.spawnParticle("flame", x, y, z, ...)` | +| Particle circle | `world.spawnParticleCircle(x, y, z, radius, "heart", 20)` | +| Firework | `world.launchFirework(x, y, z, "red", "large_ball")` | +| Lightning | `world.strikeLightning(x, y, z)` | +| Explosion | `world.explode(x, y, z, power)` | +| Play sound (global) | `world.playSound("pling", pos, 1.0, 1.0)` | +| Play sound (per-player) | `player.playSound("pling", 1.0, 1.0)` | + +### Potion Effects + +| I want to... | Use this | +| ----------------- | --------------------------------------------------------------------- | +| Apply an effect | `entity.addEffect("minecraft:speed", duration, level, hideParticles)` | +| Clear all effects | `entity.clearEffects()` | + +### Event System + +| I want to... | Use this | +| --------------------- | --------------------------------------------------------------------------- | +| Run every tick | `world.onTick((info) => { ... })` | +| On player join | `world.onPlayerJoin((entity, tick) => { ... })` | +| On player leave | `world.onPlayerLeave((entity, tick) => { ... })` | +| On entity death | `world.onEntityDeath((entity, killer, tick) => { ... })` | +| On entity damaged | `world.onEntityDamage((entity, amount, source, attacker, tick) => { ... })` | +| On right-click entity | `world.onInteract((entity, target, tick) => { ... })` | +| On right-click block | `world.onBlockActivate((entity, x, y, z, voxel, tick) => { ... })` | +| On button pressed | `world.onButtonPressed((entity, button, tick) => { ... })` | +| On player respawn | `world.onPlayerRespawn((entity, tick) => { ... })` | +| Run once after delay | `setTimeout(() => { ... }, ticks)` | +| Run on interval | `setInterval(() => { ... }, ticks)` | +| Cancel event listener | `token.cancel()` | +| Check if active | `token.active()` | + +### Data Persistence + +| I want to... | Use this | +| -------------------- | ------------------------------- | +| Read/write JSON data | `storage.getDataStorage("key")` | +| SQL query | `db.sql("SELECT ...")` | +| SQL write | `db.sql("INSERT INTO ...")` | + +### HTTP Requests + +| I want to... | Use this | +| ------------ | ---------------------------------------------------- | +| GET request | `http.fetch("https://...")` | +| POST JSON | `http.fetch(url, { method: "POST", headers, body })` | +| Parse JSON | `resp.json()` or `{ responseType: "json" }` | +| Read text | `resp.text()` | +| Set timeout | `http.fetch(url, { timeout: 5000 })` | + +### Game Systems + +| I want to... | Use this | +| ------------------ | ------------------------------------------------ | +| Create scoreboard | `world.addScoreboard("name")` | +| Set score | `world.setScore("player", "board", 10)` | +| Display scoreboard | `world.showScoreboard("sidebar", "name")` | +| Show BossBar | `world.showBossbar("id", "title", 0.5, "red")` | +| Create team | `world.createTeam("teamName", "color")` | +| Join team | `world.joinTeam(entity, "teamName")` | +| Set world border | `world.borderSize = 500` | +| Shrink border | `world.shrinkBorder(100, 60)` | +| Change time | `world.time = 6000` | +| Set weather | `world.rainDensity = 0` / `world.clearWeather()` | +| Change game rule | `world.setGameRule("keepInventory", true)` | + +### Math Tools + +| I want to... | Use this | +| ------------- | ------------------------------------------------------------- | +| 3D coordinate | `new GameVector3(x, y, z)` | +| Vector math | `v.add(other)`, `v.scale(n)`, `v.length()` | +| Bounding box | `new GameBounds3(min, max)` | +| Color | `new GameRGBColor(r, g, b)` / `new GameRGBAColor(r, g, b, a)` | + +### Cross-Script Messaging + +| I want to... | Use this | +| -------------------------- | ------------------------------------------ | +| Send to another script | `world.sendMessage("projectName", data)` | +| Receive from other scripts | `world.onMessage((from, data) => { ... })` | + +## Global Objects + +| Object | Type | Description | +| ---------------- | ------------ | -------------------------------------------------------------------------------------------- | +| `world` | Server | World control, see [world.md](world.md) | +| `voxels` | Server | Block operations, see [voxels.md](voxels.md) | +| `entity` | Server value | Entity wrapper (from callbacks or `world.spawnEntity`), see [entity.md](entity.md) | +| `player` | Server value | Player wrapper (via `entity.player`), see [player.md](player.md) | +| `storage` | Both | Data persistence, see [storage.md](storage.md) | +| `db` | Both | SQLite database, see [database.md](database.md) | +| `http` | Both | HTTP requests, see [http.md](http.md) | +| `audio` | Client | Client sound, music, volume control, see [client.md](client.md) | +| `client` | Client | Client lifecycle, see [client.md](client.md) | +| `input` | Client | Client keyboard input, see [client.md](client.md) | +| `ui` | Client | Client screen UI, see [client.md](client.md) | +| `chat` | Client | Client chat send/receive, see [client.md](client.md) | +| `gui` | Client | Custom container GUI, see [client.md](client.md) | +| `remoteChannel` | Both | Server↔client event channel, see [server.md](server.md) / [client.md](client.md) | +| `registries` | Server | Custom blocks, items & sounds (compiled mode), see [registries.md](registries.md) | +| `console` | Both | Console logging (`log`/`warn`/`error`/`debug`) | +| `GameVector3` | Both | 3D vector, see [math.md](math.md) | +| `GameBounds3` | Both | Bounding box, see [math.md](math.md) | +| `GameRGBColor` | Both | RGB color, see [math.md](math.md) | +| `GameRGBAColor` | Both | RGBA color, see [math.md](math.md) | +| `GameQuaternion` | Both | Quaternion, see [math.md](math.md) | + +## API Legend + +| Label | Meaning | +| ------------------ | ------------------------------------------------------------------ | +| ✅ **Box3 API** | Originates from the Box3 platform; naming and semantics match Box3 | +| ⬆ **MC Extension** | Not in original Box3; added using Minecraft-specific features | + +## Documentation Style + +Each API document should follow this structure. Use the same style when adding future APIs: + +1. State the runtime at the top: server, client, or shared. +2. List globals and core concepts before method details. +3. Use `object.method(parameters)` for method headings. +4. Document parameters in tables with name, type, default, and meaning. +5. Prefer TypeScript/JavaScript examples and identify server or client context when needed. +6. For cross-side APIs, always state the direction: server → client, or client → server. +7. If docs and types disagree, treat `types/server/index.d.ts` and `types/client/index.d.ts` as the source of truth, then update the docs. + +## Detailed Document Index + +| Document | Content | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| [server.md](server.md) | Server API overview: runtime boundary, globals, events, players/entities, blocks, data, cross-side communication | +| [world.md](world.md) | World state, events, scoreboard, bossbar, teams, border, particles, fireworks, lightning, sounds | +| [entity.md](entity.md) | Entity properties, AI, equipment, potion effects, pathfinding, tags, collisions | +| [player.md](player.md) | Inventory, messaging, flight, game mode, teleport, commands, XP | +| [voxels.md](voxels.md) | Block read/write, region fill, spawner control | +| [storage.md](storage.md) | Persistent data storage | +| [database.md](database.md) | SQLite database API | +| [http.md](http.md) | HTTP request API | +| [client.md](client.md) | Client API: lifecycle, keyboard, screen UI, chat, GUI, remoteChannel, client-side storage | +| [registries.md](registries.md) | Custom blocks, items & sounds (blocks.json, items.json, sounds.json, creativeTabs.json) | +| [math.md](math.md) | GameVector3, GameBounds3, GameRGBColor, GameRGBAColor, GameQuaternion | +| [commands.md](commands.md) | `/box3script` command reference | + +## File Modules — TypeScript Build Pipeline + +Projects created with `/box3script create` come with a complete TS build environment: + +```text +config/box3/script/mygame/ +├── package.json ← esbuild + Babel + @babel/preset-typescript +├── tsconfig.base.json ← Shared TS compiler options +├── tsconfig.server.json ← Server-side TS config +├── tsconfig.client.json ← Client-side TS config +├── build.mjs ← Babel TS→JS → esbuild bundle → dist/ +├── types/ +│ ├── shared.d.ts ← Shared types (server & client) +│ ├── server/ +│ │ ├── index.d.ts ← Server type entry point +│ │ ├── server.d.ts +│ │ ├── entity.d.ts +│ │ ├── player.d.ts +│ │ ├── world.d.ts +│ │ └── voxels.d.ts +│ └── client/ +│ ├── index.d.ts ← Client type entry point +│ ├── client.d.ts +│ ├── audio.d.ts +│ ├── input.d.ts +│ ├── ui.d.ts +│ ├── chat.d.ts +│ └── gui.d.ts +├── src/ +│ ├── server/ +│ │ ├── app.ts ← Server entry point +│ │ └── ... +│ └── client/ +│ ├── app.ts ← Client entry point +│ └── ... +└── dist/ + ├── server.js ← Server compiled output + ├── client.js ← Client compiled output + └── -.jar ← Standalone JAR (/box3script compile) +``` + +Run `npm run build` to build. Use `/box3script watch` to enable file watching for auto hot-reload. + +## Deployment + +When ready to distribute, compile your script into a **standalone JAR mod** that runs on any NeoForge server alongside Box3JS: + +```js +/box3script compile +``` + +Outputs `-.jar` (metadata read from `package.json`: name, displayName, version, description, author, license, homepage, logoFile). Drop it into `mods/` and start the server. + +See [full command reference →](commands.md#box3script-compile-project) + +## Tick Conversion + +| Duration | Ticks | +| ---------- | ----- | +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1200 | +| 5 minutes | 6000 | + +## Deep Dive + +| Doc | Content | +| --------------------------------------------- | -------------------------------------------------------------- | +| [Quick Start](../guide/getting-started.md) | Setup, first script, dev cycle, debugging, deployment | +| [Architecture](../guide/architecture.md) | Rhino engine, scopes, event callbacks, build pipeline, network | +| [JS vs Java](../guide/js-vs-java.md) | Box3JS scripting vs native Java modding comparison | + +## Tutorials + +Learn Box3JS from scratch with the tutorial series in `docs/tutorial/`: + +| Tutorial | Content | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| [01-basics.md](../tutorial/01-basics.md) | From zero: first script, chat commands, timers | +| [02-player-items.md](../tutorial/02-player-items.md) | Player controls: teleport, items, potion effects, game modes | +| [03-events-entities.md](../tutorial/03-events-entities.md) | Events & entities: AI, combat, patrols | +| [04-advanced-systems.md](../tutorial/04-advanced-systems.md) | Advanced: scoreboard, BossBar, teams, world border | +| [05-examples.md](../tutorial/05-examples.md) | Real-world: PvP arena, effects, fireworks, wave mobs | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/client.md similarity index 83% rename from Box3JS-NeoForge-1.21.1/docs/api/client_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/client.md index 10b91d3..383325a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/client_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/client.md @@ -1,3 +1,6 @@ +--- +--- + # client — Client-side API Client scripts run locally on the player's Minecraft client. The entry file is `src/client/app.ts`, and the compiled output is `dist/client.js`. Client APIs handle local UI, input, audio, chat helpers, local storage, local HTTP/SQLite, and cross-side events. @@ -17,8 +20,9 @@ Client scripts access APIs through these globals: | `gui` | `GameGUI` | Custom container GUI interface | | `remoteChannel` | `RemoteChannel` | Client ↔ Server event communication | -> **Prerequisite:** The client must have the Box3JS mod installed. The server must enable the project's client script, which is automatically sent to connecting players. -> Client scripts go in `src/client/`, server scripts in `src/server/`. The client type entry is `types/client/index.d.ts`; it does not include server APIs such as `world` / `voxels`. +::: info +The client must have the Box3JS mod installed. Client scripts go in `src/client/`, server scripts in `src/server/`. The client type entry is `types/client/index.d.ts`; it does not include server APIs such as `world` / `voxels`. +::: Client scripts cannot directly modify the server world. To change blocks, players, entities, or scoreboards, send an event to the server: @@ -104,15 +108,19 @@ audio.setVolume("player", 0.8); ### client.onTick(callback) -Registers a callback invoked every client tick (20 times/sec). No parameters, no return value. +Registers a callback invoked every client tick (20 times/sec). It receives no parameters and returns a `GameEventHandlerToken`; call `cancel()` to unsubscribe. ```js -client.onTick(() => { +const token = client.onTick(() => { // Per-frame logic }); + +// token.cancel(); ``` -> **Note:** Server-side `world.onTick()` receives a `TickInfo` object. Client-side `client.onTick()` receives nothing. +::: info Note +Server-side `world.onTick()` receives a `TickInfo` object. Client-side `client.onTick()` receives nothing. +::: ### client.getFPS() @@ -162,6 +170,70 @@ if (!info.isLocal) { } ``` +### Fog Control + +The Box3JS client can override Minecraft's fog colour and distance, providing effects similar to Box3's `world.fogColor` / `world.maxFog`. + +### client.getFogColor() + +Gets the current custom fog colour. Returns `null` if not set. + +```js +var color = client.getFogColor(); +if (color) { + console.log("Fog color: " + color.r + ", " + color.g + ", " + color.b); +} +``` + +### client.setFogColor(r, g, b) + +Sets the fog colour (RGB 0-255). + +| Parameter | Type | Description | +|-----------|--------|----------------| +| `r` | number | Red (0-255) | +| `g` | number | Green (0-255) | +| `b` | number | Blue (0-255) | + +```js +// Red fog effect +client.setFogColor(255, 50, 50); +``` + +### client.setFogStartDistance(distance) + +Sets the distance (in blocks) where fog begins. Fully transparent below this distance. + +| Parameter | Type | Description | +|------------|--------|---------------------------| +| `distance` | number | Fog start distance (blocks) | + +```js +// Fog starts 10 blocks away +client.setFogStartDistance(10); +``` + +### client.setFogEndDistance(distance) + +Sets the distance (in blocks) where fog becomes fully opaque, equivalent to Box3's `maxFog`. + +| Parameter | Type | Description | +|------------|--------|-------------------------| +| `distance` | number | Fog end distance (blocks) | + +```js +// Fully obscured by fog beyond 50 blocks +client.setFogEndDistance(50); +``` + +### client.resetFog() + +Resets fog to Minecraft's default behaviour. + +```js +client.resetFog(); +``` + ## input — Keyboard Input ### input.isKeyDown(key) @@ -240,7 +312,7 @@ token.cancel(); | Function keys | `f1`–`f12` | | Arrow keys | `up`, `down`, `left`, `right` | | Special keys | `space`, `enter`, `escape`, `tab`, `backspace`, `delete` | -| Modifiers | `left_shift`, `right_shift`, `left_ctrl`, `right_ctrl`, `left_alt`, `right_alt` | +| Modifiers | `shift`, `left_shift`, `right_shift`, `ctrl`, `left_ctrl`, `right_ctrl`, `alt`, `left_alt`, `right_alt` | ## ui — Screen UI @@ -451,8 +523,9 @@ remoteChannel.onClientEvent((event) => { }); ``` -> Server-side equivalents: `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`. -> See type declarations in `server.d.ts`. +::: info +Server-side equivalents: `remoteChannel.sendClientEvent()` / `broadcastClientEvent()` / `onServerEvent()`. See type declarations in `server.d.ts`. +::: ## storage — Client-side Storage @@ -464,7 +537,7 @@ store.set("volume", 0.8); var volume = store.get("volume"); // 0.8 ``` -Full API reference: [storage_en.md](storage_en.md). +Full API reference: [storage.md](storage.md). ## Complete Client Example diff --git a/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/commands.md similarity index 84% rename from Box3JS-NeoForge-1.21.1/docs/api/commands_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/commands.md index ac2058d..79ee38c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/commands_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/commands.md @@ -1,3 +1,6 @@ +--- +--- + # /box3script Command Reference All commands require **OP level 2** (default admin permission). All `` arguments support **Tab completion**. @@ -8,13 +11,13 @@ All commands require **OP level 2** (default admin permission). All `` Shows project status overview. -``` +```js /box3script ``` Example output: -``` +```text ══ Box3JS Script Engine ══ Watch: ● Active Sandbox: ● 1 project(s) @@ -39,7 +42,7 @@ Example output: Creates a new TypeScript script project. Generates a complete TS scaffold, **disabled** by default. -``` +```js /box3script create mygame ``` @@ -56,7 +59,7 @@ Then enable with `/box3script start mygame`. Enable and load projects. **No args** = all projects. **Project name** = only that project. **`all`** = explicitly all. -``` +```js /box3script start # enable all /box3script start all # enable all (same as no args) /box3script start mygame # enable only mygame @@ -66,7 +69,7 @@ Enable and load projects. **No args** = all projects. **Project name** = only th Disable and unload projects. **No args** = all projects. **Project name** = only that project. **`all`** = explicitly all. -``` +```js /box3script stop # disable all /box3script stop all # disable all (same as no args) /box3script stop mygame # disable only mygame @@ -76,7 +79,7 @@ Disable and unload projects. **No args** = all projects. **Project name** = only Reload scripts. **No args** = stop all, reload all enabled projects. **With project name** = reload only that project. -``` +```js /box3script reload # reload all enabled projects /box3script reload mygame # reload only mygame ``` @@ -87,7 +90,7 @@ After editing code and running `npm run build`, use `reload` to apply changes. O Toggle file watching. When on, monitors `dist/` across all projects and auto-reloads on `.js` file changes. -``` +```js /box3script watch # toggle on/off ``` @@ -95,13 +98,13 @@ Toggle file watching. When on, monitors `dist/` across all projects and auto-rel Toggle sandbox mode. When enabled, tracks all block/entity/world state changes. When disabled, rolls back and shows summary. -``` +```js /box3script sandbox mygame # toggle on/off ``` Typical workflow: -``` +```js /box3script sandbox mygame # enable sandbox /box3script start mygame # load project # ... test ... @@ -110,19 +113,25 @@ Typical workflow: /box3script sandbox mygame # disable sandbox → full rollback ``` -> **Note:** Sandbox only tracks blocks placed through script APIs (`setVoxel`/`setVoxelId`/`fillVoxel`). Manual mining is unaffected. +::: warning +Sandbox only tracks blocks placed through script APIs (`setVoxel`/`setVoxelId`/`fillVoxel`). Manual mining is unaffected. +::: ### `/box3script compile ` Compiles a script project into a **lightweight standalone JAR mod** (~50KB) that depends on the Box3JS mod for Rhino runtime and API bindings. -``` +```js /box3script compile mygame ``` -> **Dependency:** Script JARs do not bundle Rhino or Box3JS API classes. Place the Box3JS mod (`box3js`) alongside your script JAR(s) in `mods/`. +::: warning Dependency +Script JARs do not bundle Rhino or Box3JS API classes. Place the Box3JS mod (`box3js`) alongside your script JAR(s) in `mods/`. +::: -> **Custom registries:** If `registries/blocks.json`, `items.json`, `sounds.json`, `creativeTabs.json` and `assets/` are present, blocks/items/sounds are registered and resources are bundled into the JAR. The client must also install the JAR for rendering. See [registries_en.md](registries_en.md). +::: info Custom registries +If `registries/blocks.json`, `items.json`, `sounds.json`, `creativeTabs.json` and `assets/` are present, blocks/items/sounds are registered and resources are bundled into the JAR. The client must also install the JAR for rendering. See [registries.md](registries.md). +::: The compiler **reads the following `package.json` fields** and writes them to `neoforge.mods.toml`: @@ -138,7 +147,9 @@ The compiler **reads the following `package.json` fields** and writes them to `n | `bugs.url` | `issueTrackerURL` | Issue tracker link | | `logoFile` | `logoFile` | Mod icon (PNG path in project, bundled as `logo.png`) | -> **`logoFile` usage:** Set to a relative path of a PNG file in the project root (e.g. `"logoFile": "logo.png"`). The file is automatically bundled as `logo.png` in the JAR root — no manual `neoforge.mods.toml` config needed. NeoForge recommends 128×128 or 256×256, PNG format only. Leave empty for the default mod icon. +::: tip logoFile usage +Set to a relative path of a PNG file in the project root (e.g. `"logoFile": "logo.png"`). The file is automatically bundled as `logo.png` in the JAR root — no manual `neoforge.mods.toml` config needed. NeoForge recommends 128×128 or 256×256, PNG format only. Leave empty for the default mod icon. +::: Output filename format: `dist/-.jar`. Compilation runs on a background thread — no server tick blocking. The output path is shown in chat on completion. @@ -149,7 +160,7 @@ Output filename format: `dist/-.jar`. Compilation runs on a backg **Output JAR contents:** -``` +```text mygame-1.0.0.jar ├── META-INF/neoforge.mods.toml ← mod metadata (depends on box3js) ├── logo.png ← mod icon (if specified) @@ -166,7 +177,7 @@ mygame-1.0.0.jar **Deployment:** Place the script JAR alongside the Box3JS mod in `mods/`: -``` +```text mods/ ├── box3js-1.0.0.jar ← Box3JS main mod └── mygame-1.0.0.jar ← compiled script mod @@ -183,7 +194,9 @@ mods/ | Hot reload | Yes | No (restart server to update) | | Use case | Development & debugging | Distribution & deployment | -> **Note:** Compiled JARs are standard NeoForge mods managed by the NeoForge mod loader. They are **not** controlled by `/box3script start/stop/reload`. Multiple compiled JARs can coexist in `mods/` — each runs independently with its own hardcoded metadata. +::: warning +Compiled JARs are standard NeoForge mods managed by the NeoForge mod loader. They are **not** controlled by `/box3script start/stop/reload`. Multiple compiled JARs can coexist in `mods/` — each runs independently with its own hardcoded metadata. +::: ## Configuration File @@ -198,7 +211,7 @@ Enable/disable state is saved in `config/box3/scripts.json`: ## Script Directory Structure -``` +```text config/box3/ ├── scripts.json ← project enable/disable config ├── script/ ← scripts directory diff --git a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/database.md similarity index 92% rename from Box3JS-NeoForge-1.21.1/docs/api/database_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/database.md index bde65ce..f7cb4d6 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/database_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/database.md @@ -1,8 +1,13 @@ +--- +--- + # Database API Box3JS exposes SQLite capabilities through the global `db` object. Connections are managed automatically. -> **Runtime:** Available on both server and client. Server databases live at `config/box3/data/.db`; client databases live under the local game directory at `box3/client-db/.db`. The two sides do not share database files; use `remoteChannel` when data must be synchronized. +::: info Runtime +Available on both server and client. Server databases live at `config/box3/data/.db`; client databases live under the local game directory at `box3/client-db/.db`. The two sides do not share database files; use `remoteChannel` when data must be synchronized. +::: ## Dependency & Graceful Fallback @@ -16,10 +21,14 @@ db API requires SQLite JDBC driver. Install the minecraft-sqlite-jdbc mod, then After installing `minecraft-sqlite-jdbc` and restarting the server, the `db` API becomes available. -> **NeoForge dev environment note:** -> -> - Put `minecraft-sqlite-jdbc` under `run/mods/`. -> - The file must be a `.jar` (for example, `xxx.jar`), not `.zip`, otherwise NeoForge will not load it. +::: warning NeoForge dev environment +- Put `minecraft-sqlite-jdbc` under `run/mods/`. +- The file must be a `.jar` (for example, `xxx.jar`), not `.zip`, otherwise NeoForge will not load it. +::: + +## `db.isAvailable()` + +Checks whether the SQLite JDBC driver is available. When unavailable, `db.sql(...)` returns a safe empty error result or shows a clear hint; scripts can call this first to degrade gracefully. ## `db.sql(sql, ...params)` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md similarity index 96% rename from Box3JS-NeoForge-1.21.1/docs/api/entity_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/entity.md index f218985..8eaa364 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/entity.md @@ -1,6 +1,9 @@ +--- +--- + # entity — Entity API -`entity` represents any entity in the Minecraft world (mobs, animals, items, players). Obtain via `world.spawnEntity()`, `world.createEntity()`, `world.querySelector()`, `world.searchBox()`, `world.entitiesInRadius()`, or event callback parameters. +`entity` represents any entity in the Minecraft world (mobs, animals, items, players). Use `entity.player` to get the corresponding `player` object (non-null only when the entity is a player). @@ -415,7 +418,9 @@ entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); entity.setAttribute("minecraft:generic.armor", 10); ``` -> Note: `maxHp` / `hp` / `walkSpeed` / `jumpPower` and other Box3 convenience properties use these attributes internally. Prefer the convenience properties; use `setAttribute` only for attributes not exposed as properties. +::: tip +`maxHp` / `hp` / `walkSpeed` / `jumpPower` and other Box3 convenience properties use these attributes internally. Prefer the convenience properties; use `setAttribute` only for attributes not exposed as properties. +::: ## Lifecycle diff --git a/Box3JS-NeoForge-1.21.1/docs/api/http_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/http.md similarity index 84% rename from Box3JS-NeoForge-1.21.1/docs/api/http_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/http.md index 2b419c4..b0542fe 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/http_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/http.md @@ -1,8 +1,13 @@ +--- +--- + # HTTP API -Box3JS provides HTTP request capabilities via the global `http` object, supporting all HTTP methods, timeout, custom headers, auto-parsing, binary uploads, and both synchronous and asynchronous calling modes. +HTTP requests via the global `http` object. -> **Runtime:** Available on both server and client. Server-side synchronous requests block the server tick, so avoid long-running requests in high-frequency callbacks. Client-side synchronous requests block the client render/logic thread. **Async requests** (`async: true`) deliver results via callbacks. +::: info Runtime +Available on both server and client. Synchronous requests block the current thread — avoid long requests in high-frequency callbacks. **Async requests** (`async: true`) deliver results via callbacks. +::: ## `http.fetch(url, options?)` @@ -27,9 +32,11 @@ Sends an HTTP request and returns `GameHttpFetchResponse`. | `onResponse` | `function` | — | Callback on async success, receives `GameHttpFetchResponse` | | `onError` | `function` | — | Callback on async failure, receives error message string | -> When `responseType` is set, the parsed result is available via `resp.data` — no need to call `resp.json()` manually. -> -> In async mode `fetch()` returns `null`. Results are delivered via callbacks. +::: info +When `responseType` is set, the parsed result is available via `resp.data` — no need to call `resp.json()` manually. + +In async mode `fetch()` returns `null`. Results are delivered via callbacks. +::: ## GameHttpFetchResponse diff --git a/Box3JS-NeoForge-1.21.1/docs/en/api/math.md b/Box3JS-NeoForge-1.21.1/docs/en/api/math.md new file mode 100644 index 0000000..1915335 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/math.md @@ -0,0 +1,408 @@ +--- +--- + +# Math Types + +The following data types are globally available in JS. + +## GameVector3 + +A 3D vector with double-precision components. Used for position, direction, velocity, etc. + +### Constructor + +```js +var v = new GameVector3(); // Zero vector (0, 0, 0) +var v = new GameVector3(x, y, z); // Specified coordinates +``` + +### Properties + +| Property | Type | Description | +| -------- | -------- | ------------------------------------- | +| `v.x` | `number` | X component (east/west), read/write | +| `v.y` | `number` | Y component (up/down), read/write | +| `v.z` | `number` | Z component (north/south), read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ---------------- | ------------- | --------------------------------------------------------------------- | +| `v.set(x, y, z)` | `GameVector3` | Set all components | +| `v.copy(w)` | `GameVector3` | Copy all components from `w` | +| `v.addEq(w)` | `GameVector3` | In-place addition: `v += w` | +| `v.subEq(w)` | `GameVector3` | In-place subtraction: `v -= w` | +| `v.mulEq(w)` | `GameVector3` | In-place component-wise multiplication | +| `v.divEq(w)` | `GameVector3` | In-place component-wise division; divide-by-zero skips that component | +| `v.scaleEq(n)` | `GameVector3` | In-place scalar multiplication: `v.x *= n` … | +| `v.negEq()` | `GameVector3` | In-place negation: `v = -v` | + +#### Creating New Vectors (does not mutate) + +| Method | Returns | Description | +| --------------------------------- | ------------- | --------------------------------------------------------------- | +| `v.clone()` | `GameVector3` | Deep copy — independent vector with same values | +| `v.add(w)` | `GameVector3` | Vector addition: `v + w` | +| `v.sub(w)` | `GameVector3` | Vector subtraction: `v - w` | +| `v.mul(w)` | `GameVector3` | Component-wise multiplication | +| `v.div(w)` | `GameVector3` | Component-wise division; divide-by-zero → 0 | +| `v.scale(n)` | `GameVector3` | Scalar multiplication: each component × `n` | +| `v.cross(w)` | `GameVector3` | Cross product: `v × w` | +| `v.normalize()` | `GameVector3` | Unit vector; zero vector returns `(0,0,0)` | +| `v.lerp(w, t)` | `GameVector3` | Linear interpolation: `t=0` → this, `t=1` → `w` | +| `v.towards(w)` | `GameVector3` | Direction vector pointing toward `w` (normalized) | +| `v.max(w)` | `GameVector3` | Component-wise maximum | +| `v.min(w)` | `GameVector3` | Component-wise minimum | +| `v.neg()` | `GameVector3` | Negation: `-v` | +| `v.moveTowards(target, maxDelta)` | `GameVector3` | Move toward target by at most `maxDelta` distance | +| `v.floor()` | `GameVector3` | Component-wise floor | +| `v.ceil()` | `GameVector3` | Component-wise ceiling | +| `v.clampLength(max)` | `GameVector3` | Clamp magnitude to `max`, scale down proportionally if exceeded | + +#### Numeric Computations + +| Method | Returns | Description | +| ------------------ | -------- | -------------------------------------------------- | +| `v.dot(w)` | `number` | Dot (inner) product: `v · w` | +| `v.mag()` | `number` | Magnitude (length) | +| `v.sqrMag()` | `number` | Squared magnitude — faster than `mag()` | +| `v.distance(w)` | `number` | Euclidean distance to `w` | +| `v.angle(w)` | `number` | Angle between `v` and `w` (radians, 0–π) | +| `v.sqrDistance(w)` | `number` | Squared distance to `w` — faster than `distance()` | + +#### Comparison + +| Method | Returns | Description | +| ------------------ | --------- | ------------------------------------------------------------- | +| `v.equals(w)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `v.exactEquals(w)` | `boolean` | Exact equality — components strictly equal | +| `v.isZero()` | `boolean` | Whether this is (approximately) a zero vector, tolerance 1e-6 | + +```js +var pos = new GameVector3(0, 100, 0); +var target = new GameVector3(10, 100, 10); + +// Distance +var dist = pos.distance(target); // ~14.14 + +// Direction vector +var dir = target.sub(pos).normalize(); + +// Angle +var angle = pos.angle(target); // radians + +// Comparison +var a = new GameVector3(1, 2, 3); +var b = new GameVector3(1.0000001, 2.0000001, 3.0000001); +a.equals(b); // true (within tolerance) +a.exactEquals(b); // false + +// Teleport entity (LiveVec3) +entity.position.set(0, 100, 0); +``` + +### Static Methods + +```js +// Spherical coordinates → vector +var v = GameVector3.fromPolar(mag, phi, theta); +// mag: radius +// phi: azimuth angle (radians, horizontal rotation around Y) +// theta: elevation angle (radians, from horizontal plane) +``` + +### toString + +```js +var v = new GameVector3(1, 2, 3); +v.toString(); // "GameVector3(1.0, 2.0, 3.0)" +``` + +## GameBounds3 + +Axis-aligned bounding box (AABB), defined by two opposing corners: `lo` (minimum corner) and `hi` (maximum corner). + +### Constructor + +```js +var bounds = new GameBounds3( + new GameVector3(-1, 0, -1), // lo (min corner) + new GameVector3(1, 2, 1), // hi (max corner) +); +``` + +### Properties + +| Property | Type | Description | +| ----------- | ------------- | -------------------------- | +| `bounds.lo` | `GameVector3` | Minimum corner, read/write | +| `bounds.hi` | `GameVector3` | Maximum corner, read/write | + +### Instance Methods + +| Method | Returns | Description | +| ------------------------------------------ | --------------------- | ---------------------------------------------------------- | +| `bounds.set(lox, loy, loz, hix, hiy, hiz)` | `GameBounds3` | Set all boundaries in-place, returns this | +| `bounds.copy(b)` | `GameBounds3` | Copy values from `b` in-place, returns this | +| `bounds.intersects(other)` | `boolean` | Whether this intersects `other` | +| `bounds.intersect(other)` | `GameBounds3 \| null` | Intersection bounds, or `null` if no overlap | +| `bounds.contains(v)` | `boolean` | Whether point `v` is inside (inclusive) | +| `bounds.containsBounds(b)` | `boolean` | Whether this fully contains `b` | +| `bounds.center()` | `GameVector3` | Center point of the bounds | +| `bounds.size()` | `GameVector3` | Size of the bounds (width, height, depth) | +| `bounds.expand(delta)` | `GameBounds3` | Expand all faces outward by `delta`, returns new bounds | +| `bounds.expandEq(delta)` | `GameBounds3` | In-place expand all faces outward by `delta`, returns this | +| `bounds.growToInclude(v)` | `GameBounds3` | In-place grow to include point `v`, returns this | +| `bounds.closestPoint(v)` | `GameVector3` | Closest point on the bounds to point `v` | +| `bounds.move(offset)` | `GameBounds3` | Translate by `offset`, returns new bounds | +| `bounds.moveEq(offset)` | `GameBounds3` | In-place translate by `offset`, returns this | + +### Static Methods + +```js +// Create minimal bounds from an array of GameVector3 +var points = [new GameVector3(0, 0, 0), new GameVector3(5, 10, 3)]; +var box = GameBounds3.fromPoints(points); // returns GameBounds3 or null +``` + +### toString + +```js +bounds.toString(); // "GameBounds3(GameVector3(-1.0, 0.0, -1.0), GameVector3(1.0, 2.0, 1.0))" +``` + +```js +// Query entities within bounds +var entities = world.searchBox(bounds); + +// Check if point is inside +if (bounds.contains(player.position)) { + // Player is inside the area +} +``` + +## GameRGBColor + +An RGB color with three channels ranging from 0.0 to 1.0. + +### Constructor + +```js +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); +``` + +### Properties + +| Property | Type | Description | +| --------- | -------- | ------------------------------- | +| `color.r` | `number` | Red channel (0–1), read/write | +| `color.g` | `number` | Green channel (0–1), read/write | +| `color.b` | `number` | Blue channel (0–1), read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ---------------- | -------------- | ---------------------------------------------------- | +| `c.set(r, g, b)` | `GameRGBColor` | Set all channels | +| `c.copy(o)` | `GameRGBColor` | Copy all channels from another color | +| `c.addEq(o)` | `GameRGBColor` | In-place addition: `c += o` | +| `c.subEq(o)` | `GameRGBColor` | In-place subtraction: `c -= o` | +| `c.mulEq(o)` | `GameRGBColor` | In-place channel-wise multiplication | +| `c.divEq(o)` | `GameRGBColor` | In-place channel-wise division; divide-by-zero skips | +| `c.scaleEq(n)` | `GameRGBColor` | In-place scalar multiplication: each channel × `n` | + +#### Creating New Colors (does not mutate) + +| Method | Returns | Description | +| -------------- | -------------- | ----------------------------------------------- | +| `c.clone()` | `GameRGBColor` | Deep copy | +| `c.add(o)` | `GameRGBColor` | Channel-wise addition | +| `c.sub(o)` | `GameRGBColor` | Channel-wise subtraction | +| `c.mul(o)` | `GameRGBColor` | Channel-wise multiplication | +| `c.div(o)` | `GameRGBColor` | Channel-wise division; divide-by-zero → 0 | +| `c.lerp(o, t)` | `GameRGBColor` | Linear interpolation: `t=0` → this, `t=1` → `o` | +| `c.scale(n)` | `GameRGBColor` | Scalar multiplication: each channel × `n` | +| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `c.toRGBA()` | `string` | CSS format string: `"rgba(r,g,b,1.0)"` | + +### Static Methods + +```js +var randomColor = GameRGBColor.random(); // Each channel 0–1 random +``` + +### toString + +```js +new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" +``` + +## GameRGBAColor + +An RGBA color with four channels ranging from 0.0 to 1.0. + +### Constructor + +```js +var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +var opaque = new GameRGBAColor(0, 1, 0, 1.0); +``` + +### Properties + +| Property | Type | Description | +| --------- | -------- | ------------------------------- | +| `color.r` | `number` | Red channel (0–1), read/write | +| `color.g` | `number` | Green channel (0–1), read/write | +| `color.b` | `number` | Blue channel (0–1), read/write | +| `color.a` | `number` | Alpha opacity (0–1), read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ------------------- | --------------- | ---------------------------------------------------- | +| `c.set(r, g, b, a)` | `GameRGBAColor` | Set all four channels | +| `c.copy(o)` | `GameRGBAColor` | Copy all channels from another RGBA color | +| `c.addEq(o)` | `GameRGBAColor` | In-place addition | +| `c.subEq(o)` | `GameRGBAColor` | In-place subtraction | +| `c.mulEq(o)` | `GameRGBAColor` | In-place channel-wise multiplication | +| `c.divEq(o)` | `GameRGBAColor` | In-place channel-wise division; divide-by-zero skips | +| `c.scaleEq(n)` | `GameRGBAColor` | In-place scalar multiplication: each channel × `n` | + +#### Creating New Colors (does not mutate) + +| Method | Returns | Description | +| ---------------- | --------------- | --------------------------------------------------------- | +| `c.clone()` | `GameRGBAColor` | Deep copy | +| `c.add(o)` | `GameRGBAColor` | Channel-wise addition | +| `c.sub(o)` | `GameRGBAColor` | Channel-wise subtraction | +| `c.mul(o)` | `GameRGBAColor` | Channel-wise multiplication | +| `c.div(o)` | `GameRGBAColor` | Channel-wise division; divide-by-zero → 0 | +| `c.lerp(o, t)` | `GameRGBAColor` | Linear interpolation | +| `c.scale(n)` | `GameRGBAColor` | Scalar multiplication: each channel × `n` | +| `c.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `c.blendEq(rgb)` | `GameRGBColor` | Alpha-blend onto an RGB background, returns displayed RGB | + +### toString + +```js +new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5)" +``` + +```js +// Alpha blending +var fg = new GameRGBAColor(1, 0, 0, 0.5); // Semi-transparent red +var bg = new GameRGBColor(1, 1, 1); // White background +var result = fg.blendEq(bg); // Blended RGB color +``` + +## GameQuaternion + +A quaternion used for 3D rotation. Unit quaternions (magnitude=1) represent pure rotations. + +### Constructor + +```js +var q = new GameQuaternion(); // Identity (1, 0, 0, 0) +var q = new GameQuaternion(w, x, y, z); // Specified components +``` + +### Properties + +| Property | Type | Description | +| -------- | -------- | ----------------------------------- | +| `q.w` | `number` | Real (scalar) component, read/write | +| `q.x` | `number` | Imaginary X component, read/write | +| `q.y` | `number` | Imaginary Y component, read/write | +| `q.z` | `number` | Imaginary Z component, read/write | + +### Instance Methods + +#### Mutating (return this) + +| Method | Returns | Description | +| ------------------- | ---------------- | ---------------------------- | +| `q.set(w, x, y, z)` | `GameQuaternion` | Set all components | +| `q.copy(p)` | `GameQuaternion` | Copy all components from `p` | + +#### Creating New Quaternions (does not mutate) + +| Method | Returns | Description | +| --------------- | ---------------- | ----------------------------------------------- | +| `q.clone()` | `GameQuaternion` | Deep copy | +| `q.add(p)` | `GameQuaternion` | Component-wise addition | +| `q.sub(p)` | `GameQuaternion` | Component-wise subtraction | +| `q.mul(p)` | `GameQuaternion` | Hamilton product: `q × p` (NOT commutative) | +| `q.div(p)` | `GameQuaternion` | Division: `q × p⁻¹` | +| `q.inv()` | `GameQuaternion` | Conjugate (equals inverse for unit quaternions) | +| `q.normalize()` | `GameQuaternion` | Normalize, returns unit quaternion | + +#### Interpolation + +| Method | Returns | Description | +| --------------- | ---------------- | --------------------------------------------------------- | +| `q.slerp(p, t)` | `GameQuaternion` | Spherical linear interpolation: `t=0` → this, `t=1` → `p` | + +#### Numeric Computations + +| Method | Returns | Description | +| ------------- | --------- | ------------------------------------- | +| `q.dot(p)` | `number` | Dot product | +| `q.mag()` | `number` | Magnitude (norm) | +| `q.sqrMag()` | `number` | Squared magnitude | +| `q.angle(p)` | `number` | Angular difference from `p` (radians) | +| `q.equals(p)` | `boolean` | Approximate equality, tolerance 1e-6 | + +#### Rotation Operations (rotate around local axes, returns new quaternion) + +| Method | Returns | Description | +| ------------------- | ---------------- | ------------------------------------------------------------------- | +| `q.rotateX(rad)` | `GameQuaternion` | Rotate around X axis | +| `q.rotateY(rad)` | `GameQuaternion` | Rotate around Y axis | +| `q.rotateZ(rad)` | `GameQuaternion` | Rotate around Z axis | +| `q.rotateVector(v)` | `GameVector3` | Rotate vector `v` by this quaternion | +| `q.toEuler()` | `GameVector3` | Convert to Euler angles (YZX order), returns `(x, y, z)` in radians | + +#### Axis-Angle Decomposition + +```js +var result = q.getAxisAngle(); +// result.angle — rotation angle (radians) +// result.axis — rotation axis (unit GameVector3) +``` + +### Static Methods + +```js +// Create from axis-angle representation +var q1 = GameQuaternion.fromAxisAngle(axis, rad); +// axis: GameVector3 (auto-normalized) +// rad: rotation angle (radians) + +// Create from Euler angles (YZX rotation order: Y → Z → X) +var q2 = GameQuaternion.fromEuler(x, y, z); +// x, y, z: rotation around each axis in radians + +// Shortest-arc quaternion rotating from vector a to b +var q3 = GameQuaternion.rotationBetween(fromVec, toVec); + +// Create quaternion from look-at direction (from → to) +var q4 = GameQuaternion.lookAt(from, to, up); +// from: GameVector3 — observer position +// to: GameVector3 — target point +// up: GameVector3 — up direction (default (0,1,0)) +``` + +### toString + +```js +q.toString(); // "GameQuaternion(0.707, 0.0, 0.707, 0.0)" +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/player.md similarity index 96% rename from Box3JS-NeoForge-1.21.1/docs/api/player_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/player.md index 98b4ec4..126bbbd 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/player.md @@ -1,6 +1,9 @@ +--- +--- + # player — Player API -The `player` object is obtained via `entity.player` and represents a logged-in player. It includes all `entity` capabilities (like `hp`, `position`, `tags()`, etc.) plus player-specific features: inventory, XP, flight, messaging, teleport, etc. +Obtained via `entity.player`. Includes all `entity` capabilities plus inventory, XP, flight, messaging, teleport, etc. ```js world.onPlayerJoin(function(entity, tick) { @@ -57,6 +60,22 @@ console.log("Player scale: " + player.scale); ## Movement +### player.position + +Readonly reference. Current world position. You can mutate the vector with `player.position.set(x, y, z)`; prefer `player.teleport(pos)` when moving the player. + +### player.velocity + +Readonly reference. Current velocity vector; mutate with `.set()`. + +### player.bounds + +Readonly. Player bounding-box half extents. + +### player.onGround + +Readonly. Whether the player is currently standing on a block. + ### player.walkSpeed diff --git a/Box3JS-NeoForge-1.21.1/docs/api/registries_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/registries.md similarity index 92% rename from Box3JS-NeoForge-1.21.1/docs/api/registries_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/registries.md index b1511a7..080e1b5 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/registries_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/registries.md @@ -1,16 +1,17 @@ +--- +--- + # Custom Registries (registries API) -> **Server-side only.** `registries` is `undefined` on the client. For client code, use the ResourceLocation string directly (e.g. `audio.playSound("colorzone:victory_fanfare", 1.0, 1.0)`). -> -> **Only available in compiled JAR mode (`/box3script compile`).** In interpreted mode (`/box3script start`), `registries` is `undefined`. -> -> **The compiled JAR must be installed on both server and client** for block textures/models to render. Without it on the client, blocks appear as purple/black missing textures. +::: warning +Available only in compiled JAR mode (`/box3script compile`) on the **server**. `registries` is `undefined` on client and in interpreted mode. JAR must be installed on both sides to render. +::: Blocks, items, and sound events are declared in JSON config files. At compile time, `DeferredRegister` code is generated and injected into the `@Mod` class. Assets are bundled from the project's `assets/` directory into the JAR. ## Project Layout -``` +```text mygame/ ├── registries/ │ ├── blocks.json ← block definitions @@ -137,7 +138,9 @@ Item textures follow the same pattern as block textures: `assets/textures/item/< } ``` -> **Note:** Creative tab icon lookup searches items first, then blocks. If `creativeTabs.json`'s `icon` matches an item key, that item will be used as the icon. +::: tip +Creative tab icon lookup searches items first, then blocks. If `creativeTabs.json`'s `icon` matches an item key, that item will be used as the icon. +::: ### Equipment Types (Tools & Armor) @@ -198,7 +201,9 @@ Equipment examples: } ``` -> **Note:** Equipment items always have `maxStackSize` fixed to 1 (unstackable). `nutrition`/`saturation`/`alwaysEdible` only apply to `"food"` type. +::: tip +Equipment items always have `maxStackSize` fixed to 1 (unstackable). `nutrition`/`saturation`/`alwaysEdible` only apply to `"food"` type. +::: ## sounds.json @@ -234,7 +239,7 @@ The compiler auto-generates `assets//sounds.json` in the standard Minecra Corresponding files: -``` +```js assets/sounds/victory_fanfare.ogg assets/sounds/skill_cast.ogg assets/sounds/background_music.ogg @@ -266,7 +271,7 @@ audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); Follows the standard Minecraft resource pack structure: -``` +```text assets// ├── blockstates/.json ← auto-generated unless you provide custom ├── models/block/.json ← auto-generated; can override @@ -281,7 +286,9 @@ assets// └── item/.png ``` -> **Note:** `` is taken from the `name` field in `package.json` (text after the last `/`, e.g. `@scope/mygame` → `mygame`). +::: tip +`` is taken from the `name` field in `package.json` (text after the last `/`, e.g. `@scope/mygame` → `mygame`). +::: At compile time, `assets/` is automatically bundled as `assets//` in the JAR. @@ -289,7 +296,7 @@ At compile time, `assets/` is automatically bundled as `assets//` in the Language files must be **created manually** in `assets/lang/`. They are not auto-generated. At minimum, provide `en_us.json` and `zh_cn.json`. You can also add more languages: -``` +```text mygame/ └── assets/ └── lang/ diff --git a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/server.md similarity index 86% rename from Box3JS-NeoForge-1.21.1/docs/api/server_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/server.md index 8345f37..c0358a7 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/server_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/server.md @@ -1,8 +1,13 @@ +--- +--- + # server — Server-side API Overview Server scripts run on the Minecraft server thread. The entry file is `src/server/app.ts`, and the compiled output is `dist/server.js`. Server APIs handle world state, entities and players, block read/write, event callbacks, persistence, HTTP, and server-to-client event delivery. -> Client UI, keyboard input, local sounds, and local GUI are not server APIs. See [client_en.md](client_en.md). +::: info +Client UI, keyboard input, local sounds, and local GUI are not server APIs. See [client.md](client.md). +::: ## Server Globals @@ -60,7 +65,7 @@ remoteChannel.sendClientEvent(entity, { | Entity interaction | `world.onInteract(handler)` | | Block interaction | `world.onBlockActivate(handler)` | | Block break/place | `world.onVoxelDestroy(handler)` / `world.onBlockPlace(handler)` | -| Timers | `world.setTimeout(fn, ticks)` / `world.setInterval(fn, ticks)` | +| Timers | `setTimeout(fn, ticks)` / `setInterval(fn, ticks)` | ```ts world.onChat((entity, message) => { @@ -157,14 +162,10 @@ globalConfig.set("season", "spring"); ## Cross-Side Events -Use `remoteChannel` to communicate with client scripts. Before sending to a player, check whether their client supports Box3JS: +Use `remoteChannel` to communicate with client scripts. Packets are optional — vanilla clients silently ignore them, no manual check needed: ```ts world.onPlayerJoin((entity) => { - if (!entity.hasBox3JSClient()) { - return; - } - remoteChannel.sendClientEvent(entity, { type: "welcome", text: "Welcome to the server", @@ -172,6 +173,15 @@ world.onPlayerJoin((entity) => { }); ``` +Broadcast a client event to every player: + +```ts +remoteChannel.broadcastClientEvent({ + type: "serverNotice", + text: "A server event was triggered", +}); +``` + Receive client events: ```ts @@ -210,15 +220,15 @@ Custom content files: | Document | Content | |----------|---------| -| [world_en.md](world_en.md) | World state, events, scoreboard, bossbar, teams, border, particles, sounds | -| [entity_en.md](entity_en.md) | Entity properties, AI, equipment, effects, navigation, tags | -| [player_en.md](player_en.md) | Player inventory, messaging, teleport, flight, game mode, XP | -| [voxels_en.md](voxels_en.md) | Block read/write, region fill, spawners | -| [storage_en.md](storage_en.md) | JSON persistence | -| [database_en.md](database_en.md) | SQLite database | -| [http_en.md](http_en.md) | HTTP requests | -| [registries_en.md](registries_en.md) | Custom registries and compiled JAR mode | -| [math_en.md](math_en.md) | Vectors, bounds, colors, quaternions | +| [world.md](world.md) | World state, events, scoreboard, bossbar, teams, border, particles, sounds | +| [entity.md](entity.md) | Entity properties, AI, equipment, effects, navigation, tags | +| [player.md](player.md) | Player inventory, messaging, teleport, flight, game mode, XP | +| [voxels.md](voxels.md) | Block read/write, region fill, spawners | +| [storage.md](storage.md) | JSON persistence | +| [database.md](database.md) | SQLite database | +| [http.md](http.md) | HTTP requests | +| [registries.md](registries.md) | Custom registries and compiled JAR mode | +| [math.md](math.md) | Vectors, bounds, colors, quaternions | ## Recommended Style @@ -227,4 +237,3 @@ Custom content files: - Keep tokens for long-lived event listeners and call `cancel()` when disabling a game mode or subsystem. - Rate-limit large `voxels.fillVoxel()` calls, mass entity spawning, and synchronous HTTP requests to avoid blocking server ticks. - Give storage namespaces explicit names, such as `storage.getDataStorage("arena/scores")` or `storage.getGroupStorage("global/season")`. - diff --git a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/storage.md similarity index 88% rename from Box3JS-NeoForge-1.21.1/docs/api/storage_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/storage.md index 4e46b1b..e33d556 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/storage_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/storage.md @@ -1,11 +1,20 @@ +--- +--- + # storage — Data Storage API `storage` provides JSON file persistence with in-memory caching for fast reads/writes. -> **Runtime:** Available on both server and client. Server data is saved under `config/box3/storage//`; client data is saved under the local game directory at `box3/client-storage//`. Each project automatically gets an independent namespace. +::: info Runtime +Available on both server and client. Server data is saved under `config/box3/storage//`; client data is saved under the local game directory at `box3/client-storage//`. Each project automatically gets an independent namespace. +::: ## Getting a Storage Instance +### storage.key + +Readonly. The root `storage` object always returns an empty string; read `store.key` for a concrete namespace name. + ### storage.getDataStorage(name) Gets or creates a named storage. Same name returns the same instance. @@ -14,7 +23,9 @@ Gets or creates a named storage. Same name returns the same instance. Gets a **cross-project shared** storage. All projects access the same data via the same `name` (uses `__shared__/` namespace internally). Useful for global leaderboards, shared config, etc. -> Server-side only. Client local storage only provides `getDataStorage(name)`. +::: warning +Server-side only. Client local storage only provides `getDataStorage(name)`. +::: ```js var store = storage.getDataStorage("leaderboard"); @@ -41,7 +52,9 @@ var winner = store.get("lastWinner"); // "Steve" (string) var cfg = store.get("config"); // {difficulty: "hard", ...} (object) ``` -> **Note:** After data is reloaded from disk, complex values are returned as plain JSON objects (for example map-like objects). Avoid relying on original JS prototype methods. +::: warning Note +After data is reloaded from disk, complex values are returned as plain JSON objects (for example map-like objects). Avoid relying on original JS prototype methods. +::: ### store.keys() diff --git a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md index ffe5726..4e55b7a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/voxels_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/voxels.md @@ -1,3 +1,6 @@ +--- +--- + # voxels — Block Operations API `voxels` provides pure block-level read/write operations. No entity logic is involved. diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/en/api/world.md similarity index 96% rename from Box3JS-NeoForge-1.21.1/docs/api/world_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/api/world.md index 979715d..e90b74a 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/api/world.md @@ -1,6 +1,9 @@ +--- +--- + # world — World API -`world` is a global singleton representing the Minecraft server's world state. Controls weather, time, game rules, entity spawning; registers event callbacks; manages scoreboards, bossbars, teams; and fires particles, fireworks, lightning, and other visual effects. +`world` is a global singleton representing the Minecraft server's world state. ## World Properties @@ -369,34 +372,28 @@ world.say("§6[Announcement] §fThe match is about to begin!"); ## Timers -### world.setTimeout(handler, ticks) - -⬆ MC Extension | Execute once after `ticks` delay, returns timer ID. - -### world.setInterval(handler, ticks) - -⬆ MC Extension | Execute repeatedly every `ticks`, returns timer ID. +`setTimeout` and `setInterval` are global functions (not on `world`). Rhino does not provide the browser built-in timers; Box3JS supplies them. They return `GameEventHandlerToken` — call `.cancel()` to stop. -### world.clearTimeout(id) +### setTimeout(handler, ticks) -⬆ MC Extension | Cancel a timeout. +Execute once after `ticks` delay. -### world.clearInterval(id) +### setInterval(handler, ticks) -⬆ MC Extension | Cancel an interval. +Execute repeatedly every `ticks`. ```js -var tid = world.setTimeout(() => { +var token = setTimeout(() => { world.say("Executed after 3 seconds"); }, 60); // 60 ticks = 3 seconds -var iid = world.setInterval(() => { +var interval = setInterval(() => { world.say("Executed every 10 seconds"); }, 200); // 200 ticks = 10 seconds // Cancel -world.clearTimeout(tid); -world.clearInterval(iid); +token.cancel(); +interval.cancel(); ``` ## Scoreboard @@ -469,12 +466,12 @@ Remove a boss bar. ```js // Create a 3-minute countdown boss bar var totalTicks = 3600; -var iid = world.setInterval(() => { +var iid = setInterval(() => { totalTicks -= 20; var remain = totalTicks / 3600; if (remain <= 0) { world.removeBossbar("timer"); - world.clearInterval(iid); + iid.cancel(); } else { world.showBossbar( "timer", @@ -552,7 +549,7 @@ world.borderSize = 500; world.setBorderDamage(2); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.shrinkBorder(100, 120); // shrink to 100 over 2 minutes }, 600); // start after 30 seconds ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/en/guide/README.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/README.md new file mode 100644 index 0000000..0668a17 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/README.md @@ -0,0 +1,32 @@ +--- +--- + +# Box3JS Guides + +## Pick Your Path + +| I want to... | Read this | +|-------------|----------| +| Understand where Box3JS comes from and why | [Box3JS & Box3](about-box3js.md) | +| Write my first script in 10 minutes | [Quick Start](getting-started.md) | +| Use ready-made templates to implement features | [Common Recipes](recipes.md) | +| Understand how Box3JS works internally | [Architecture](architecture.md) | +| Decide between Box3JS and Java modding | [JS vs Java](js-vs-java.md) | +| Troubleshoot problems | [FAQ](faq.md) | + +## Learning Path + +```text +Box3JS & Box3 → Quick Start Architecture JS vs Java + │ │ │ + │ Setup │ Rhino engine │ Pros & cons + │ First script │ Scope isolation │ Use case decision tree + │ Dev cycle │ Event callbacks │ Hybrid approach + │ Debug & deploy │ Build pipeline │ + │ │ Network comms │ + │ │ Sandbox + hot-reload │ + └──────────┬───────────────┴───────────────────────┘ + │ + ▼ + API Reference + Tutorials +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md new file mode 100644 index 0000000..b4fcceb --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/about-box3js.md @@ -0,0 +1,138 @@ +--- +--- + +# Box3JS and Box3 (Shenqi Code Island) + +## What is Box3 (神奇代码岛) + +[Box3](https://box3.fun) (also known as 神奇代码岛 or "Code Island 3.0") is a **multiplayer 3D game creation platform** developed by Shenzhen Qimengdao Technology Co., Ltd. (深圳奇梦岛科技有限公司), a brand under Codemao (编程猫). Users create racing games, PvP arenas, RPGs, FPS shooters, and even MOBAs — all in a browser, using nothing but JavaScript. + +Key features: + +- **JavaScript programming** — Write game logic in JS/TypeScript with zero game engine experience +- **Real-time multiplayer** — Platform provides servers, rooms, and network sync out of the box +- **Cross-platform** — Works on PC, mobile, and tablet with no downloads +- **Community UGC** — All games are created by users; the platform itself ships no preset content + +Box3 has given thousands of young creators their first experience of "making a game." Its API design has been battle-tested by a large community over years, resulting in a clean, intuitive, and efficient programming model. + +## What is Box3JS + +Box3JS is a **community-driven Minecraft mod** (NeoForge 1.21.1) that embeds a JavaScript engine (Mozilla Rhino) inside the Minecraft server, enabling developers to write server-side gameplay logic in TypeScript, with optional client-side scripts for key listeners, screen UI, and local audio. + +**Box3JS is not an official product of Box3.** It was created by community developers familiar with the Box3 ecosystem, continuing Box3's API design philosophy and naming conventions. + +## Why Box3JS for Minecraft Exists + +### The Idea + +Box3's API design is genuinely excellent — refined through years of community use: + +- Global object injection — no `import`/`require` needed +- Tick-based timers — perfectly synced with the game world +- Universal cancellation pattern (`GameEventHandlerToken`) +- Per-project scope isolation — multiple scripts never interfere + +But Box3 runs on its own closed platform. Creators can't reach the Minecraft ecosystem — which has a larger player community, richer block mechanics, and a mature mod distribution system. + +**Box3JS's core mission: bring the Box3-level developer experience into Minecraft.** + +### Who Box3JS is For + +| User | Why | +| -------------------------------------------- | ----------------------------------------------------------------------- | +| Box3 platform developers | Same API style — reuse existing skills with zero learning curve | +| MC server owners wanting custom gameplay | No Java, Gradle, or Mixin needed — just write JS | +| Programming education | TypeScript + hot reload + sandbox rollback = ideal teaching environment | +| Developers who don't want to write Java mods | Ready-to-use APIs cover common needs with no build pipeline burden | + +## Unique Advantages of Box3JS + +### 1. Box3-Compatible API Design + +If you've written scripts for Box3, Box3JS code looks immediately familiar: + +```js +// This code looks almost identical on Box3 and Box3JS +world.onPlayerJoin((entity) => { + entity.player.directMessage(`§aWelcome ${entity.player.name}!`); +}); + +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage("Hello!"); + return false; + } + return true; +}); +``` + +For a detailed API comparison, see [Box3 API vs Box3JS](../BOX3_API_COMPARISON.md). + +### 2. Real Minecraft World + +Box3JS directly operates the Minecraft world — real blocks, vanilla entities, complete game mechanics. You can: + +- Manipulate real MC blocks (`voxels.setVoxel`, `voxels.fillVoxel`) +- Spawn vanilla entities with AI, equipment, and potion effects +- Use Minecraft's scoreboards, BossBars, and team system +- Execute vanilla commands (`player.runCommand`) +- Control weather, time, world borders, and game rules + +### 3. Hot Reload + Sandbox + +- **Save-and-reload** — Edit → `npm run build` → `/box3script reload` — no server restart +- **Sandbox protection** — Enable sandbox to track all world changes; disable to roll back everything +- **Auto hot-reload** — Enable `watch` to auto-reload on build output changes + +### 4. Standalone Distribution + +When development is done, compile into a standalone JAR with one command: + +```js +/box3script compile mygame +``` + +Generates `mygame-1.0.0.jar` — drop it into any NeoForge 1.21.1 server's `mods/` directory. No source code needed, no build tools required. Ready for CurseForge and Modrinth distribution. + +### 5. Dual-Side Architecture + +```text +Server (authoritative) Client (presentation) +world.* / voxels.* client.* / input.* +entity.* / player.* ←→ ui.* / audio.* / gui.* +storage / db / http storage / db / http + └── remoteChannel ──────┘ +``` + +Server handles authoritative game logic; client handles local presentation (UI, audio, input). Bidirectional communication via `remoteChannel`. + +### 6. Full TypeScript Experience + +- Complete `.d.ts` type declarations for VS Code IntelliSense +- esbuild + Babel build pipeline with modern syntax support (`const`, arrow functions, template literals, `async/await`) +- ESLint code checking +- Mutually exclusive `tsconfig.server.json` / `tsconfig.client.json` to prevent API misuse + +## Differences from Box3 Platform + +Box3JS is not a 1:1 copy of Box3's API. Differences stem from the fundamental differences between the two platforms: + +| Area | Box3 Platform | Box3JS (MC) | +| ------------------- | --------------------------------------------------- | --------------------------------------------------------------- | +| Rendering | Custom 3D engine | Minecraft vanilla renderer | +| Physics | Custom physics engine | Minecraft vanilla physics | +| Weather | Independent rain/snow/fog (rich parameter control) | MC vanilla weather (rainDensity/thunderDensity) | +| Lighting | Manual/natural light modes (lightMode/sunFrequency) | MC vanilla lighting | +| Custom models | Built-in editor | Requires Resource Pack (MC mechanism) | +| Custom blocks/items | Runtime registration | Requires JAR compilation (`registries` + `/box3script compile`) | +| Database | Built-in KV storage | JSON storage + SQLite (requires sqlite-jdbc mod) | +| Networking | Platform-managed | `remoteChannel` custom payloads | + +**Design principle:** Keep API naming and semantics consistent where possible, but don't force-fit MC-incompatible features. For a detailed comparison, see [Box3 API vs Box3JS](../BOX3_API_COMPARISON.md). + +## Next Steps + +- **Get started**: [Quick Start Guide](getting-started.md) — write your first MC script in 10 minutes +- **How it works**: [Architecture](architecture.md) — Rhino engine, scope isolation, build pipeline +- **API reference**: [API by Task](../api/README.md) — find APIs by "I want to..." diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/architecture.md similarity index 90% rename from Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/architecture.md index a9734bf..360bf23 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/architecture_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/architecture.md @@ -1,25 +1,11 @@ -# Box3JS Architecture - -A deep dive into Box3JS's internals: how the JS engine is embedded in Minecraft, how scopes are managed, how the build pipeline works, and how network communication is implemented. - -## Table of Contents - -1. [Overall Architecture](#overall-architecture) -2. [Rhino Engine](#rhino-engine) -3. [Scopes & Isolation](#scopes--isolation) -4. [Global Object Injection](#global-object-injection) -5. [Event Callback Mechanism](#event-callback-mechanism) -6. [Build Pipeline](#build-pipeline) -7. [Network Communication](#network-communication) -8. [Sandbox System](#sandbox-system) -9. [File Watching & Hot Reload](#file-watching--hot-reload) -10. [Compiled Release Mode](#compiled-release-mode) - +--- --- +# Box3JS Architecture + ## Overall Architecture -``` +```text ┌──────────────────────────┐ │ Minecraft Server │ │ (NeoForge) │ @@ -57,7 +43,7 @@ A deep dive into Box3JS's internals: how the JS engine is embedded in Minecraft, ### Key Package Structure -``` +```text com.box3lab.box3js ├── Box3JS.java ← @Mod entry point ├── script/ ← Server engine @@ -80,19 +66,18 @@ com.box3lab.box3js └── ... ``` ---- - ## Rhino Engine ### Why Rhino -| Engine | Type | Speed | JVM Integration | ES Version | -|--------|------|-------|----------------|------------| -| Mozilla Rhino | Interpreted | Medium | Native (Java impl) | ES5 | -| GraalJS | JIT | Fast | Requires config | ES2023 | -| Nashorn | JIT | Fast | JDK built-in (removed) | ES6 | +| Engine | Type | Speed | JVM Integration | ES Version | +| ------------- | ----------- | ------ | ---------------------- | ---------- | +| Mozilla Rhino | Interpreted | Medium | Native (Java impl) | ES5 | +| GraalJS | JIT | Fast | Requires config | ES2023 | +| Nashorn | JIT | Fast | JDK built-in (removed) | ES6 | Reasons for choosing Rhino: + - **Pure Java implementation**, zero-config JVM embedding, no startup overhead - **Mature and stable**, widely validated in the Minecraft modding community - **Compatible with NeoForge classloader**, no special configuration needed @@ -114,32 +99,30 @@ scope.put("storage", scope, storageApi); // 2. Initialize console JS bridge cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); -// 3. Load user script -cx.evaluateReader(scope, scriptReader, "app.js", 1, null); +// 3. Load server entry script +cx.evaluateReader(scope, scriptReader, "server.js", 1, null); ``` ### Type Bridging When Java objects are exposed to JS, Rhino automatically handles type conversion: -| Java Type | JS Type | -|-----------|---------| -| `String` | `string` | -| `int` / `double` | `number` | -| `boolean` | `boolean` | -| `Map` | `object` | -| `List` | `array` | +| Java Type | JS Type | +| ------------------------- | --------- | +| `String` | `string` | +| `int` / `double` | `number` | +| `boolean` | `boolean` | +| `Map` | `object` | +| `List` | `array` | | Java object (method call) | JS object | Most Box3JS return values are **native Java objects** (e.g., `ServerPlayer` wrappers). Complex returns (e.g., `querySelectorAll`) return Java `List`, which Rhino maps to JS arrays. ---- - ## Scopes & Isolation ### Per-Project Independent Scopes -``` +```text Rhino Context │ ┌──────────────┼──────────────┐ @@ -153,6 +136,7 @@ Most Box3JS return values are **native Java objects** (e.g., `ServerPlayer` wrap ``` Each project has: + - **Independent top-level scope** — variables don't cross-contaminate - **Independent event callback lists** — stored by `Box3JSEventBus` keyed by project name - **Independent storage namespace** — `storage.getDataStorage("coins")` reads per-project data @@ -161,18 +145,17 @@ Each project has: ### Cleanup Mechanism When stopping a project: + 1. `Box3JSEventBus` clears all event callbacks for that project 2. Scoreboards/BossBars/teams created by the project are removed 3. If sandbox was enabled, all block and entity changes are rolled back 4. Rhino scope is released, GC collects ---- - ## Global Object Injection ### Server-Side Injection -``` +```text Box3ScriptEngine.setupScope(scope) │ ├── scope.put("world", scope, new Box3JSWorld(...)) @@ -194,7 +177,7 @@ Box3ScriptEngine.setupScope(scope) ### Client-Side Injection -``` +```text Box3JSClientEngine.init(scope) │ ├── scope.put("audio", scope, audioObj) @@ -218,23 +201,29 @@ The Java `Box3JSConsole` method signature is `log(Object... args)` (varargs). Rh ```js // CONSOLE_INIT_JS — injected into every scope console = { - log: function() { return _jConsole.log.apply(_jConsole, arguments); }, - debug: function() { return _jConsole.debug.apply(_jConsole, arguments); }, - warn: function() { return _jConsole.warn.apply(_jConsole, arguments); }, - error: function() { return _jConsole.error.apply(_jConsole, arguments); }, + log: function () { + return _jConsole.log.apply(_jConsole, arguments); + }, + debug: function () { + return _jConsole.debug.apply(_jConsole, arguments); + }, + warn: function () { + return _jConsole.warn.apply(_jConsole, arguments); + }, + error: function () { + return _jConsole.error.apply(_jConsole, arguments); + }, // ... }; ``` `.apply()` ensures multiple arguments are correctly forwarded to the Java varargs method. ---- - ## Event Callback Mechanism ### Complete Chain -``` +```text Minecraft event fires │ ▼ @@ -293,6 +282,7 @@ token.active(); // check if still active ``` Java side: + ```java public class GameEventHandlerToken { private boolean active = true; @@ -301,11 +291,9 @@ public class GameEventHandlerToken { } ``` ---- - ## Build Pipeline -``` +```text src/server/app.ts (TypeScript + ES2020 syntax) │ ▼ @@ -349,7 +337,7 @@ import babel from "@babel/core"; // 1. Babel: TS → ES5 JS const es5Code = babel.transformSync(tsCode, { presets: ["@babel/preset-typescript"], - targets: { rhino: "1.9.1" } + targets: { rhino: "1.9.1" }, }); // 2. esbuild: bundle @@ -358,17 +346,15 @@ await build({ bundle: true, outfile: "dist/server.js", target: "es5", - format: "iife" + format: "iife", }); ``` ---- - ## Network Communication ### remoteChannel Architecture -``` +```text ┌──────────────────────┐ ┌──────────────────────┐ │ Server (Java) │ │ Client (Java) │ │ │ │ │ @@ -392,7 +378,8 @@ await build({ ### Data Flow **Server → Client:** -``` + +```text JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) → Box3JSRemoteChannel.java → JSON.stringify(eventData) @@ -405,7 +392,8 @@ JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) ``` **Client → Server:** -``` + +```text JS: remoteChannel.sendServerEvent({ key: "space" }) → Box3JSClientEngine → JSON.stringify @@ -418,18 +406,17 @@ JS: remoteChannel.sendServerEvent({ key: "space" }) ### Data Format All data crossing the network must be **JSON-serializable**: + - `string`, `number`, `boolean`, `null` - Plain objects `{ key: value }` - Arrays `[1, 2, 3]` - NOT supported: functions, `GameVector3` instances, Java objects ---- - ## Sandbox System ### How It Works -``` +```text /box3script sandbox mygame ← enable sandbox │ ▼ @@ -468,13 +455,11 @@ class SandboxTracker { - **Player testing** — let players try new features, rollback after without affecting the live server - **Debugging** — test destructive operations (explode, fillVoxel) ---- - ## File Watching & Hot Reload ### Workflow -``` +```text /box3script watch ← enable file watching │ ▼ @@ -499,13 +484,11 @@ New code takes effect (no manual reload needed) - 300ms debounce prevents multiple reloads during esbuild multi-chunk writes - Reload is atomic: stops old script (cleans up callbacks + resources) before loading new one ---- - ## Compiled Release Mode ### `/box3script compile` Flow -``` +```text Input: config/box3/script/mygame/ │ ▼ @@ -566,18 +549,16 @@ public static final DeferredBlock RUBY_BLOCK = **Note:** `registries` is only available in compiled JAR mode. In interpreted mode (`/box3script start`), `registries` is `undefined`. ---- - ## Performance Considerations ### Overhead Sources -| Layer | Overhead | Notes | -|-------|---------|-------| -| NeoForge event dispatch | Low | Same as vanilla Minecraft | -| Box3JS event forwarding | Medium | Java → JS argument boxing | -| Rhino execution | **Medium-High** | Interpreted, no JIT | -| JS code itself | Depends on code | Loops in `onTick` are most sensitive | +| Layer | Overhead | Notes | +| ----------------------- | --------------- | ------------------------------------ | +| NeoForge event dispatch | Low | Same as vanilla Minecraft | +| Box3JS event forwarding | Medium | Java → JS argument boxing | +| Rhino execution | **Medium-High** | Interpreted, no JIT | +| JS code itself | Depends on code | Loops in `onTick` are most sensitive | ### Performance Tips diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/faq.md similarity index 91% rename from Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/faq.md index 15a88bb..7db7df3 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/faq_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/faq.md @@ -1,12 +1,14 @@ -# FAQ & Troubleshooting +--- +--- -Common issues encountered during Box3JS development and how to resolve them. +# FAQ & Troubleshooting ## Script Loading ### Q: My script doesn't run. `/box3script` shows the project as ○ (not loaded) Checklist: + 1. Did `npm run build` succeed? Is `dist/server.js` present? 2. Has `/box3script start ` been run? 3. Check the server console for errors prefixed with `[Box3JS]` @@ -44,6 +46,7 @@ Run `npm install` once after creating or cloning a project. After that, only `np ### Q: TypeScript reports type errors but the script runs fine TypeScript only checks types at build time. At runtime, Rhino doesn't enforce types. To fix: + 1. Check the `.d.ts` signatures in `types/server/` and `types/client/` for correctness 2. If the type is genuinely wrong, use `// @ts-expect-error` as a temporary bypass 3. Consider fixing the `.d.ts` file for proper type coverage @@ -51,6 +54,7 @@ TypeScript only checks types at build time. At runtime, Rhino doesn't enforce ty ### Q: `npm run build` succeeds but the script throws syntax errors at runtime Babel compiles to ES5 targeting Rhino 1.9.1 (ES5 only). Common pitfalls: + - Don't use `async/await` in `src/` (Babel doesn't fully compile these to ES5) - Don't use `Promise` (Rhino 1.9.1 doesn't support it) - `let`/`const`, `=>` arrow functions, and template literals are handled by Babel — safe to use @@ -73,7 +77,8 @@ Babel compiles to ES5 targeting Rhino 1.9.1 (ES5 only). Common pitfalls: ### Q: API says "xxx is not a function" Check: -1. Is the method name spelled correctly? See [API reference](../api/README_en.md) + +1. Is the method name spelled correctly? See [API reference](../api/README.md) 2. Is it on the right global object? e.g. `world.say()` not `server.say()` 3. Does it need `new`? e.g. `new GameVector3(x, y, z)` 4. Are you calling a client API from a server script? (`audio`/`input`/`ui`/`chat` only work in `src/client/`) @@ -81,6 +86,7 @@ Check: ### Q: Script runs slowly / server lags Rhino is an interpreted engine (no JIT). Optimization tips: + - **Avoid heavy work in onTick** — Use `setInterval` to reduce frequency - **Cache query results** — Don't call `querySelectorAll` every tick - **Minimize JS ↔ Java crossings** — Batch operations are faster than individual calls @@ -106,6 +112,7 @@ Install the `minecraft-sqlite-jdbc` mod. If you don't use `db`, you don't need i ### Q: How do I prevent SQL injection? Use parameterized queries (recommended): + ```js // ✅ Safe: parameterized db.sql("SELECT * FROM t WHERE name = ?", userInput); @@ -143,19 +150,12 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: How to detect if a player has Box3JS client mod installed? -```js -// Server-side check -if (entity.player.hasBox3JSClientMod()) { - // Can send client events -} else { - // Fallback to chat messages - entity.player.directMessage("Install Box3JS client for full experience"); -} -``` +No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional payloads — players without the Box3JS client mod will silently ignore these packets without errors or disconnects. You can safely send events to all players. ### Q: Can client and server use `remoteChannel` at the same time? Yes. `remoteChannel` provides bidirectional channels: + - Client → Server: `remoteChannel.sendServerEvent()` → `remoteChannel.onServerEvent()` - Server → Client (targeted): `remoteChannel.sendClientEvent(entity, ...)` → `remoteChannel.onClientEvent()` - Server → Client (broadcast): `remoteChannel.broadcastClientEvent(...)` → `remoteChannel.onClientEvent()` @@ -170,7 +170,7 @@ Cannot send: functions, `GameVector3` instances, Java objects. To send coordinat ### Q: How do I distribute my script? -``` +```js /box3script compile ``` @@ -178,17 +178,15 @@ Outputs `-.jar`. Drop it into `mods/`. Recipients also need Bo ### Q: What's the difference between compiled JAR and interpreted mode? -| Feature | Interpreted Mode | Compiled JAR | -|---------|-----------------|--------------| -| registries | Not available | Available (custom blocks/items/sounds) | -| Hot reload | ✅ | ❌ (requires restart) | -| Distribution | Copy entire project directory | Single JAR file | -| Updates | Edit JS files directly | Recompile | +| Feature | Interpreted Mode | Compiled JAR | +| ------------ | ----------------------------- | -------------------------------------- | +| registries | Not available | Available (custom blocks/items/sounds) | +| Hot reload | ✅ | ❌ (requires restart) | +| Distribution | Copy entire project directory | Single JAR file | +| Updates | Edit JS files directly | Recompile | ### Q: Why is registries only available in compiled mode? Custom blocks/items/sounds require NeoForge's `DeferredRegister`, which must be registered during mod startup. Interpreted mode has no startup registration phase, so `registries` only works when compiled as a JAR. ---- - For more questions, ask on [GitHub Issues](https://github.com/box3lab/Box3JS). diff --git a/Box3JS-NeoForge-1.21.1/docs/en/guide/getting-started.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/getting-started.md new file mode 100644 index 0000000..56672a4 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/getting-started.md @@ -0,0 +1,968 @@ +--- +--- + +# Quick Start: From Zero to Your First Box3JS Script + +For readers with **zero modding experience**. Just need to know JavaScript. + +## What is Box3JS + +Box3JS is a **Minecraft mod** that embeds a JavaScript runtime inside the server, letting you write gameplay in JS/TypeScript. Client-side scripts are optionally delivered for key listeners, screen UI, and local audio. + +Box3JS inherits its API design from **[Box3](https://box3.fun) (神奇代码岛)**, a browser-based multiplayer 3D game creation platform where thousands of creators build games with JavaScript. Box3's API has been battle-tested by its community — clean, intuitive, and efficient. Box3JS brings that same API paradigm to Minecraft, so you can build mini-games and custom gameplay the Box3 way. + +::: tip More background +→ [Box3JS & Box3](about-box3js.md) +::: + +### Architecture at a Glance + +```text +You write Build tools Minecraft runs + TypeScript ───→ compile to ES5 ───→ Rhino engine executes + │ + ┌──────┴──────┐ + │ NeoForge │ + │ Minecraft │ + │ API layer │ + └─────────────┘ +``` + +- **You write TypeScript** with full type hints and modern syntax +- **Babel compiles** down to ES5 (Rhino only supports ES5) +- **esbuild bundles** into a single JS file +- **Rhino runs inside the JVM**, directly calling Minecraft APIs +- **Dual-side execution** (server + client), communicating via `remoteChannel` + +### What You Can Do + +| Category | Examples | +| ------------------ | ------------------------------------------------------- | +| Chat Commands | `!heal`, `!home`, `!shop` | +| Event Response | Welcome on join, death penalty, block break logging | +| Entity Control | Spawn mobs, set AI, custom bosses | +| Mini-Games | PvP arena, parkour, wave survival | +| World Manipulation | Place/replace blocks, fill regions, change weather/time | +| Data Persistence | JSON storage, SQLite database | +| Game Systems | Scoreboards, BossBars, teams, world borders | +| HTTP Requests | Web API calls, webhook notifications | +| Client Scripts | Key listeners, screen UI, client audio, custom GUIs | + +### What You Can't Do + +- **Render custom models/particles** — requires a client resource pack or Java mod +- **Add new blocks/items at runtime** — requires compiling to a JAR (`/box3script compile`) +- **Modify vanilla mechanics** — changing recipes or mob AI behavior requires Mixin +- **Use modern JS syntax at runtime** — Rhino supports ES5 only, but you can use modern TypeScript in source (the build step handles transpilation) + +## Setup + +### What You Need + +1. **Minecraft server** with Box3JS + NeoForge 1.21.1 installed +2. **Node.js** 18+ (for local builds only — not needed on the server) +3. A text editor (VS Code recommended, with full TypeScript IntelliSense) + +### Verify Installation + +In-game, run: + +```js +/box3script +``` + +If you see the project status panel, Box3JS is running. + +```text +══ Box3JS Script Engine ══ + Watch: ○ Inactive Sandbox: ○ Inactive + Projects: 0 enabled | 0 loaded +``` + +## Create a Project + +### One-Command Creation + +In-game: + +```js +/box3script create mygame +``` + +This generates a complete TypeScript project at `config/box3/script/mygame/`. + +### Understanding Project Structure + +```text +config/box3/script/mygame/ +├── package.json ← Project config (name, version, build deps) +├── tsconfig.base.json ← Shared TS compiler options +├── tsconfig.server.json ← Server TS config (references server/ types) +├── tsconfig.client.json ← Client TS config (references client/ types) +├── build.mjs ← Build script (esbuild + Babel) +├── eslint.config.mjs ← ESLint rules +├── types/ ← ★ Type declarations (API reference) +│ ├── shared.d.ts ← Shared server & client types +│ ├── server/ +│ │ ├── index.d.ts ← Server type entry point +│ │ ├── server.d.ts ← world, remoteChannel, registries +│ │ ├── entity.d.ts ← GameEntity interface +│ │ ├── player.d.ts ← GamePlayer interface +│ │ ├── world.d.ts ← GameWorld interface +│ │ └── voxels.d.ts ← GameVoxels interface +│ └── client/ +│ ├── index.d.ts ← Client type entry point +│ ├── client.d.ts ← GameClient, RemoteChannel +│ ├── audio.d.ts ← GameAudio +│ ├── input.d.ts ← GameInput +│ ├── ui.d.ts ← GameUI +│ ├── chat.d.ts ← GameChat +│ └── gui.d.ts ← GameGUI, GuiController +├── src/ +│ ├── server/ +│ │ └── app.ts ← ★ Server entry point (where you write code) +│ └── client/ +│ └── app.ts ← Client entry point +├── registries/ ← Custom content (blocks/items/sounds JSON) +└── assets/lang/ ← Custom content localization +``` + +**Key insights:** + +- The `.d.ts` files in `types/` are your API reference — VS Code uses them for IntelliSense +- `tsconfig.server.json` and `tsconfig.client.json` are **mutually exclusive** — server code never sees client globals like `client`, `input`, etc. +- You only need to write code in `src/server/app.ts` (and optionally `src/client/app.ts`); the build tooling handles everything else + +### Install Dependencies + +```bash +cd config/box3/script/mygame +npm install +``` + +Run `npm install` once (installs esbuild, Babel, TypeScript build tooling). + +## Your First Script: Line by Line + +Open `src/server/app.ts`, clear the contents, and write the code below. Let's understand each part. + +### 1. Startup Log + +```js +console.log("MyGame script started!"); +``` + +**What happens:** `console` is a global object injected by Box3JS (no `import` needed). Behind it is a Java class `Box3JSConsole`, bridged to JS via Rhino. `console` supports six methods: `log`, `warn`, `error`, `debug`, `clear`, `assert`. + +### 2. Welcome Players on Join + +```js +world.onPlayerJoin((entity) => { + const p = entity.player; + + // Broadcast to all + world.say(`§e${p.name} §7joined the server`); + + // Private message + p.directMessage(`§aWelcome to the server, ${p.name}!`); + + // Particle welcome effect + const { position: pos } = p; + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); + world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); +}); +``` + +**Line by line:** + +- `world.onPlayerJoin(...)` — Registers a "player join" event listener. Returns a `GameEventHandlerToken` (not saved in this example, so the listener stays active until script reload). +- `entity.player` — `entity` is the callback parameter, representing the joining entity. `.player` gets the player wrapper (if the entity isn't a player, `.player` is `undefined`). +- `p.directMessage(...)` — Sends a private message visible only to that player. +- `§a` is a Minecraft color code (green). `§e` = yellow, `§6` = gold, `§7` = gray, `§c` = red. +- `p.position` — Returns a `GameVector3` object with `.x`, `.y`, `.z` properties. +- `world.spawnParticleCircle(...)` — Spawns a ring of particles at the player's position. +- `world.playSound(...)` — Plays a sound at the player's position. + +### 3. Chat Commands + +```js +world.onChat((entity, message) => { + const p = entity.player; + + if (message === "!hello") { + p.directMessage(`§eHello, ${p.name}!`); + return false; // suppress chat message from broadcasting + } + + if (message === "!pos") { + const { position: pos2 } = p; + p.directMessage( + `§eYour position: §f${Math.floor(pos2.x)}, ${Math.floor(pos2.y)}, ${Math.floor(pos2.z)}`, + ); + return false; + } + + if (message === "!help") { + p.directMessage("§eAvailable commands: !help, !hello, !pos, !home, !shop"); + return false; + } + + return true; // normal messages pass through +}); +``` + +**Key concepts:** + +- `return false` — Blocks the event from continuing (message won't appear in chat). This is the universal Box3JS event convention: **return false to cancel the default behavior**. +- `return true` — Let it through; message broadcasts normally. +- `Math.floor()` — Regular JS, rounds coordinates down for readability. + +### 4. Periodic Announcement + +```js +setInterval(() => { + const count = world.querySelectorAll("*").length; + world.say(`§7[Info] §fPlayers online: ${String(count)}`); +}, 6000); // 6000 ticks = 5 minutes +``` + +**Key understanding:** + +- `setInterval` is a **global function** (not `world.setInterval`), just like in browsers and Node.js. +- The second argument is **ticks** (Minecraft time unit), not milliseconds. 1 second = 20 ticks. +- `setInterval` returns a `GameEventHandlerToken` — call `.cancel()` to stop it. +- `world.querySelectorAll("*")` returns all online entities. +- `world.say(...)` broadcasts to the entire server. + +### Tick Conversion Table + +| Duration | Ticks | +| ---------- | ------ | +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1,200 | +| 5 minutes | 6,000 | +| 10 minutes | 12,000 | +| 30 minutes | 36,000 | + +## Core Design Philosophy: Why the API Works This Way + +Understanding the design rationale behind Box3JS APIs helps you write more efficient and safer scripts. Here are the most important design decisions and their reasons. + +### Design 1: Global Object Injection — No Imports Needed + +```js +// In Box3JS, use directly — no import required +world.onTick(() => { ... }); +console.log("hello"); +storage.getDataStorage("coins"); + +// Compare: if this were Node.js +// const { world } = require("box3js"); ← Not needed! +``` + +**Why?** Rhino is a bare ECMAScript engine — it doesn't support CommonJS `require()` or ES Module `import`. Box3JS injects all API objects as global variables at Rhino scope initialization time, directly from Java. The `.d.ts` TypeScript files declare these globals with `declare`, giving you full IntelliSense in VS Code without any imports. + +### Design 2: Tick-Based Timing — Not Milliseconds + +```js +// Box3JS time unit is ticks (1/20 second) +setTimeout(() => { ... }, 100); // 100 ticks = 5 seconds later + +// Compare browser: +// setTimeout(() => { ... }, 5000); // 5000 milliseconds = 5 seconds +``` + +**Why?** Box3JS timers execute **on the main game tick loop**, not on separate Java threads. Each game tick (1/20 second), the engine checks all timers, decrements their remaining ticks, and fires callbacks whose countdown reaches 0. Benefits: + +- **Thread safety** — Callbacks always run on the main thread; you can safely call any Minecraft API +- **Precise synchronization** — Timers are perfectly synced with the game world; a lagging server slows timers too +- **Zero overhead** — No extra threads or thread pools + +### Design 3: GameEventHandlerToken — Universal Cancellation + +```js +// All onXxx() and setTimeout/setInterval return GameEventHandlerToken +const token = world.onTick(() => { + // runs every tick +}); + +// Cancel the listener (these are equivalent) +token.cancel(); + +// Check if still active +if (token.active()) { + // ... +} +``` + +**Why?** Early designs provided separate cancellation methods per event type (`removeTickListener`, `removeChatListener`...), which meant: + +1. Memorizing different cancellation API names for each event type +2. No unified way to manage them (how do you batch-cancel all listeners on script stop?) + +A unified `GameEventHandlerToken` pattern solves this: + +- **One pattern for everything** — `onTick`, `onPlayerJoin`, `onChat`, `setInterval` all use `.cancel()` +- **Auto-cleanup on reload** — When stopping a project, the engine iterates all tokens and cancels them in bulk +- **Chainable management** — Collect tokens in an array and cancel them all at once + +### Design 4: Per-Project Scope Isolation + +```text +Server runs 3 script projects simultaneously, completely independent: + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Scope A │ │ Scope B │ │ Scope C │ +│ "mygame" │ │ "lobby" │ │ "survival" │ +│ │ │ │ │ │ +│ var x = 1 │ │ var x = 2 │ │ var x = 3 │ +│ own events │ │ own events │ │ own events │ +│ own storage │ │ own storage │ │ own storage │ +│ own timers │ │ own timers │ │ own timers │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +**Why?** A server might run multiple scripts simultaneously (lobby system, mini-games, economy...). Without isolation: + +- Variable name collisions (`var playerCount` defined in two scripts) +- Event callback interference (`/box3script reload lobby` accidentally clearing survival's callbacks) +- Data leakage (lobby's script reading survival's storage) + +Box3JS gives each project an **independent Rhino top-level scope**, backed by separate `Box3JSEventBus` namespaces. Stopping or reloading one project never affects others. + +### Design 5: Dual-Side Architecture + remoteChannel + +```text +┌──────────────────────┐ ┌──────────────────────┐ +│ Server │ │ Client │ +│ │ │ │ +│ world.* │ │ client.* │ +│ voxels.* │ JSON │ audio.* │ +│ entity.* │ ←─────→ │ input.* │ +│ player.* │ events │ ui.* │ +│ storage/db/http │ │ chat.* / gui.* │ +│ remoteChannel ──────┼─────────┼── remoteChannel │ +│ │ │ storage/db/http │ +└──────────────────────┘ └──────────────────────┘ +``` + +**Why the split?** + +- **Security** — The server is the world authority (blocks, entities, data); clients only handle local presentation (UI, audio, input) +- **Performance** — Client scripts run on each player's own machine, consuming zero server resources +- **Flexibility** — Write server-only scripts for most cases, or add client scripts to enhance the experience + +**remoteChannel communication rules:** + +```js +// Server → single client +remoteChannel.sendClientEvent(player, { type: "welcome", msg: "hi" }); + +// Server → all clients +remoteChannel.broadcastClientEvent({ type: "game_start" }); + +// Client → server +remoteChannel.sendServerEvent({ key: "space", pressed: true }); +``` + +**Critical limitation:** Data crossing the network must be **JSON-serializable**. No functions, `GameVector3` instances, or Java objects. To send coordinates, use `{ x: 1, y: 2, z: 3 }` instead of `new GameVector3(1, 2, 3)`. + +### Design 6: TypeScript Source + Babel Compilation to ES5 + +```text +src/server/app.ts Babel esbuild dist/server.js +(TypeScript, ES2020) ───→ ES5 JavaScript ───→ bundle ───→ (single file) +``` + +**Why the build step?** + +- **Rhino 1.9.1 only supports ES5** — `let`, `const`, arrow functions, template literals, and `class` are all ES6+ features Rhino can't parse +- **Babel downlevels** — Transforms modern syntax into `var`, `function`, string concatenation, and other ES5 equivalents +- **esbuild bundles** — You can write multiple `.ts` files, but Rhino has no module system. esbuild merges everything into a single IIFE + +**You can safely use in source:** + +- `const` / `let` (transpiled to `var`) +- Arrow functions `() => {}` (transpiled to `function(){}`) +- Template literals `` `hello ${name}` `` (transpiled to `"hello " + name`) +- `class` (transpiled to `function` + prototype) +- `async/await` (via regenerator transform) + +### Design 7: Event Callbacks Return false to Cancel + +```js +world.onChat((entity, message) => { + if (message.startsWith("!")) { + // This is a command — don't broadcast + return false; + } + return true; // normal messages pass through +}); +``` + +**Why?** Inspired by the browser DOM event `preventDefault` pattern. Minecraft events typically have "default behavior" (e.g., chat messages broadcast to everyone). `return false` tells the engine: "I've handled this event — skip the default behavior." + +### Design 8: Sandbox Mode — Safe Testing + +```js +/box3script sandbox mygame # enable sandbox +# ... test script (spawn entities, modify blocks, explode)... +/box3script sandbox mygame # disable → auto-rollback all changes +``` + +**Why?** Once a script modifies the world, those changes are permanent (blocks replaced, entities spawned). Sandbox mode tracks all world modifications made by the script and auto-rolls them back when disabled. This lets developers fearlessly test destructive operations without damaging the live server. + +## API Quick Tour + +Organized by "what do I want to do?" — the most commonly used APIs. For the complete reference, see the [API docs](../api/README.md). + +### Messages & Chat + +```js +// Broadcast to all +world.say("§6Server will restart in 5 minutes"); + +// Private message (only target player sees it) +player.directMessage("§aYour balance: 100 coins"); + +// Action bar text (above hotbar) +player.actionBar("§ePress F to open menu"); + +// Screen title (large centered text) +player.title("§6BOSS FIGHT", "§cThe Ancient Dragon awakens"); + +// Chat interception (command system) +world.onChat((entity, message) => { + if (message === "!help") { + entity.player.directMessage("§eAvailable commands: !help, !home, !shop"); + return false; + } + return true; +}); +``` + +### Player Properties & Control + +```js +// Get player info +const name = player.name; +const pos = player.position; // GameVector3 { x, y, z } +const hp = player.hp; +const mode = player.gameMode; // "survival", "creative", "adventure", "spectator" + +// Modify player state +player.hp = 20; // full heal +player.maxHp = 40; // increase max health +player.food = 20; // full hunger +player.gameMode = "creative"; // switch to creative +player.canFly = true; // allow flight +player.flying = true; // start flying + +// Teleport +player.teleport(new GameVector3(100, 64, 100)); + +// Kick +player.kick("You have been kicked by an admin"); + +// Run vanilla commands as the player +player.runCommand("effect give @s minecraft:speed 30 1"); +``` + +### Items & Inventory + +```js +// Give items +player.giveItem("minecraft:diamond", 64); +player.giveItem("minecraft:diamond_sword", 1); + +// Give named items (4th param is the lore/description array) +player.giveNamedItem("minecraft:stick", 1, "§6Magic Wand", ["A magical wand"]); + +// Give enchanted items (enchantments as { enchantId: level } record) +player.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:unbreaking": 3, +}); + +// Check held item +const held = player.getHeldItem(); + +// Clear inventory +player.clearInventory(); +``` + +### Event System + +```js +// Every tick (use sparingly! Heavy work in onTick drags down TPS) +const tickToken = world.onTick(() => { + // runs every tick +}); + +// Player events +world.onPlayerJoin((entity) => { + entity.player.directMessage("Welcome!"); +}); + +world.onPlayerLeave((entity, _tick) => { + world.say(`${entity.player.name} left the server`); +}); + +world.onPlayerRespawn((entity, _tick) => { + entity.player.teleport(new GameVector3(0, 100, 0)); + entity.player.directMessage("You respawned!"); +}); + +// Entity events +world.onEntityDeath((entity, _killer, _tick) => { + if (entity.isPlayer()) { + world.say(`${entity.player.name} died`); + } +}); + +world.onEntityDamage((entity, amount, source, _attacker, _tick) => { + if (amount > 10) { + console.log(`High damage: ${String(amount)} from ${source}`); + } +}); + +// Interaction events +world.onInteract((entity, target, _tick) => { + // Player right-clicked an entity + if (target.hasTag("npc")) { + entity.player.directMessage("Hello!"); + } +}); + +world.onBlockActivate((entity, x, y, z, voxel, _tick) => { + // Player right-clicked a block + if (voxel === "minecraft:chest") { + entity.player.directMessage( + `You clicked a chest at ${String(x)}, ${String(y)}, ${String(z)}`, + ); + } +}); + +world.onBlockPlace((entity, x, y, z, voxel, _voxelId, _tick) => { + // Player placed a block + console.log( + `${entity.player.name} placed ${voxel} at ${String(x)}, ${String(y)}, ${String(z)}`, + ); +}); + +world.onVoxelDestroy((entity, _x, _y, _z, voxel, _tick) => { + // Player is about to break a block + if (voxel === "minecraft:diamond_block") { + entity.player.directMessage("§cYou can't break diamond blocks!"); + return false; // cancel the break + } +}); + +// Timers (global functions) +const timer = setTimeout(() => { + world.say("30 seconds have passed!"); +}, 600); // 600 ticks = 30 seconds + +const interval = setInterval(() => { + world.say("Minute announcement"); +}, 1200); // 1200 ticks = 1 minute + +// Cancel timers +timer.cancel(); +interval.cancel(); +``` + +### Entity Manipulation + +```js +// Spawn an entity (returns GameEntity | null) +const zombie = world.spawnEntity( + "minecraft:zombie", + new GameVector3(100, 64, 100), +); +if (zombie) { + // use zombie ... +} + +// Create with full config (nameTag/glowing/equipment set after creation) +const boss = world.createEntity({ + type: "minecraft:zombie", + position: new GameVector3(100, 64, 100), + hp: 200, + maxHp: 200, + tags: ["boss"], +}); +if (boss) { + boss.setNameTag("§cAncient Zombie King"); + boss.glowing = true; + boss.setEquipment("head", "minecraft:diamond_helmet"); + boss.setEquipment("chest", "minecraft:diamond_chestplate"); + + // Control entities + boss.setAI(false); // disable AI (stands still) + boss.invulnerable = true; // invincible + boss.navigateTo(110, 64, 100, 1.5); // navigate to target + + // Potion effects + boss.addEffect("minecraft:strength", 600, 2, false); + boss.addEffect("minecraft:speed", 600, 1, true); + boss.clearEffects(); + + // Equipment + boss.setEquipment("head", "minecraft:iron_helmet"); + boss.setEquipment("mainhand", "minecraft:iron_sword"); + + // Tags (for marking and querying) + boss.addTag("boss"); + boss.addTag("stage_1"); + boss.hasTag("boss"); // → true +} + +// Query entities +const nearby = world.entitiesInRadius(pos, 10); // within 10 blocks +const all = world.querySelectorAll("*"); // all entities +const players = world.querySelectorAll("player"); // all players +const monsters = world.querySelectorAll("monster"); // all monsters +``` + +### Block Operations + +```js +// Read a block +const block = voxels.getVoxel(100, 64, 100); + +// Place a block +voxels.setVoxel(100, 64, 100, "minecraft:stone"); +voxels.setVoxel(100, 65, 100, "minecraft:torch"); + +// Fill a region +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:glass"); + +// Replace blocks (only matching type) +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:air", "minecraft:stone"); +``` + +### Data Persistence + +```js +// JSON storage (per-project namespace) +const store = storage.getDataStorage("coins"); +store.set("player1", 100); +const coins = store.get("player1"); // → 100 +store.delete("player1"); +const keys = store.keys(); // → array of all keys + +// SQLite database +db.sql("CREATE TABLE IF NOT EXISTS players (name TEXT, score INT)"); +db.sql("INSERT INTO players VALUES ('Steve', 100)"); + +const result = db.sql("SELECT * FROM players WHERE score > 50"); +// result.rows[0] → { name: "Steve", score: 100 } +// result.firstRow → { name: "Steve", score: 100 } +// result.rowCount → 1 +// result.columnNames → ["name", "score"] +``` + +### Game Systems + +```js +// Scoreboard +world.addScoreboard("kills"); +world.setScore("Steve", "kills", 42); +world.showScoreboard("sidebar", "kills"); + +// BossBar +world.showBossbar("boss1", "§cAncient Dragon", 0.8, "red"); +world.setBossbar("boss1", "§cAncient Dragon §7[80%]", 0.5); + +// Teams +world.createTeam("red", "red"); +world.createTeam("blue", "blue"); +world.joinTeam(entity, "red"); + +// World border +world.borderSize = 500; // set border size +world.shrinkBorder(100, 1200); // shrink to 100 over 1200 ticks + +// Weather & time +world.time = 6000; // set time (0=dawn, 6000=noon, 12000=dusk, 18000=midnight) +world.rainDensity = 0; // stop rain +world.clearWeather(); // clear skies +world.thunderDensity = 1; // thunderstorm + +// Game rules +world.setGameRule("keepInventory", true); +world.setGameRule("doDaylightCycle", false); +``` + +### Visual Effects + +```js +const pos = new GameVector3(100, 64, 100); + +// Particles +world.spawnParticle("minecraft:flame", pos.x, pos.y, pos.z, 0, 0, 0, 1, 10); +world.spawnParticleCircle(pos.x, pos.y, pos.z, 2, "minecraft:heart", 30); + +// Fireworks +world.launchFirework(pos.x, pos.y, pos.z, "red", "large_ball"); +world.launchFirework(pos.x, pos.y, pos.z, "green", "star"); + +// Lightning & explosion +world.strikeLightning(pos.x, pos.y, pos.z); +world.explode(pos.x, pos.y, pos.z, 4); // power 4 explosion + +// Sounds +world.playSound("minecraft:entity.ender_dragon.growl", pos, 1.0, 1.0); +player.playSound("minecraft:block.note_block.pling", 1.0, 2.0); // only that player hears it +``` + +### HTTP Requests + +```js +// GET request +const resp = http.fetch("https://api.example.com/data"); +if (resp.ok) { + const data: unknown = resp.json(); + console.log(data); +} + +// POST JSON +const resp2 = http.fetch("https://api.example.com/webhook", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "Server restarted" }) +}); + +// With timeout +const resp3 = http.fetch("https://slow-api.com/data", { + timeout: 5000 // 5 second timeout +}); +``` + +### Client Scripting (requires Box3JS client mod) + +```js +// Client entry: src/client/app.ts + +// Every frame +client.onTick(() => { + // client tick callback +}); + +// Keyboard input +if (input.isKeyDown("space")) { + // spacebar is being held down +} + +input.onKeyPress("f", () => { + // F key pressed — notify server + remoteChannel.sendServerEvent({ action: "open_menu" }); +}); + +// Screen UI +ui.showOverlay("§ePress F to open menu"); // above hotbar +ui.showTitle("§6BOSS SPAWNED!", "§cGet ready!"); // centered screen title + +// Client audio +audio.playSound("minecraft:block.note_block.pling", 1.0, 1.0); +audio.playMusic("minecraft:music.game", 0.5, 1.0); +audio.stopAll(); + +// Fog control (client-side rendering) +client.setFogColor(255, 100, 50); // reddish fog appearance +client.setFogStartDistance(10); // fog begins at 10 blocks +client.setFogEndDistance(50); // fully obscured at 50 blocks +client.resetFog(); // restore default + +// Chat +chat.sendMessage("Hello everyone!"); +chat.onMessage((msg, _sender, _isSystem) => { + if (msg.includes("secret")) { + // handle messages containing "secret" + } +}); + +// Receive server events +remoteChannel.onClientEvent((event) => { + if (event.args.type === "boss_spawned") { + audio.playSound("minecraft:entity.ender_dragon.growl", 1.0, 1.0); + } +}); +``` + +## Dev Cycle + +### Standard Flow + +After each code change: + +```js +Edit code → npm run build → /box3script reload mygame → test +``` + +### Build + +```bash +npm run build +``` + +Output: + +```js + dist/server.js 7.1kb +Done in 240ms +``` + +What the build does: + +1. **Babel** compiles TypeScript to ES5 JavaScript (Rhino only supports ES5) +2. **esbuild** bundles all modules into a single file (Rhino lacks `require()`) +3. Outputs to `dist/server.js` and `dist/client.js` + +### Load & Reload + +In-game: + +```js +/box3script start mygame # first launch +/box3script reload mygame # reload after changes (no server restart) +``` + +`reload` is atomic: stops the old script (cleans up all callbacks, timers, scoreboards), then loads the new one. + +### Auto Hot-Reload + +Enable file watching so builds auto-trigger reload: + +```js +/box3script watch +``` + +**Note:** `watch` monitors the `dist/` compiled output (`.js`), not `src/` source. So you need to run `npm run build` first to generate new `dist/` files. Combine with `npm run build -- --watch` for save-and-reload workflow. + +### Multi-Project Management + +```js +/box3script start mygame lobby # start multiple projects +/box3script stop mygame # stop one +/box3script stopall # stop all +/box3script reload mygame # reload one +/box3script # view all project statuses +``` + +## Debugging + +### Troubleshooting Order + +1. **Check console** — server logs show errors with `[Box3JS] [projectName]` prefix +2. **Check status** — `/box3script` to see if the project shows `◉` (loaded) +3. **Check build** — `npm run build` should complete without errors +4. **Add logging** — use `console.log()` at key points to print variable values +5. **Read line numbers** — Java exception stacks include JS filenames and line numbers (line numbers correspond to compiled `dist/server.js`, not `.ts` source) + +### Common Errors + +| Error | Cause | Fix | +| --------------------------- | ------------------------- | -------------------------------------------------------------------------------- | +| `console is not defined` | Engine init failed | Check mod installation | +| `world is not defined` | Scope issue | Ensure code is at global scope, not inside a nested function | +| `Cannot find name 'xxx'` | TypeScript type error | Check spelling, or look up the correct API name in `types/` `.d.ts` | +| `npm run build` fails | JS syntax error | Check ESLint output or terminal error line numbers | +| Script not executing | Project not enabled | Check `/box3script` status | +| Timer never fires | Tick count miscalculation | Remember: 1 sec = 20 ticks, not 1000 | +| Client script not working | Player lacks client mod | Box3JS client mod must be installed | +| remoteChannel not receiving | Data isn't JSON | Ensure you're sending plain objects, not Java objects or `GameVector3` instances | + +### Sandbox Testing + +Sandbox mode enables safe testing: all world modifications are tracked and rolled back when disabled. + +```js +/box3script sandbox mygame # enable sandbox +# ... test script (spawn entities, modify blocks, explode, etc.)... +/box3script sandbox mygame # disable → auto-rollback all changes +``` + +**Use cases:** + +- **First test of a new script** — unsure what it does? Sandbox first +- **Player play-testing** — let players try new features, rollback after without affecting the live server +- **Debugging destructive operations** — test `fillVoxel`, `explode`, etc. + +### Performance Tips + +Box3JS scripts run on the server main thread — unreasonable code affects TPS: + +1. **Avoid large loops in `onTick`** — scan entities on condition triggers, not every tick +2. **Cache query results** — don't put `querySelectorAll` in onTick +3. **Use `setInterval` over `onTick`** — if you don't need 20/sec, use longer intervals (e.g., 100 ticks = 5 seconds) +4. **Minimize JS ↔ Java crossings** — batch operations are faster than individual calls + +A typical parkour script consumes < 0.5ms/tick, with virtually no impact on server TPS. + +## Deployment + +### Dev Mode → Production Release + +When development is done, compile your script into a **standalone JAR mod**: + +```js +/box3script compile mygame +``` + +Generates `mygame-1.0.0.jar` (version from `package.json`). Drop it into any NeoForge server's `mods/` directory. + +**Notes:** + +- Box3JS must also be installed as a dependency (provides the Rhino runtime) +- If you use `registries` (custom blocks/items), clients must also install the JAR +- The JAR contains compiled JS — no source code needed +- The compiled JAR is a standalone NeoForge mod with its own `mods.toml` + +### package.json Config + +```json +{ + "name": "mygame", + "displayName": "My Game", + "version": "1.0.0", + "description": "A custom mini-game", + "author": "YourName", + "license": "MIT", + "homepage": "https://example.com", + "logoFile": "logo.png" +} +``` + +These metadata fields are written into the JAR's `mods.toml` and shown in the game's mod list. + +### Dev Mode vs Compiled Mode + +| | Dev Mode (`/box3script start`) | Compiled Mode (`/box3script compile`) | +| ------------ | ------------------------------ | ------------------------------------- | +| Code changes | Hot reload, no restart | Must recompile | +| `registries` | `undefined` | ✅ Available | +| Distribution | Source code required | JAR only | +| Use case | Development, testing | Release, distribution | + +## Next Steps + +Now you understand Box3JS's core design philosophy and basic API usage. Next: + +- **API details**: [API by Task](../api/README.md) — find APIs by "I want to..." +- **Event system**: [Tutorial 3: Events & Entities](../tutorial/03-events-entities.md) +- **Client APIs**: [Client API docs](../api/client.md) — key listeners, screen UI, client audio +- **Internals**: [Architecture](architecture.md) — Rhino engine, scopes, build pipeline, networking +- **Tech choice**: [JS vs Java](js-vs-java.md) — Box3JS scripting vs native Java modding +- **FAQ**: [Frequently Asked Questions](faq.md) +- **Recipes**: [Code Snippets & Recipes](recipes.md) — copy-paste solutions for common tasks diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/js-vs-java.md similarity index 59% rename from Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/js-vs-java.md index 1a0e269..264d23d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/js-vs-java.md @@ -1,26 +1,25 @@ -# JS Scripting vs Native Java Mod Development +--- +--- -This guide helps you decide: **when to use Box3JS scripting, and when to write a native Java mod.** +# JS Scripting vs Native Java Mod Development ## Overview -| Aspect | Box3JS (JS/TS) | Native Java Mod | -|--------|---------------|-----------------| -| **Barrier to entry** | JavaScript knowledge enough | Requires Java + Gradle + MC modding knowledge | -| **Dev speed** | Edit → build → reload (seconds) | Edit → compile → restart MC (minutes) | -| **Hot reload** | Supported (`/box3script reload`) | Not supported; restart required per change | -| **Publishing** | `/box3script compile` → JAR | `gradlew build` → JAR | -| **Performance** | Medium (Rhino interpreted) | High (JIT-compiled bytecode) | -| **API coverage** | High-level wrappers (100+ methods) | Full Minecraft/NeoForge API | -| **Type safety** | TypeScript declarations | Java static types | -| **Debugging** | console.log + server output | IDE breakpoint debugging | -| **Dependency mgmt** | npm (build-time only) | Gradle/Maven | -| **Client features** | Limited (UI/input/audio/chat) | Full (rendering, models, GUI, net protocol) | -| **Custom blocks/items** | JSON config + compile-time gen | Java classes + registration | -| **Modify vanilla behavior** | No (no Mixin) | Yes (Mixin/ASM/CoreMod) | -| **Team collaboration** | JS source + Git | Java source + Git + Gradle | - ---- +| Aspect | Box3JS (JS/TS) | Native Java Mod | +| --------------------------- | ---------------------------------- | --------------------------------------------- | +| **Barrier to entry** | JavaScript knowledge enough | Requires Java + Gradle + MC modding knowledge | +| **Dev speed** | Edit → build → reload (seconds) | Edit → compile → restart MC (minutes) | +| **Hot reload** | Supported (`/box3script reload`) | Not supported; restart required per change | +| **Publishing** | `/box3script compile` → JAR | `gradlew build` → JAR | +| **Performance** | Medium (Rhino interpreted) | High (JIT-compiled bytecode) | +| **API coverage** | High-level wrappers (100+ methods) | Full Minecraft/NeoForge API | +| **Type safety** | TypeScript declarations | Java static types | +| **Debugging** | console.log + server output | IDE breakpoint debugging | +| **Dependency mgmt** | npm (build-time only) | Gradle/Maven | +| **Client features** | Limited (UI/input/audio/chat) | Full (rendering, models, GUI, net protocol) | +| **Custom blocks/items** | JSON config + compile-time gen | Java classes + registration | +| **Modify vanilla behavior** | No (no Mixin) | Yes (Mixin/ASM/CoreMod) | +| **Team collaboration** | JS source + Git | Java source + Git + Gradle | ## Dev Experience Comparison @@ -64,11 +63,11 @@ public class HealMod { This is Box3JS's **single biggest productivity advantage**. -| Action | Box3JS | Java Mod | -|--------|--------|---------| -| Change one line | build(3s) + reload(1s) = **4 seconds** | compile(10-60s) + restartMC(30-120s) = **40-180 seconds** | -| Test a chat command | Edit → build → reload in-game | Edit → compile → restart MC → enter world | -| Iterations per day | **50+** | 5–10 | +| Action | Box3JS | Java Mod | +| ------------------- | -------------------------------------- | --------------------------------------------------------- | +| Change one line | build(3s) + reload(1s) = **4 seconds** | compile(10-60s) + restartMC(30-120s) = **40-180 seconds** | +| Test a chat command | Edit → build → reload in-game | Edit → compile → restart MC → enter world | +| Iterations per day | **50+** | 5–10 | For gameplay scripts (mini-games, RPG mechanics, economy systems), hot reload is **irreplaceable** — gameplay needs constant tuning, and you can't afford to wait for restarts. @@ -96,6 +95,7 @@ world.setScore("Steve", "kills", 5); #### 4. One-Click Project Scaffolding `/box3script create` generates a complete project with: + - TypeScript config + type declarations - Build pipeline (Babel + esbuild) - ESLint code checking @@ -107,27 +107,25 @@ Compare: a Java mod requires manually creating a Gradle project, configuring Neo Before committing to a full Java mod, prototype gameplay with Box3JS: -``` +```js Idea → 30min Box3JS script → test with friends → tweak → gameplay validated ↓ Decide to ship full mod → rewrite in Java ``` ---- - ### Box3JS Disadvantages #### 1. Performance Overhead Rhino is an **interpreted** JS engine (no JIT), single-threaded. Performance-sensitive operations (e.g., scanning thousands of entities per tick) can bottleneck. -| Scenario | Box3JS | Java | -|----------|--------|------| -| Chat commands | Imperceptible | Imperceptible | -| 100 entities per tick | Acceptable | Acceptable | -| 10,000 entities per tick | **May lag** | Acceptable | -| Complex pathfinding math | **Noticeably slow** | Fast | -| Fill entire Y=0 chunk region | **Very slow** | Fast | +| Scenario | Box3JS | Java | +| ---------------------------- | ------------------- | ------------- | +| Chat commands | Imperceptible | Imperceptible | +| 100 entities per tick | Acceptable | Acceptable | +| 10,000 entities per tick | **May lag** | Acceptable | +| Complex pathfinding math | **Noticeably slow** | Fast | +| Fill entire Y=0 chunk region | **Very slow** | Fast | **Rule of thumb:** If `onTick` takes >1ms, consider optimizing or switching to Java. @@ -135,16 +133,16 @@ Rhino is an **interpreted** JS engine (no JIT), single-threaded. Performance-sen Box3JS wraps 100+ common APIs, but not everything: -| What you want | Box3JS | Java | -|-------------|--------|------| -| Modify recipes | No | Yes `RecipeManager` | -| Custom GUI (chest UI) | No | Yes `MenuProvider` / `Screen` | -| Modify mob AI | Partial (setAI/setTarget) | Yes Brain/Memory system | -| Custom dimensions | No | Yes `DimensionType` | -| Datapacks / loot tables | No | Yes full support | -| Network protocol | High-level (remoteChannel) | Yes low-level `CustomPayload` | -| Modify vanilla classes | No | Yes Mixin / ASM | -| Render custom models | No | Yes full render pipeline | +| What you want | Box3JS | Java | +| ----------------------- | -------------------------- | ----------------------------- | +| Modify recipes | No | Yes `RecipeManager` | +| Custom GUI (chest UI) | No | Yes `MenuProvider` / `Screen` | +| Modify mob AI | Partial (setAI/setTarget) | Yes Brain/Memory system | +| Custom dimensions | No | Yes `DimensionType` | +| Datapacks / loot tables | No | Yes full support | +| Network protocol | High-level (remoteChannel) | Yes low-level `CustomPayload` | +| Modify vanilla classes | No | Yes Mixin / ASM | +| Render custom models | No | Yes full render pipeline | #### 3. No Breakpoint Debugging @@ -153,12 +151,14 @@ Only `console.log` output for debugging. No IDE breakpoints, variable watches, o #### 4. Limited Client Features Client scripts can do: + - Key input detection - Screen UI display - Sound/music playback - Chat send/receive But cannot do: + - Custom rendering (models, particles, GUI) - HUD modification - Custom shaders @@ -167,6 +167,7 @@ But cannot do: #### 5. ES5 Limitations Rhino 1.9.1 only supports ES5 syntax. You cannot use: + - `let` / `const` (Babel compiles to `var`) - Arrow functions (Babel compiles to `function`) - `async` / `await` @@ -181,11 +182,9 @@ But **Babel compiles everything to ES5**, so you write modern TS and the build c Compiled JARs depend on Box3JS as a runtime. Users need both Box3JS + your JAR installed. Pure Java mods are self-contained. ---- - ## Decision Tree -``` +```text What do you want to build? │ ├─ Mini-game (PvP/parkour/racing) @@ -224,13 +223,11 @@ What do you want to build? Lots of content → Native Java mod ``` ---- - ## Hybrid Approach Best practice: **Box3JS for gameplay, Java for infrastructure**. -``` +```text ┌──────────────────────────────────┐ │ Java Mod (low-level capabilities)│ │ - Custom blocks/items registry │ @@ -250,22 +247,21 @@ Best practice: **Box3JS for gameplay, Java for infrastructure**. ``` A real-world architecture example: + - Java mod adds custom weapons, custom mobs, a new dimension - Box3JS scripts define wave rules, boss skill patterns, quest triggers - Gameplay designers can independently edit scripts without touching Java ---- - ## Summary -| Choose Box3JS | Choose Java | -|--------------|------------| -| You're building gameplay/mini-games | You need to modify vanilla mechanics | -| You need rapid iteration | You need custom rendering/models | -| Your team has JS developers | Your team is primarily Java developers | -| Logic is complex but no rendering | Project has many custom blocks/entities/dims | -| You want to prototype before committing | You're publishing to CurseForge/Modrinth | -| You need hot reload | You need maximum performance | -| Project is server-side focused | Project needs client-side rendering | +| Choose Box3JS | Choose Java | +| --------------------------------------- | -------------------------------------------- | +| You're building gameplay/mini-games | You need to modify vanilla mechanics | +| You need rapid iteration | You need custom rendering/models | +| Your team has JS developers | Your team is primarily Java developers | +| Logic is complex but no rendering | Project has many custom blocks/entities/dims | +| You want to prototype before committing | You're publishing to CurseForge/Modrinth | +| You need hot reload | You need maximum performance | +| Project is server-side focused | Project needs client-side rendering | **Neither is better — only better-suited to the current project.** For server-side gameplay development, Box3JS's productivity advantages are overwhelming: hot reload + low barrier + rich API. For projects needing vanilla mechanic modification or custom rendering, Java is essential. diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md b/Box3JS-NeoForge-1.21.1/docs/en/guide/recipes.md similarity index 89% rename from Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/guide/recipes.md index 6366ccc..2063b64 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/recipes_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/guide/recipes.md @@ -1,23 +1,9 @@ -# Common Recipes: Box3JS Feature Templates - -This guide is "cookbook" style — not a per-API walkthrough, but a series of "want to implement X? Copy this template and tweak." All code is build-verified. - -## Contents +--- +--- -1. [Chat Commands](#chat-commands) -2. [Economy System](#economy-system) -3. [Teleport System](#teleport-system) -4. [Respawn Protection](#respawn-protection) -5. [Shop / NPC](#shop--npc) -6. [Daily Rewards](#daily-rewards) -7. [Leaderboards](#leaderboards) -8. [Wave Spawning](#wave-spawning) -9. [Shrinking Zone](#shrinking-zone) -10. [HTTP Webhook](#http-webhook) -11. [Client HUD](#client-hud) -12. [Cross-Script Integration](#cross-script-integration) +# Common Recipes ---- +"Want to implement X? Copy this template and tweak." All code is build-verified. ## Chat Commands @@ -73,8 +59,6 @@ if (message === "!admin") { } ``` ---- - ## Economy System Scoreboard-based economy. Data persists through `/box3script reload` (scoreboards are independent of script lifecycle). @@ -150,8 +134,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## Teleport System ### Home TP (in-memory, lost on restart) @@ -243,24 +225,20 @@ world.onChat((entity, message) => { }); ``` ---- - ## Respawn Protection ```js // Give brief invulnerability after respawn world.onPlayerRespawn((entity) => { const p = entity.player; - p.addEffect("minecraft:resistance", 100, 4, true); // 5s Resistance V (near-invulnerable) - p.addEffect("minecraft:regeneration", 100, 2, true); // 5s Regen III - p.addEffect("minecraft:fire_resistance", 100, 0, true); // 5s Fire Resistance + p.addEffect("minecraft:resistance", 100, 4, true); // 5s Resistance V (near-invulnerable) + p.addEffect("minecraft:regeneration", 100, 2, true); // 5s Regen III + p.addEffect("minecraft:fire_resistance", 100, 0, true); // 5s Fire Resistance p.directMessage("§a5 seconds of respawn protection"); p.playSound("minecraft:block.beacon.activate", 1.0, 1.5); }); ``` ---- - ## Shop / NPC Right-click an entity (e.g. villager) to open a dialog/shop: @@ -318,12 +296,11 @@ world.onChat((entity, message) => { }); ``` ---- - ## Daily Rewards ```js -const dailyRewards = storage.getDataStorage<{ lastClaimed: number }>("daily-rewards"); +const dailyRewards = + storage.getDataStorage < { lastClaimed: number } > "daily-rewards"; world.onChat((entity, message) => { const p = entity.player; @@ -331,9 +308,9 @@ world.onChat((entity, message) => { if (message === "!daily") { const now = Date.now(); const record = dailyRewards.get(p.userId); - const cooldown = 24 * 60 * 60 * 1000; // 24 hours + const cooldown = 24 * 60 * 60 * 1000; // 24 hours - if (record && (now - record.lastClaimed) < cooldown) { + if (record && now - record.lastClaimed < cooldown) { const hours = Math.ceil((record.lastClaimed + cooldown - now) / 3600000); p.directMessage(`§cPlease wait ${hours} hours before claiming again`); return false; @@ -346,7 +323,9 @@ world.onChat((entity, message) => { const coins = world.getScore(p.name, "coins"); world.setScore(p.name, "coins", coins + bonus); dailyRewards.set(p.userId, { lastClaimed: now }); - p.directMessage(`§aDaily reward claimed! 3 diamonds + 8 XP bottles + ${bonus} coins`); + p.directMessage( + `§aDaily reward claimed! 3 diamonds + 8 XP bottles + ${bonus} coins`, + ); p.playSound("minecraft:entity.player.levelup", 1.0, 1.0); return false; } @@ -354,8 +333,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## Leaderboards ```js @@ -382,8 +359,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## Wave Spawning Complete wave system with scaling difficulty: @@ -403,7 +378,7 @@ function spawnWave(pos: GameVector3): void { world.say(`§c§l⚔ Wave ${wave} begins! §f${count} mobs`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 12; const z = pos.z + (Math.random() - 0.5) * 12; const type = types[Math.floor(Math.random() * types.length)]; @@ -424,8 +399,6 @@ function spawnWave(pos: GameVector3): void { } ``` ---- - ## Shrinking Zone ```js @@ -445,10 +418,10 @@ function startShrinkPhase(centerX: number, centerZ: number, stages: { size: numb world.say(`§cBorder shrinking to ${stage.size} blocks! (${stage.duration}s)`); world.shrinkBorder(stage.size * 2, stage.duration); stageIndex++; - world.setTimeout(nextStage, stage.duration * 20); + setTimeout(nextStage, stage.duration * 20); } - world.setTimeout(nextStage, 100); // Start after 5 seconds + setTimeout(nextStage, 100); // Start after 5 seconds } // Usage: 100→50→25→10, 60s per stage @@ -460,8 +433,6 @@ startShrinkPhase(0, 0, [ ]); ``` ---- - ## HTTP Webhook ```js @@ -478,8 +449,12 @@ world.onEntityDeath((entity, killer) => { }), timeout: 5000, async: true, - onResponse: (resp) => { console.log(`Webhook sent: ${resp.status}`); }, - onError: (err) => { console.warn(`Webhook failed: ${err}`); }, + onResponse: (resp) => { + console.log(`Webhook sent: ${resp.status}`); + }, + onError: (err) => { + console.warn(`Webhook failed: ${err}`); + }, }); } }); @@ -488,7 +463,7 @@ world.onEntityDeath((entity, killer) => { const SERVER_NAME = "My Server"; const WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_ID"; -world.setInterval(() => { +setInterval(() => { const playerCount = world.querySelectorAll("*").length; http.fetch(WEBHOOK_URL, { @@ -503,8 +478,6 @@ world.setInterval(() => { }, 6000); ``` ---- - ## Client HUD Combine `remoteChannel` for a custom client-side HUD (server provides data, client displays it): @@ -559,8 +532,6 @@ remoteChannel.onClientEvent((event) => { }); ``` ---- - ## Cross-Script Integration Multiple script projects communicating: @@ -589,6 +560,4 @@ function endGame(): void { } ``` ---- - -Each recipe is self-contained — grab what you need. See the [API reference](../api/README_en.md) and [tutorials](../tutorial/README_en.md) for more detail. +Each recipe is self-contained — grab what you need. See the [API reference](../api/README.md) and [tutorials](../tutorial/README.md) for more detail. diff --git a/Box3JS-NeoForge-1.21.1/docs/en/index.md b/Box3JS-NeoForge-1.21.1/docs/en/index.md new file mode 100644 index 0000000..9f5cfba --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/en/index.md @@ -0,0 +1,78 @@ +--- +layout: home + +hero: + name: "Box3JS" + text: "JS/TS Scripting Engine for Minecraft" + tagline: Build custom gameplay, mini-games, and UIs — no JDK, no Java compilation required. + actions: + - theme: brand + text: Get Started + link: /en/guide/getting-started + +features: + - icon: 🎮 + title: Server & Client Scripting + details: Server-side world manipulation, entities, recipes. Client-side keyboard input, screen UI, sounds, SQLite storage, and HTTP requests. + - icon: 📦 + title: TypeScript First + details: Full DTS type definitions for all 17 global objects. Built-in esbuild + Babel pipeline transpiles modern TS to Rhino-compatible ES5. + - icon: 🔄 + title: Hot Reload + details: Edit scripts and see changes instantly without restarting the server. File watcher auto-reloads on save. + - icon: 🌐 + title: Bidirectional Communication + details: remoteChannel enables server↔client event messaging. Server broadcasts to all players; clients reply independently. + - icon: 🗄️ + title: Dual-Side Storage & Database + details: JSON file persistence and SQLite on both server and client. Pagination, atomic updates, counters, and tagged-template queries. + - icon: 🧩 + title: Custom Blocks & Items + details: Block textures, item models, equipment, sounds, and creative tabs — all registered from JSON configs (standalone/JAR mode). + - icon: 📚 + title: Comprehensive Docs + details: 50+ pages across API reference, progressive tutorials, cookbook recipes, architecture deep-dive, and FAQ — in Chinese and English. + - icon: 🚀 + title: Standalone JAR Mode + details: Compile your script project into a self-contained JAR mod. No runtime dependency on Box3JS — just drop it in your mods folder. + +--- + +## Quick Start + +```bash +# In-game: create a new project +/box3script create mygame + +# Build and watch +cd config/box3/script/mygame +npm install +npm run build -- --watch + +# TypeScript type checking +npm run check +``` + +```ts +// src/server/app.ts — your first script +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage(`Hello, ${entity.player.name}!`); + return false; + } + return true; +}); +``` + +[Read full docs →](/en/guide/getting-started_en) + +## Version Info + +| Component | Version | +|-----------|---------| +| Minecraft | 1.21.1 | +| Mod Loader | NeoForge | +| Java | 21 | +| JS Engine | Mozilla Rhino 1.9.1 (ES5) | +| TypeScript | via Babel → ES5 | + diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/01-basics.md similarity index 78% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/01-basics.md index 9faf13a..5cb5022 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/01-basics.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 1: 5-Minute Quick Start Get from zero to your first running Box3JS script — no Minecraft modding experience needed, just JavaScript knowledge. @@ -11,7 +14,7 @@ Get from zero to your first running Box3JS script — no Minecraft modding exper Run one command in-game: -``` +```js /box3script create hello ``` @@ -46,7 +49,7 @@ world.onPlayerJoin((entity) => { Back in-game: -``` +```js /box3script start hello ``` @@ -57,19 +60,17 @@ Now when a player joins, they'll receive "§aWelcome to the server!" in green. T Try changing the welcome message to: ```js -entity.player.directMessage("§6Hello, " + entity.player.name + "!"); +entity.player.directMessage(`§6Hello, ${entity.player.name}!`); ``` Save, run `npm run build`, then in-game: -``` +```js /box3script reload hello ``` No server restart required — changes take effect immediately. ---- - These 5 steps form the complete dev cycle: **edit code → build → reload**. The rest of this tutorial dives into everything you can build. ## Message System @@ -94,22 +95,22 @@ player.title("§6§lMain Title", "§7Subtitle"); player.title("§c§lBOSS", "Ancient Dragon", 10, 60, 10); ``` -| Method | Location | Visibility | -|--------|----------|------------| -| `world.say()` | Chat | Server-wide | -| `player.directMessage()` | Chat | Single player | -| `player.actionBar()` | Above hotbar | Single player | -| `player.title()` | Screen center | Single player | +| Method | Location | Visibility | +| ------------------------ | ------------- | ------------- | +| `world.say()` | Chat | Server-wide | +| `player.directMessage()` | Chat | Single player | +| `player.actionBar()` | Above hotbar | Single player | +| `player.title()` | Screen center | Single player | ### console Logging `console` outputs to the server console in the format `[Box3JS] [projectName] message`: ```js -console.log("Info"); // [Box3JS] [hello] Info -console.debug("Debug"); // [Box3JS] [hello] [DEBUG] Debug +console.log("Info"); // [Box3JS] [hello] Info +console.debug("Debug"); // [Box3JS] [hello] [DEBUG] Debug console.warn("Warning"); // [Box3JS] [hello] [WARN] Warning -console.error("Error"); // [Box3JS] [hello] [ERROR] Error +console.error("Error"); // [Box3JS] [hello] [ERROR] Error ``` ## Chat Command System @@ -128,7 +129,7 @@ world.onChat((entity, message) => { p.directMessage("§f!pos §7- Check position"); p.directMessage("§f!day §7- Set to daytime"); p.directMessage("§f!clear §7- Clear weather"); - return false; // ★ Return false to suppress the chat message + return false; // ★ Return false to suppress the chat message case "!hello": p.directMessage(`§eHello, ${p.name}!`); @@ -141,7 +142,7 @@ world.onChat((entity, message) => { case "!pos": { const pos = p.position; p.directMessage( - `§eYour position: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}` + `§eYour position: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}`, ); return false; } @@ -156,7 +157,7 @@ world.onChat((entity, message) => { world.say(`§e${p.name} §fcleared the weather`); return false; } - return true; // Non-command messages pass through normally + return true; // Non-command messages pass through normally }); ``` @@ -175,7 +176,14 @@ world.onPlayerJoin((entity) => { // Particle circle + sound const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); ``` @@ -186,44 +194,44 @@ Effect: when a player joins, they see a screen title, hear a bell chime, and gre ```js // Broadcast player count every 5 minutes -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7Online: §f${count} §7players`); -}, 6000); // 6000 ticks = 5 minutes +}, 6000); // 6000 ticks = 5 minutes // Run once after 30 seconds -world.setTimeout(() => { +setTimeout(() => { world.say("§6Server has been running for 30 seconds"); -}, 600); // 600 ticks = 30 seconds +}, 600); // 600 ticks = 30 seconds ``` **Tick conversion:** 20 ticks = 1 second -| Duration | Ticks | -|----------|-------| -| 1 second | 20 | -| 5 seconds | 100 | -| 30 seconds | 600 | -| 1 minute | 1200 | -| 5 minutes | 6000 | +| Duration | Ticks | +| ---------- | ----- | +| 1 second | 20 | +| 5 seconds | 100 | +| 30 seconds | 600 | +| 1 minute | 1200 | +| 5 minutes | 6000 | ## World Properties ```js // Time -world.time = 6000; // Noon (0=sunrise, 6000=noon, 12000=sunset, 18000=midnight) +world.time = 6000; // Noon (0=sunrise, 6000=noon, 12000=sunset, 18000=midnight) // Weather -world.rainDensity = 1.0; // Full rain +world.rainDensity = 1.0; // Full rain world.thunderDensity = 0.5; // Thunderstorm -world.clearWeather(); // Clear skies +world.clearWeather(); // Clear skies // Difficulty -world.difficulty = "hard"; // peaceful / easy / normal / hard +world.difficulty = "hard"; // peaceful / easy / normal / hard // Game rules -world.setGameRule("keepInventory", true); // Keep inventory on death -world.setGameRule("doFireTick", false); // Fire doesn't spread +world.setGameRule("keepInventory", true); // Keep inventory on death +world.setGameRule("doFireTick", false); // Fire doesn't spread world.setGameRule("doMobSpawning", false); // Disable mob spawning ``` @@ -241,14 +249,27 @@ console.log("[Hello] Script loaded"); // ── Welcome effects ── world.onPlayerJoin((entity) => { const p = entity.player; - p.title("§6§lWelcome to the server!", "§7Type §f!help §7for commands", 5, 70, 10); + p.title( + "§6§lWelcome to the server!", + "§7Type §f!help §7for commands", + 5, + 70, + 10, + ); const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); // ── Periodic announcement ── -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7Online: §f${count} §7players`); }, 6000); @@ -268,7 +289,9 @@ world.onChat((entity, message) => { return false; case "!pos": { const pos = p.position; - p.directMessage(`§ePosition: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`); + p.directMessage( + `§ePosition: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`, + ); return false; } case "!online": @@ -291,13 +314,13 @@ world.onChat((entity, message) => { ### Dev Cycle -``` +```js Edit code → npm run build → /box3script reload hello → Test ``` Enable file watching for auto hot-reload (no manual reload needed): -``` +```js /box3script watch ``` @@ -305,7 +328,7 @@ Enable file watching for auto hot-reload (no manual reload needed): With sandbox enabled, all world modifications by the script are tracked and can be rolled back with one command: -``` +```js /box3script sandbox hello # Enable # ... test your script ... /box3script sandbox hello # Disable → all changes rolled back @@ -314,6 +337,7 @@ With sandbox enabled, all world modifications by the script are tracked and can ### Debugging When something goes wrong, check in this order: + 1. Check the server console for errors (`console.log` output appears here) 2. Verify the script is loaded: run `/box3script` and check if the project shows `◉` (loaded and running) 3. Verify the build succeeded: `npm run build` should complete without errors @@ -321,4 +345,4 @@ When something goes wrong, check in this order: ## Next Step -[Tutorial 2: Players & Items](../tutorial/02-player-items_en.md) — teleport, items, enchantments, potion effects, game modes. +[Tutorial 2: Players & Items](../tutorial/02-player-items.md) — teleport, items, enchantments, potion effects, game modes. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/02-player-items.md similarity index 99% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/02-player-items.md index b5c1dd7..f3c3540 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/02-player-items.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 2: Players & Items This tutorial covers player properties, teleportation, giving items, potion effects, game modes, and more. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/03-events-entities.md similarity index 95% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/03-events-entities.md index 1ef72fc..76bac8c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/03-events-entities.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 3: Events & Entities This tutorial dives into event callbacks, block interactions, entity spawning, AI, and combat events. @@ -26,7 +29,7 @@ All events are registered via `world.onXxx(handler)` and return a `GameEventHand ```js const token = world.onTick((info) => { - console.log("Tick: " + info.tick); + console.log(`Tick: ${info.tick}`); }); token.cancel(); // Unsubscribe @@ -164,9 +167,10 @@ const entity = world.createEntity({ maxHp: 40, tags: ["elite", "undead"], }); +if (!entity) return; // createEntity may return null entity.setEquipment("mainhand", "minecraft:bow"); -entity.setTarget(somePlayerEntity); // Set attack target +// entity.setTarget(targetEntity); // Set attack target (requires entity reference) entity.clearTarget(); // Clear target entity.navigateTo(10, 100, 10, 0.5); // Navigate to position entity.setPersistent(true); // Persistent (won't be unloaded) @@ -199,9 +203,9 @@ function createPatrol( guard.setAI(true); let wpIndex = 0; - const tid = world.setInterval(() => { + const tid = setInterval(() => { if (guard.destroyed) { - world.clearInterval(tid); + tid.cancel(); return; } // Reached current waypoint → move to next @@ -251,20 +255,20 @@ if (entity.hasTag("boss")) { const tags = entity.tags(); // ["boss", "undead"] // Entity collision -world.onEntityContact((entityA, entityB, tick) => { +world.onEntityContact((entityA, entityB, _tick) => { if (entityA.isPlayer() && entityB.hasTag("boss")) { entityA.player.actionBar("§cWatch out — Boss!"); } }); -world.onEntitySeparate((entityA, entityB, tick) => { +world.onEntitySeparate((entityA, entityB, _tick) => { // Two entities separated }); ``` ## 3.8 Common Entity Types -``` +```js minecraft:zombie Zombie minecraft:skeleton Skeleton minecraft:creeper Creeper diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/04-advanced-systems.md similarity index 98% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/04-advanced-systems.md index b3f6efd..f169307 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/04-advanced-systems.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 4: Advanced Game Systems This tutorial covers scoreboards, BossBars, teams, world border, and cross-script communication. @@ -38,7 +41,7 @@ world.addScoreboard("playtime", "dummy"); world.showScoreboard("sidebar", "playtime"); // +1 every minute -world.setInterval(() => { +setInterval(() => { world.querySelectorAll("*").forEach((entity) => { if (!entity.isPlayer()) { return; } const p = entity.player; @@ -94,11 +97,11 @@ Color options: `"blue"` `"green"` `"pink"` `"purple"` `"red"` `"white"` `"yellow let timeLeft = 30; world.showBossbar("demo_timer", "§eCountdown Demo", 1.0, "green"); -const timerId = world.setInterval(() => { +const timerId = setInterval(() => { timeLeft--; if (timeLeft <= 0) { world.removeBossbar("demo_timer"); - world.clearInterval(timerId); + timerId.cancel(); world.say("§c⏰ Time's up!"); world.playSound("minecraft:block.note_block.pling", new GameVector3(0, 70, 0), 1.0, 0.5); return; @@ -201,7 +204,7 @@ world.borderSize = 200; world.setBorderDamage(1); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.say("§cBorder shrinking to 50 blocks!"); world.shrinkBorder(50, 60); world.playSound( diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/05-examples.md similarity index 81% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/05-examples.md index 802b761..94a8275 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/05-examples.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 5: Visual Effects & Complete Mini-Games This tutorial covers particles, fireworks, lightning, explosions, and other visual effects, plus three verified complete mini-games. @@ -19,28 +22,28 @@ world.spawnParticleCircle(0, 100, 0, 4.0, "minecraft:end_rod", 36); Common particles: -| Particle ID | Effect | -|-------------|--------| -| `minecraft:flame` | Fire | -| `minecraft:cloud` | Smoke | -| `minecraft:happy_villager` | Green particles (positive) | -| `minecraft:witch` | Purple particles | -| `minecraft:portal` | Portal | -| `minecraft:end_rod` | End rod light | -| `minecraft:heart` | Hearts | -| `minecraft:note` | Music notes | -| `minecraft:dragon_breath` | Dragon's breath | -| `minecraft:angry_villager` | Angry particles (red) | -| `minecraft:soul_fire_flame` | Soul fire (blue) | -| `minecraft:redstone` | Redstone particles | -| `minecraft:explosion` | Explosion particles | +| Particle ID | Effect | +| --------------------------- | -------------------------- | +| `minecraft:flame` | Fire | +| `minecraft:cloud` | Smoke | +| `minecraft:happy_villager` | Green particles (positive) | +| `minecraft:witch` | Purple particles | +| `minecraft:portal` | Portal | +| `minecraft:end_rod` | End rod light | +| `minecraft:heart` | Hearts | +| `minecraft:note` | Music notes | +| `minecraft:dragon_breath` | Dragon's breath | +| `minecraft:angry_villager` | Angry particles (red) | +| `minecraft:soul_fire_flame` | Soul fire (blue) | +| `minecraft:redstone` | Redstone particles | +| `minecraft:explosion` | Explosion particles | ### Spiral Rising Particles ```js function spiralEffect(pos: GameVector3): void { for (let i = 0; i < 40; i++) { - world.setTimeout(() => { + setTimeout(() => { const angle = (i / 40) * Math.PI * 4; const radius = 2.0; const px = pos.x + Math.cos(angle) * radius; @@ -70,18 +73,28 @@ Firework shapes: `"ball"` `"large_ball"` `"star"` `"creeper"` `"burst"` ### Sequential Firework Show ```js -const colors = ["red", "gold", "green", "blue", "purple", "white", "pink", "aqua"]; +const colors = [ + "red", + "gold", + "green", + "blue", + "purple", + "white", + "pink", + "aqua", +]; const shapes = ["ball", "large_ball", "star", "creeper", "burst"]; for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const c = colors[i % colors.length]; const s = shapes[i % shapes.length]; world.launchFirework( pos.x + (Math.random() - 0.5) * 10, pos.y + 5 + Math.random() * 8, pos.z + (Math.random() - 0.5) * 10, - c, s + c, + s, ); }, i * 300); } @@ -91,13 +104,13 @@ for (let i = 0; i < 8; i++) { ```js // Lightning: (x, y, z, damage) -world.strikeLightning(0, 100, 0); // Default damage -world.strikeLightning(0, 100, 0, 10); // 10 damage -world.strikeLightning(0, 100, 0, 0); // No damage, visual only +world.strikeLightning(0, 100, 0); // Default damage +world.strikeLightning(0, 100, 0, 10); // 10 damage +world.strikeLightning(0, 100, 0, 0); // No damage, visual only // Summon lightning around a player for (let i = 0; i < 3; i++) { - world.setTimeout(() => { + setTimeout(() => { const lx = pos.x + (Math.random() - 0.5) * 12; const lz = pos.z + (Math.random() - 0.5) * 12; world.strikeLightning(lx, pos.y, lz, 0); @@ -110,14 +123,24 @@ world.playSound("minecraft:entity.lightning_bolt.thunder", pos, 1.0, 1.0); ```js // Explosion: (x, y, z, power, causesFire) -world.explode(0, 100, 0, 4, false); // Power 4, no fire -world.explode(0, 100, 0, 8, true); // Power 8, causes fire +world.explode(0, 100, 0, 4, false); // Power 4, no fire +world.explode(0, 100, 0, 8, true); // Power 8, causes fire // Player-triggered self-destruct (3-second countdown) world.playSound("minecraft:block.note_block.bass", pos, 1.0, 0.5); -world.setTimeout(() => { - world.spawnParticle("minecraft:explosion", pos.x, pos.y, pos.z, 1, 0, 0, 0, 0); - world.setTimeout(() => { +setTimeout(() => { + world.spawnParticle( + "minecraft:explosion", + pos.x, + pos.y, + pos.z, + 1, + 0, + 0, + 0, + 0, + ); + setTimeout(() => { world.explode(pos.x, pos.y, pos.z, 4, false); world.playSound("minecraft:entity.generic.explode", pos, 1.0, 1.0); }, 10); @@ -137,22 +160,22 @@ player.playSound("minecraft:entity.player.levelup", 1.0, 1.0); Common sounds: -| Sound ID | Use | -|----------|-----| -| `minecraft:block.note_block.pling` | Bell chime | -| `minecraft:block.note_block.bass` | Bass note | -| `minecraft:entity.experience_orb.pickup` | XP orb pickup | -| `minecraft:entity.player.levelup` | Level up | -| `minecraft:entity.ender_dragon.growl` | Dragon roar (boss entrance) | -| `minecraft:entity.wither.spawn` | Wither spawn (menacing) | -| `minecraft:entity.lightning_bolt.thunder` | Thunder | -| `minecraft:entity.generic.explode` | Explosion | -| `minecraft:entity.witch.throw` | Potion throw | -| `minecraft:block.beacon.activate` | Beacon activation | -| `minecraft:block.anvil.land` | Anvil landing | -| `minecraft:ui.toast.challenge_complete` | Challenge complete | -| `minecraft:entity.player.burp` | Eating sound | -| `minecraft:entity.enderman.teleport` | Teleport sound | +| Sound ID | Use | +| ----------------------------------------- | --------------------------- | +| `minecraft:block.note_block.pling` | Bell chime | +| `minecraft:block.note_block.bass` | Bass note | +| `minecraft:entity.experience_orb.pickup` | XP orb pickup | +| `minecraft:entity.player.levelup` | Level up | +| `minecraft:entity.ender_dragon.growl` | Dragon roar (boss entrance) | +| `minecraft:entity.wither.spawn` | Wither spawn (menacing) | +| `minecraft:entity.lightning_bolt.thunder` | Thunder | +| `minecraft:entity.generic.explode` | Explosion | +| `minecraft:entity.witch.throw` | Potion throw | +| `minecraft:block.beacon.activate` | Beacon activation | +| `minecraft:block.anvil.land` | Anvil landing | +| `minecraft:ui.toast.challenge_complete` | Challenge complete | +| `minecraft:entity.player.burp` | Eating sound | +| `minecraft:entity.enderman.teleport` | Teleport sound | ## 5.6 Player Join/Leave Effects @@ -160,12 +183,29 @@ Common sounds: world.onPlayerJoin((entity, _tick) => { const pos = entity.position; world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); }); world.onPlayerLeave((entity, _tick) => { const pos = entity.position; - world.spawnParticle("minecraft:cloud", pos.x, pos.y, pos.z, 10, 0.3, 0.3, 0.3, 0.01); + world.spawnParticle( + "minecraft:cloud", + pos.x, + pos.y, + pos.z, + 10, + 0.3, + 0.3, + 0.3, + 0.01, + ); }); ``` @@ -174,6 +214,7 @@ world.onPlayerLeave((entity, _tick) => { This is a practical application of the design patterns from Tutorial 4 — a full red-vs-blue PvP mini-game integrating events, BossBar, scoreboard, teams, particles, fireworks, shrinking border, airdrops, and more. **Commands:** + - `!pvp join` — Join the game - `!pvp leave` — Leave the queue - `!pvp start` — (OP) Start the game @@ -181,6 +222,7 @@ This is a practical application of the design patterns from Tutorial 4 — a ful - `!pvp status` — Check status **Features:** + - Lobby countdown 30s → game duration 300s - Auto-assign red/blue teams + team prefixes - Kill scoring + global announcements + firework effects @@ -215,9 +257,9 @@ const state: PvPState = { blueScore: 0, }; -let pvpGameTimer: number | null = null; -let pvpAirdropTimer: number | null = null; -let pvpLobbyTimer: number | null = null; +let pvpGameTimer: GameEventHandlerToken | null = null; +let pvpAirdropTimer: GameEventHandlerToken | null = null; +let pvpLobbyTimer: GameEventHandlerToken | null = null; // ── Initialization ── world.setGameRule("keepInventory", false); @@ -269,9 +311,9 @@ world.onChat((entity, message, _tick) => { function startLobby(): void { state.phase = "starting"; let cd = 30; - pvpLobbyTimer = world.setInterval(() => { + pvpLobbyTimer = setInterval(() => { cd--; - if (cd <= 0 && pvpLobbyTimer) { world.clearInterval(pvpLobbyTimer); beginPvPGame(); } + if (cd <= 0 && pvpLobbyTimer) { pvpLobbyTimer.cancel(); beginPvPGame(); } else if (cd <= 5) { world.say(`§eGame starts in §c${cd} §eseconds!`); } else if (cd % 10 === 0) { world.say(`§7Game starts in ${cd} seconds...`); } }, 20); @@ -322,7 +364,7 @@ function beginPvPGame(): void { // Game countdown let remaining = DURATION; - pvpGameTimer = world.setInterval(() => { + pvpGameTimer = setInterval(() => { remaining--; const progress = remaining / DURATION; const mins = Math.floor(remaining / 60); @@ -343,20 +385,20 @@ function beginPvPGame(): void { if (remaining === 60) { world.say("§cFinal minute!"); } if (remaining === 30) { world.strikeLightning(ARENA.x, ARENA.y, ARENA.z, 0); } if (remaining <= 0 && pvpGameTimer) { - world.clearInterval(pvpGameTimer); + pvpGameTimer.cancel(); endPvPGame(); } }, 20); // Airdrops - pvpAirdropTimer = world.setInterval(() => { + pvpAirdropTimer = setInterval(() => { if (state.phase !== "playing") return; const angle = Math.random() * Math.PI * 2; const dist = Math.random() * ARENA_RADIUS * 0.6; const dx = ARENA.x + Math.cos(angle) * dist; const dz = ARENA.z + Math.sin(angle) * dist; world.strikeLightning(dx, ARENA.y + 30, dz, 0); - world.setTimeout(() => { + setTimeout(() => { world.dropItem(dx, ARENA.y + 1, dz, "minecraft:ender_pearl", 2); world.dropItem(dx, ARENA.y + 1, dz, "minecraft:golden_apple", 2); world.launchFirework(dx, ARENA.y + 3, dz, "yellow", "ball"); @@ -408,7 +450,7 @@ world.onPlayerRespawn((entity, _tick) => { function endPvPGame(): void { state.phase = "ending"; world.removeBossbar("pvp_timer"); - if (pvpAirdropTimer) { world.clearInterval(pvpAirdropTimer); } + if (pvpAirdropTimer) { pvpAirdropTimer.cancel(); } let winner = "Draw!"; let color = "e"; @@ -427,7 +469,7 @@ function endPvPGame(): void { // Victory fireworks for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const cs = ["red", "gold", "green", "blue", "purple"]; const ss = ["ball", "large_ball", "star", "burst"]; world.launchFirework( @@ -441,7 +483,7 @@ function endPvPGame(): void { } // Reset after 30 seconds - world.setTimeout(() => { + setTimeout(() => { state.phase = "waiting"; state.playersReady = 0; state.redScore = 0; state.blueScore = 0; world.hideScoreboard("sidebar"); @@ -546,7 +588,7 @@ function startWave(pos: GameVector3): void { world.say(`§c§l⚔ Wave ${wave} begins!§f Spawning ${count} zombies`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 10; const z = pos.z + (Math.random() - 0.5) * 10; const zombie = world.spawnEntity("minecraft:zombie", new GameVector3(x, pos.y, z)); @@ -565,7 +607,7 @@ world.onEntityDeath((entity, killer, _tick) => { mobsAlive--; if (mobsAlive <= 0) { world.say(`§a§l✔ Wave ${wave} cleared!`); - world.setTimeout(() => startWave(entity.position), 200); + setTimeout(() => startWave(entity.position), 200); } }); ``` @@ -580,7 +622,7 @@ world.onChat((entity, message, _tick) => { if (message === "!sounds") { const notes = [1.0, 1.2, 1.5, 2.0]; notes.forEach((pitch, i) => { - world.setTimeout(() => { + setTimeout(() => { p.playSound("minecraft:block.note_block.pling", 1.0, pitch); }, i * 100); }); @@ -591,8 +633,6 @@ world.onChat((entity, message, _tick) => { }); ``` ---- - All example code has been verified with `tsc --noEmit`, `eslint`, and `node build.mjs`. Ready to use. For more API details, refer to the complete API docs in the `docs/api/` directory. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/06-client-scripting.md similarity index 87% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/06-client-scripting.md index 1cdc153..bd9ccbe 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/06-client-scripting.md @@ -1,3 +1,6 @@ +--- +--- + # Tutorial 6: Client-Side Scripting This tutorial covers all 9 global objects available in Box3JS client scripts: lifecycle, keyboard input, screen UI, chat control, sounds/music, local storage, SQLite, HTTP requests, and bidirectional communication. @@ -34,6 +37,7 @@ The colorzone project (`src/client/app.ts`) contains a complete client-side demo | ui | On-screen text | F6 to show settings | | chat | Chat commands | `!fav` `!mob` | | audio | Custom sounds | V key | +| fog | Fog colour and distance control | — | ## 6.3 client — Lifecycle @@ -50,6 +54,8 @@ client.onTick(() => { }); ``` +Like other event APIs, `client.onTick()` returns a `GameEventHandlerToken`; call `token.cancel()` when you no longer need the listener. + **Performance tip:** Client onTick also runs on the main thread. Avoid tight loops; use modulo to reduce the effective execution rate. ## 6.4 input — Keyboard Input @@ -135,7 +141,30 @@ const musicVol = audio.getVolume("music"); // Read current volume audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); ``` -## 6.8 storage — Client-Side Local Storage +## 6.8 fog — Fog Control + +Override Minecraft's fog colour and render distance: + +```js +// Set fog colour (RGB 0-255) +client.setFogColor(255, 100, 50); + +// Set fog distance (in blocks) +client.setFogStartDistance(10); // fog begins beyond 10 blocks +client.setFogEndDistance(50); // fully obscured beyond 50 blocks + +// Read current fog colour +const color = client.getFogColor(); // returns GameRGBColor or null + +// Restore Minecraft default fog +client.resetFog(); +``` + +::: warning +Fog changes take effect locally on each client. Use `remoteChannel` to let the server trigger fog changes on clients, enabling server-controlled weather effects. +::: + +## 6.9 storage — Client-Side Local Storage Client-side `storage` uses the same API as the server but stores data locally on each player's machine: @@ -192,7 +221,7 @@ const page = notes.list({ pageSize: 10, ascending: false }); const entries = page.getCurrentPage(); ``` -## 6.9 db — Client-Side SQLite +## 6.10 db — Client-Side SQLite The client also supports SQLite (requires `minecraft-sqlite-jdbc` mod): @@ -240,9 +269,11 @@ function searchMobs(keyword: string): void { } ``` -> When `minecraft-sqlite-jdbc` is not installed, `db.isAvailable()` returns `false` and all SQL calls silently return empty results. +::: warning +When `minecraft-sqlite-jdbc` is not installed, `db.isAvailable()` returns `false` and all SQL calls silently return empty results. +::: -## 6.10 http — Client HTTP Requests +## 6.11 http — Client HTTP Requests ```js // Synchronous GET @@ -284,7 +315,7 @@ http.fetch("https://httpbin.org/post", { }); ``` -## 6.11 remoteChannel — Bidirectional Communication +## 6.12 remoteChannel — Bidirectional Communication This is the most powerful client scripting feature: server and client can send events to each other. @@ -361,22 +392,15 @@ remoteChannel.onServerEvent((event) => { ### Detecting Client Compatibility -```js -// Server-side: check if a player has Box3JS client mod installed -if (entity.player.hasBox3JSClientMod()) { - // Can send client events - remoteChannel.sendClientEvent(entity, { type: "custom_ui", ... }); -} else { - // Fallback to chat messages - entity.player.directMessage("Install Box3JS client for the full experience"); -} -``` +No manual detection is needed. `remoteChannel.sendClientEvent()` uses optional payloads — players without the Box3JS client mod will silently ignore them without errors or disconnects. You can safely send events to all players. ### Data Format -> **Important:** Data sent across the network must be JSON-serializable (string, number, boolean, null, plain objects, arrays). You cannot send functions, Java objects, or `GameVector3`. +::: warning +Data sent across the network must be JSON-serializable (string, number, boolean, null, plain objects, arrays). You cannot send functions, Java objects, or `GameVector3`. +::: -## 6.12 Practical Example: Custom HUD Status Bar +## 6.13 Practical Example: Custom HUD Status Bar Combining input, ui, remoteChannel, and storage to create a custom HUD: @@ -443,7 +467,7 @@ ui.showTitle("§6Custom HUD Active", "§7F6=Coords F7=FPS", 10, 40, 10); console.log("[HUD] Client HUD demo loaded"); ``` -## 6.13 Debugging Client Scripts +## 6.14 Debugging Client Scripts Client script `console.log` output goes to the **client log** (not the server log). Check your Minecraft launcher or logs directory. @@ -455,4 +479,4 @@ Troubleshooting order: ## Next Steps -[API Reference →](../api/client_en.md) Complete client API docs · [Tutorial 1](01-basics_en.md) Back to basics +[API Reference →](../api/client.md) Complete client API docs · [Tutorial 1](01-basics.md) Back to basics diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/README.md similarity index 57% rename from Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md rename to Box3JS-NeoForge-1.21.1/docs/en/tutorial/README.md index 038197c..cfb76e0 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/README_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/en/tutorial/README.md @@ -1,10 +1,11 @@ -# Box3JS Tutorials +--- +--- -Learn Box3JS scripting from scratch. Each tutorial takes 10–15 minutes and includes complete runnable code. +# Box3JS Tutorials ## Learning Path -``` +```text Beginner Intermediate Advanced │ │ │ │ Tutorial 1: Basics │ Tutorial 3: Events │ Tutorial 5: Mini-Games @@ -21,20 +22,20 @@ Beginner Intermediate Advanced │ ▼ Want to go deeper? - → [Architecture](../guide/architecture_en.md) - → [JS vs Java](../guide/js-vs-java_en.md) + → [Architecture](../guide/architecture.md) + → [JS vs Java](../guide/js-vs-java.md) ``` ## Tutorial List | # | Tutorial | You'll learn | |---|---------|-------------| -| 1 | [From Zero](01-basics_en.md) | Create project → build → first script → chat commands → timers | -| 2 | [Players & Items](02-player-items_en.md) | Teleport, flight, give items, enchantments, potion effects, game modes | -| 3 | [Events & Entities](03-events-entities_en.md) | All event callbacks, spawn entities, AI control, patrol guards, collision | -| 4 | [Advanced Systems](04-advanced-systems_en.md) | Scoreboards, BossBars, teams, world border, cross-script messaging | -| 5 | [Real Mini-Games](05-examples_en.md) | Full PvP arena, particle effects, fireworks, wave spawning, home TP | -| 6 | [Client-Side Scripting](06-client-scripting_en.md) | Keyboard input, screen UI, sound/music, local storage, SQLite, HTTP, remoteChannel | +| 1 | [From Zero](01-basics.md) | Create project → build → first script → chat commands → timers | +| 2 | [Players & Items](02-player-items.md) | Teleport, flight, give items, enchantments, potion effects, game modes | +| 3 | [Events & Entities](03-events-entities.md) | All event callbacks, spawn entities, AI control, patrol guards, collision | +| 4 | [Advanced Systems](04-advanced-systems.md) | Scoreboards, BossBars, teams, world border, cross-script messaging | +| 5 | [Real Mini-Games](05-examples.md) | Full PvP arena, particle effects, fireworks, wave spawning, home TP | +| 6 | [Client-Side Scripting](06-client-scripting.md) | Keyboard input, screen UI, sound/music, local storage, SQLite, HTTP, remoteChannel | ## Prerequisites @@ -42,7 +43,7 @@ Beginner Intermediate Advanced - **Environment:** All server-side code runs on the server; players need nothing installed. Client scripts require the Box3JS client mod. - **Hot Reload:** Edit code → `npm run build` → `/box3script reload` — no server restart needed. - **Deployment:** When done, `/box3script compile` packages your script into a standalone JAR for `mods/`. -- **API Lookup:** Stuck on "which API does X"? Check the [API Task Reference](../api/README_en.md). +- **API Lookup:** Stuck on "which API does X"? Check the [API Task Reference](../api/README.md). ## Quick Example @@ -58,7 +59,7 @@ world.onChat((entity, message) => { return true; }); -world.setInterval(() => { +setInterval(() => { world.say("Players online: " + world.querySelectorAll("*").length); }, 6000); ``` @@ -69,25 +70,25 @@ world.setInterval(() => { | I want to... | Read this | |-------------|----------| -| Look up a specific API | [API Reference](../api/README_en.md) | -| Understand Box3JS internals | [Architecture](../guide/architecture_en.md) | -| Ship my script as a standalone mod | [Quick Start - Deployment](../guide/getting-started_en.md#deployment) | -| Register custom blocks/items/sounds | [registries API](../api/registries_en.md) | -| Write client scripts (UI/input/audio) | [client API](../api/client_en.md) | -| Decide Box3JS vs Java modding | [JS vs Java](../guide/js-vs-java_en.md) | +| Look up a specific API | [API Reference](../api/README.md) | +| Understand Box3JS internals | [Architecture](../guide/architecture.md) | +| Ship my script as a standalone mod | [Quick Start - Deployment](../guide/getting-started.md#deployment) | +| Register custom blocks/items/sounds | [registries API](../api/registries.md) | +| Write client scripts (UI/input/audio) | [client API](../api/client.md) | +| Decide Box3JS vs Java modding | [JS vs Java](../guide/js-vs-java.md) | ## Full API Docs | Doc | Description | |-----|-------------| -| [world](../api/world_en.md) | World state, events, particles, fireworks, sound | -| [entity](../api/entity_en.md) | Entity properties, AI, equipment, effects | -| [player](../api/player_en.md) | Inventory, messages, flight, teleport | -| [voxels](../api/voxels_en.md) | Block read/write, region fill | -| [storage](../api/storage_en.md) | JSON data persistence | -| [database](../api/database_en.md) | SQLite database | -| [http](../api/http_en.md) | HTTP network requests | -| [client](../api/client_en.md) | Client scripts (UI/input/chat/audio) | -| [registries](../api/registries_en.md) | Custom blocks/items/sounds | -| [math](../api/math_en.md) | GameVector3, Color, Quaternion | -| [commands](../api/commands_en.md) | `/box3script` command reference | +| [world](../api/world.md) | World state, events, particles, fireworks, sound | +| [entity](../api/entity.md) | Entity properties, AI, equipment, effects | +| [player](../api/player.md) | Inventory, messages, flight, teleport | +| [voxels](../api/voxels.md) | Block read/write, region fill | +| [storage](../api/storage.md) | JSON data persistence | +| [database](../api/database.md) | SQLite database | +| [http](../api/http.md) | HTTP network requests | +| [client](../api/client.md) | Client scripts (UI/input/chat/audio) | +| [registries](../api/registries.md) | Custom blocks/items/sounds | +| [math](../api/math.md) | GameVector3, Color, Quaternion | +| [commands](../api/commands.md) | `/box3script` command reference | diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/README.md b/Box3JS-NeoForge-1.21.1/docs/guide/README.md index 4647174..291190d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/README.md @@ -1,11 +1,10 @@ # Box3JS 指南 -从零开始了解 Box3JS——无论你是想快速上手还是深入原理。 - ## 按需求选择 | 我想... | 读这个 | |---------|--------| +| 了解 Box3JS 的起源和为什么要做 | [Box3JS 与神奇代码岛](about-box3js.md) | | 10 分钟写出第一个脚本 | [快速开始](getting-started.md) | | 看现成模板改一改就实现功能 | [常用配方](recipes.md) | | 理解 Box3JS 内部怎么运作 | [运行原理](architecture.md) | @@ -14,17 +13,17 @@ ## 学习路径 -``` -快速开始 运行原理 JS vs Java - │ │ │ - │ 环境搭建 │ Rhino 引擎 │ 优劣势对比 - │ 第一个脚本 │ 作用域隔离 │ 适用场景 - │ 开发循环 │ 事件回调机制 │ 决策树 - │ 调试部署 │ 构建管线 │ 混合方案 - │ │ 网络通信 │ - │ │ 沙盒 + 热重载 │ - └───────────┬───────────┴──────────────────────┘ - │ - ▼ - API 参考 + 教程 +```text +Box3JS 与神奇代码岛 → 快速开始 运行原理 JS vs Java + │ │ │ + │ 环境搭建 │ Rhino 引擎 │ 优劣势对比 + │ 第一个脚本 │ 作用域隔离 │ 适用场景 + │ 开发循环 │ 事件回调机制 │ 决策树 + │ 调试部署 │ 构建管线 │ 混合方案 + │ │ 网络通信 │ + │ │ 沙盒 + 热重载 │ + └───────────┬───────────┴──────────────────────┘ + │ + ▼ + API 参考 + 教程 ``` diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md deleted file mode 100644 index 8349aff..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/guide/README_en.md +++ /dev/null @@ -1,30 +0,0 @@ -# Box3JS Guides - -Start here whether you want to get your hands dirty quickly or dive deep into internals. - -## Pick Your Path - -| I want to... | Read this | -|-------------|----------| -| Write my first script in 10 minutes | [Quick Start](getting-started_en.md) | -| Use ready-made templates to implement features | [Common Recipes](recipes_en.md) | -| Understand how Box3JS works internally | [Architecture](architecture_en.md) | -| Decide between Box3JS and Java modding | [JS vs Java](js-vs-java_en.md) | -| Troubleshoot problems | [FAQ](faq_en.md) | - -## Learning Path - -``` -Quick Start Architecture JS vs Java - │ │ │ - │ Setup │ Rhino engine │ Pros & cons - │ First script │ Scope isolation │ Use case decision tree - │ Dev cycle │ Event callbacks │ Hybrid approach - │ Debug & deploy │ Build pipeline │ - │ │ Network comms │ - │ │ Sandbox + hot-reload │ - └──────────┬───────────────┴───────────────────────┘ - │ - ▼ - API Reference + Tutorials -``` diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md new file mode 100644 index 0000000..0752e76 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/guide/about-box3js.md @@ -0,0 +1,133 @@ +# Box3JS 与神奇代码岛 + +## 神奇代码岛是什么 + +[神奇代码岛](https://dao3.fun)(Box3)是一款**多人联机 3D 游戏创作平台**。在这里,用户不需要学游戏引擎,只要会 JavaScript,就能在浏览器里创建竞速、对战、RPG、FPS 甚至 MOBA 等各类 3D 游戏。 + +核心特点: + +- **JavaScript 编程** — 用 JS/TypeScript 写游戏逻辑,零游戏引擎基础 +- **多人实时联机** — 平台自带服务器、房间系统、网络同步 +- **跨端即玩** — 电脑、手机、平板均可,无需下载 +- **社区 UGC** — 所有游戏均由玩家创作。 + +神奇代码岛为成千上万的年轻创作者提供了第一次"写游戏"的体验。它的 API 设计经过了大量创作者长期验证,形成了一套简洁、直觉、高效的编程模型。 + +## Box3JS 是什么 + +Box3JS 是一个**社区驱动的 Minecraft 模组**,它在 Minecraft 服务端内部嵌入了一个完整的 JavaScript 引擎(Mozilla Rhino),让开发者用 TypeScript 编写服务端游戏逻辑,并可选择下发客户端脚本实现按键监听、屏幕 UI 等本地交互。 + +**Box3JS 不是神奇代码岛的官方产品**,而是由熟悉 Box3 生态的社区开发者创建,延续了神奇代码岛 API 的设计哲学和命名风格。 + +## 为什么要做 Box3JS MC 版 + +### 出发点 + +神奇代码岛的 API 经过了长期打磨,设计得非常出色: + +- 全局对象注入,不需要 `import`/`require` +- Tick 制定时器,与游戏世界完美同步 +- 统一的事件取消模式(`GameEventHandlerToken`) +- 项目作用域隔离,多脚本互不干扰 + +但神奇代码岛跑在自己的封闭平台里,创作者无法接触到 Minecraft 生态——那里有更大的玩家社区、更丰富的方块和机制、以及成熟的模组分发体系。 + +**Box3JS 的核心目标:把神奇代码岛级别的开发体验带进 Minecraft。** + +### 谁适合用 Box3JS + +| 用户画像 | 为什么适合 | +| -------------------------- | --------------------------------------------------- | +| 神奇代码岛开发者 | API 风格一致,已有技能直接复用,零学习成本 | +| 想给 MC 服务器写玩法的服主 | 不需要学 Java、Gradle、Mixin,写 JS 就行 | +| 编程教育场景 | TypeScript + 热重载 + 沙盒回滚 = 理想的编程教学环境 | +| 不想写 Java 模组的开发者 | 开箱即用的 API 覆盖常用功能,无编译管线负担 | + +## Box3JS 的独特优势 + +### 1. 延续 Box3 的 API 设计 + +如果你写过神奇代码岛的脚本,Box3JS 的代码你几乎能直接读懂: + +```js +// 这段代码在神奇代码岛和 Box3JS 中几乎一模一样 +world.onPlayerJoin((entity) => { + entity.player.directMessage(`§a欢迎 ${entity.player.name}!`); +}); + +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage("你好!"); + return false; + } + return true; +}); +``` + +API 对比详见 [Box3 API vs Box3JS 对比](../BOX3_API_COMPARISON.md)。 + +### 2. 真正的 Minecraft 世界 + +Box3JS 直接操作 Minecraft 的世界——真实的方块、原版实体、完整的游戏机制。你可以: + +- 操作真实 MC 方块(`voxels.setVoxel`、`voxels.fillVoxel`) +- 生成原版实体并设置 AI、装备、药水效果 +- 使用 Minecraft 的计分板、BossBar、队伍系统 +- 执行原版命令(`player.runCommand`) +- 控制天气、时间、世界边界、游戏规则 + +### 3. 热重载 + 沙盒 + +- **保存即生效** — 改代码 → `npm run build` → `/box3script reload`,不需要重启服务器 +- **沙盒保护** — 测试破坏性操作时开启沙盒,关闭时一键回滚所有修改 +- **自动热重载** — 开启 `watch` 后,保存构建产物自动触发 reload + +### 4. 独立分发 + +开发完成后,一键编译为独立 JAR 模组: + +```js +/box3script compile mygame +``` + +生成 `mygame-1.0.0.jar`,放入任意 NeoForge 1.21.1 服务端的 `mods/` 目录即可运行,无需源码,无需构建工具。可上传 CurseForge、Modrinth 分发。 + +### 5. 双端架构 + +```text +服务端(权威) 客户端(表现) +world.* / voxels.* client.* / input.* +entity.* / player.* ←→ ui.* / audio.* / gui.* +storage / db / http storage / db / http + └── remoteChannel ──────┘ +``` + +服务端控制游戏权威逻辑,客户端负责本地表现(UI、音效、按键监听),通过 `remoteChannel` 双向通信。 + +### 6. TypeScript 完整体验 + +- 完整的 `.d.ts` 类型声明,VS Code 智能提示 +- esbuild + Babel 构建管线,支持现代语法(`const`、箭头函数、模板字符串、`async/await`) +- ESLint 代码检查 +- `tsconfig.server.json` / `tsconfig.client.json` 互斥,防止 API 混用 + +## 与神奇代码岛的差异 + +Box3JS 并非 1:1 复制 Box3 的 API。差异源于两个平台的根本不同: + +| 差异领域 | 神奇代码岛 | Box3JS (MC) | +| ---------- | ------------------------------------------- | ---------------------------------------------- | +| 渲染引擎 | 自研 3D 引擎 | Minecraft 原版渲染 | +| 物理引擎 | 自研物理 | Minecraft 原版物理 | +| 天气系统 | 独立的雨/雪/雾系统(丰富的参数控制) | 复用 MC 原版天气(rainDensity/thunderDensity) | +| 光照系统 | 手动/自然光照模式(lightMode/sunFrequency) | 复用 MC 原版光照 | +| 自定义模型 | 内置编辑器 | 需 Resource Pack(MC 机制) | +| 数据库 | 内置 KV 存储 | 内置 KV 存储 + SQLite | + +**设计原则:** 尽量保持 API 命名和语义一致,但对于 MC 无法支持或与 MC 机制冲突的功能,不强行模拟。详细的 API 对照见 [Box3 API vs Box3JS 对比](../BOX3_API_COMPARISON.md)。 + +## 下一步 + +- **从零开始**: [快速开始指南](getting-started.md) — 10 分钟写出第一个 MC 脚本 +- **理解原理**: [运行原理](architecture.md) — Rhino 引擎、作用域隔离、构建管线 +- **API 速查**: [API 功能速查](../api/README.md) — 按"我想做什么"查找 API diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md b/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md index f8159ec..2e1288d 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/architecture.md @@ -1,25 +1,8 @@ # Box3JS 运行原理 -本文深入讲解 Box3JS 的内部架构:JS 引擎如何嵌入 Minecraft、作用域如何管理、构建管线如何工作、网络通信如何实现。 - -## 目录 - -1. [整体架构](#整体架构) -2. [Rhino 引擎](#rhino-引擎) -3. [作用域与隔离](#作用域与隔离) -4. [全局对象注入](#全局对象注入) -5. [事件回调机制](#事件回调机制) -6. [构建管线](#构建管线) -7. [网络通信](#网络通信) -8. [沙盒系统](#沙盒系统) -9. [文件监控与热重载](#文件监控与热重载) -10. [编译发布模式](#编译发布模式) - ---- - ## 整体架构 -``` +```text ┌──────────────────────────┐ │ Minecraft Server │ │ (NeoForge) │ @@ -57,7 +40,7 @@ ### 关键包结构 -``` +```text com.box3lab.box3js ├── Box3JS.java ← @Mod 入口 ├── script/ ← 服务端引擎 @@ -83,8 +66,6 @@ com.box3lab.box3js └── ... ``` ---- - ## Rhino 引擎 ### 为什么是 Rhino @@ -117,8 +98,8 @@ scope.put("storage", scope, storageApi); // 2. 初始化 console JS 代码 cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); -// 3. 加载用户脚本 -cx.evaluateReader(scope, scriptReader, "app.js", 1, null); +// 3. 加载服务端入口脚本 +cx.evaluateReader(scope, scriptReader, "server.js", 1, null); ``` ### 类型桥接 @@ -136,13 +117,11 @@ Java 对象暴露给 JS 时,Rhino 自动处理类型转换: Box3JS 返回的多是 **Java 原生对象**(如 `ServerPlayer` 包装器),JS 侧可直接调用方法。复杂的返回值(如 `querySelectorAll`)返回 Java `List`,Rhino 映射为 JS 数组。 ---- - ## 作用域与隔离 ### 每个项目独立作用域 -``` +```text Rhino Context │ ┌──────────────┼──────────────┐ @@ -169,13 +148,11 @@ Box3JS 返回的多是 **Java 原生对象**(如 `ServerPlayer` 包装器) 3. 如果沙盒开启,回滚所有方块和实体修改 4. 释放 Rhino scope,GC 回收 ---- - ## 全局对象注入 ### 注入流程 -``` +```text Box3ScriptEngine.setupScope(scope) │ ├── scope.put("world", scope, new Box3JSWorld(...)) @@ -197,7 +174,7 @@ Box3ScriptEngine.setupScope(scope) ### 客户端注入 -``` +```text Box3JSClientEngine.init(scope) │ ├── scope.put("audio", scope, audioObj) @@ -231,13 +208,11 @@ console = { `.apply()` 确保多个参数正确传递给 Java varargs 方法。 ---- - ## 事件回调机制 ### 完整链路 -``` +```text Minecraft 事件发生 │ ▼ @@ -304,11 +279,9 @@ public class GameEventHandlerToken { } ``` ---- - ## 构建管线 -``` +```text src/server/app.ts (TypeScript + ES2020 语法) │ ▼ @@ -364,13 +337,11 @@ await build({ }); ``` ---- - ## 网络通信 ### remoteChannel 架构 -``` +```text ┌──────────────────────┐ ┌──────────────────────┐ │ Server (Java) │ │ Client (Java) │ │ │ │ │ @@ -394,7 +365,7 @@ await build({ ### 数据流 **服务端 → 客户端:** -``` +```text JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) → Box3JSRemoteChannel.java → JSON.stringify(eventData) @@ -407,7 +378,7 @@ JS: remoteChannel.sendClientEvent(player, { type: "boss_bar", hp: 50 }) ``` **客户端 → 服务端:** -``` +```text JS: remoteChannel.sendServerEvent({ key: "space" }) → Box3JSClientEngine → JSON.stringify @@ -425,13 +396,11 @@ JS: remoteChannel.sendServerEvent({ key: "space" }) - 数组 `[1, 2, 3]` - 不支持:函数、`GameVector3` 实例、Java 对象 ---- - ## 沙盒系统 ### 工作原理 -``` +```text /box3script sandbox mygame ← 开启沙盒 │ ▼ @@ -470,13 +439,11 @@ class SandboxTracker { - **玩家测试** — 让玩家试玩新功能,结束时回滚不影响正式服 - **调试** — 测试有破坏性的操作(explode、fillVoxel) ---- - ## 文件监控与热重载 ### 工作流程 -``` +```text /box3script watch ← 开启文件监控 │ ▼ @@ -501,13 +468,11 @@ Box3ScriptWatcher 启动 - 300ms 防抖避免 esbuild 写入多个 chunk 时多次 reload - reload 是原子的:先停止旧脚本(清理回调 + 资源),再加载新脚本 ---- - ## 编译发布模式 ### `/box3script compile` 流程 -``` +```text 输入: config/box3/script/mygame/ │ ▼ @@ -568,8 +533,6 @@ public static final DeferredBlock RUBY_BLOCK = **注意:** `registries` 只在编译 JAR 模式下可用。解释模式(`/box3script start`)中 `registries` 为 `undefined`。 ---- - ## 性能考虑 ### 开销来源 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/faq.md b/Box3JS-NeoForge-1.21.1/docs/guide/faq.md index ce07ea4..412a983 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/faq.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/faq.md @@ -1,12 +1,11 @@ # 常见问题与故障排查 -Box3JS 开发中经常遇到的问题及解决方案。 - ## 脚本加载 ### Q: 脚本不执行,`/box3script` 显示项目为 ○(未加载) 检查顺序: + 1. `npm run build` 是否成功?`dist/` 下是否生成了 `server.js`? 2. `/box3script start <项目名>` 是否执行过? 3. 服务端控制台是否有 `[Box3JS]` 前缀的错误日志? @@ -44,6 +43,7 @@ npm install ### Q: TypeScript 报类型错误但运行正常 TypeScript 只检查构建时类型,实际运行时 Rhino 不会做类型检查。修复步骤: + 1. 检查 `.d.ts` 中的 API 签名是否正确(`types/server/` 和 `types/client/`) 2. 如果类型确实不对,可以加 `// @ts-expect-error` 临时跳过 3. 同时考虑修复 `.d.ts` 文件 @@ -51,6 +51,7 @@ TypeScript 只检查构建时类型,实际运行时 Rhino 不会做类型检 ### Q: `npm run build` 成功但脚本报语法错误 Babel 编译为 ES5 后,目标引擎是 Rhino 1.9.1(仅支持 ES5)。常见问题: + - 不要在 `src/` 中使用 `async/await`(Babel 不会完整编译为 ES5) - 不要使用 `Promise`(Rhino 1.9.1 不支持) - `let`/`const`、`=>` 箭头函数、模板字符串由 Babel 处理,可以放心使用 @@ -73,6 +74,7 @@ Babel 编译为 ES5 后,目标引擎是 Rhino 1.9.1(仅支持 ES5)。常 ### Q: API 报 "xxx is not a function" 先确认: + 1. 方法名拼写是否正确?参考 [API 文档](../api/README.md) 2. 所在全局对象是否正确?如 `world.say()` 不是 `server.say()` 3. 是否需要 `new`?如 `new GameVector3(x, y, z)` @@ -81,6 +83,7 @@ Babel 编译为 ES5 后,目标引擎是 Rhino 1.9.1(仅支持 ES5)。常 ### Q: 脚本执行很慢/服务器卡顿 Rhino 是解释型引擎(无 JIT),需要优化: + - **不在 onTick 中做密集操作** — 用 `setInterval` 降低频率 - **缓存查询结果** — 不要每到 tick 都调用 `querySelectorAll` - **减少 JS ↔ Java 交互** — 批处理比逐个调用快 @@ -106,6 +109,7 @@ Rhino 是解释型引擎(无 JIT),需要优化: ### Q: 如何防止 SQL 注入? 用参数化查询(推荐): + ```js // ✅ 安全:参数化 db.sql("SELECT * FROM t WHERE name = ?", userInput); @@ -143,19 +147,12 @@ db.sql("SELECT * FROM t WHERE name = '" + userInput + "'"); ### Q: 如何检测玩家是否安装了 Box3JS 客户端? -```js -// 服务端检测 -if (entity.player.hasBox3JSClientMod()) { - // 可以发送客户端事件 -} else { - // 降级到聊天消息 - entity.player.directMessage("安装 Box3JS 客户端获得完整体验"); -} -``` +无需手动检测。`remoteChannel.sendClientEvent()` 使用可选数据包(optional payload),未安装 Box3JS 客户端的玩家会自动忽略这些数据包,不会产生任何错误或断线。可以直接发送,安全无副作用。 ### Q: 客户端和服务端可以同时使用 `remoteChannel` 吗? 可以。`remoteChannel` 提供双向通道: + - 客户端 → 服务端:`remoteChannel.sendServerEvent()` → `remoteChannel.onServerEvent()` - 服务端 → 客户端(定向):`remoteChannel.sendClientEvent(entity, ...)` → `remoteChannel.onClientEvent()` - 服务端 → 客户端(广播):`remoteChannel.broadcastClientEvent(...)` → `remoteChannel.onClientEvent()` @@ -170,7 +167,7 @@ if (entity.player.hasBox3JSClientMod()) { ### Q: 如何发布我的脚本? -``` +```js /box3script compile <项目名> ``` @@ -178,17 +175,15 @@ if (entity.player.hasBox3JSClientMod()) { ### Q: 编译的 JAR 和解释模式有什么区别? -| 特性 | 解释模式 | 编译 JAR | -|------|---------|---------| -| registries | 不可用 | 可用(自定义方块/物品/音效) | -| 热重载 | ✅ | ❌(需重启) | -| 分发 | 复制整个项目目录 | 单个 JAR 文件 | -| 更新 | 直接编辑 JS 文件 | 重新编译 | +| 特性 | 解释模式 | 编译 JAR | +| ---------- | ---------------- | ---------------------------- | +| registries | 不可用 | 可用(自定义方块/物品/音效) | +| 热重载 | ✅ | ❌(需重启) | +| 分发 | 复制整个项目目录 | 单个 JAR 文件 | +| 更新 | 直接编辑 JS 文件 | 重新编译 | ### Q: registries 为什么只在编译模式可用? 自定义方块/物品/音效需要 NeoForge 的 `DeferredRegister`,这必须在模组启动时注册。解释模式没有启动期注册阶段,所以 `registries` 只能在编译为 JAR 时使用。 ---- - 更多问题请在 [GitHub Issues](https://github.com/box3lab/Box3JS) 提出。 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md index 00bab8c..8511dce 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started.md @@ -1,64 +1,35 @@ # 快速开始:从零到第一个 Box3JS 脚本 -本指南面向**零模组开发经验**的读者。你只需要会 JavaScript,就能在 10 分钟内写出第一个 Minecraft 服务端脚本。 - -## 目录 - -1. [Box3JS 是什么](#box3js-是什么) -2. [环境搭建](#环境搭建) -3. [创建项目](#创建项目) -4. [第一个脚本](#第一个脚本) -5. [开发循环](#开发循环) -6. [调试技巧](#调试技巧) -7. [发布部署](#发布部署) -8. [下一步](#下一步) - ---- +面向**零模组开发经验**的读者,只需会 JavaScript 即可。 ## Box3JS 是什么 -Box3JS 是一个**服务端脚本引擎模组**(NeoForge 1.21.1)。它在 Minecraft 服务器内嵌入了一个 JavaScript 运行时(Mozilla Rhino),让你用 JS/TypeScript 编写游戏玩法逻辑。 - -### 能做什么 +Box3JS 是一个 **Minecraft 模组**,在服务器内嵌入 JavaScript 运行时,让你用 JS/TypeScript 编写游戏玩法。客户端脚本可选下发,实现按键监听、屏幕 UI 等本地交互。 -| 类别 | 示例 | -|------|------| -| 聊天命令 | `!heal`、`!home`、`!shop` | -| 事件响应 | 玩家进服欢迎、死亡惩罚、方块破坏记录 | -| 实体控制 | 生成怪物、设置 AI、自定义 Boss | -| 小游戏 | PvP 竞技场、跑酷、波次刷怪 | -| 世界操作 | 放置/替换方块、填充区域、修改天气时间 | -| 数据持久化 | JSON 存储、SQLite 数据库 | -| 游戏系统 | 计分板、BossBar、队伍、世界边界 | -| HTTP 请求 | 查询 Web API、Webhook 通知 | -| 客户端脚本 | 按键监听、屏幕 UI、客户端音效 | +Box3JS 的 API 设计继承自**[神奇代码岛](https://dao3.fun)(Box3)**——一款浏览器端的多人 3D 游戏创作平台,成千上万的创作者在上面用 JS 写游戏。神奇代码岛的 API 经过长期社区验证,简洁直观。Box3JS 把这套 API 带到了 Minecraft,让你用同一种编程范式在 MC 里构建小游戏、自定义玩法。 -### 不能做什么 +::: tip 更多背景 +→ [Box3JS 与神奇代码岛](about-box3js.md) +::: -- **渲染自定义模型/粒子** — 需要客户端资源包或 Java 模组 -- **添加新方块/物品(实时)** — 需要编译为 JAR 模组(`/box3script compile`) -- **修改原版机制** — 如修改合成表、生物行为,这些需要 Mixin +### 核心架构一览 -### 核心设计理念 - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ TypeScript │ ───→ │ Babel ES5 │ ───→ │ Rhino 引擎 │ -│ 源码 │ │ 编译 │ │ (JVM 内嵌) │ -└──────────────┘ └──────────────┘ └──────────────┘ - │ - ┌───────┴───────┐ - │ Minecraft │ - │ NeoForge API │ - └───────────────┘ +```text +你在 VS Code 里写 构建工具帮你 Minecraft 帮你跑 + TypeScript ───→ 编译成 ES5 JS ───→ Rhino 引擎执行 + │ + ┌──────┴──────┐ + │ NeoForge │ + │ Minecraft │ + │ API 层 │ + └─────────────┘ ``` -- **运行在服务端 JVM 内**,直接调用 Minecraft 和 NeoForge API -- **TypeScript 源码**通过 Babel 编译为 ES5,适配 Rhino 引擎 -- **热重载** — 修改代码后不需要重启服务器 -- **沙盒隔离** — 每个项目独立作用域,互不影响 - ---- +- **你写 TypeScript**,享受类型提示和现代语法 +- **Babel 编译**为 ES5(因为 Rhino 引擎只支持 ES5) +- **esbuild 打包**为单个 JS 文件 +- **Rhino 在 JVM 内执行**,直接调用 Minecraft API +- **服务端 + 客户端双端运行**,通过 `remoteChannel` 通信 ## 环境搭建 @@ -66,69 +37,78 @@ Box3JS 是一个**服务端脚本引擎模组**(NeoForge 1.21.1)。它在 Mi 1. **Minecraft 服务端** 安装了 Box3JS + NeoForge 1.21.1 2. **Node.js** 18+ (仅用于本地构建,服务端不需要) -3. 一个文本编辑器(VS Code 推荐) +3. 一个文本编辑器(VS Code 推荐,有完整的 TypeScript 智能提示) ### 验证安装 进入游戏,执行: -``` +```js /box3script ``` 如果看到项目状态面板,说明 Box3JS 已正常运行。 -``` +```text ══ Box3JS Script Engine ══ Watch: ○ Inactive Sandbox: ○ Inactive Projects: 0 enabled | 0 loaded ``` ---- - ## 创建项目 +### 一键创建 + 在游戏内执行: -``` +```js /box3script create mygame ``` -这会在 `config/box3/script/mygame/` 生成完整的 TypeScript 项目: +这会在 `config/box3/script/mygame/` 生成完整的 TypeScript 项目。 -``` +### 理解项目结构 + +```text config/box3/script/mygame/ ├── package.json ← 项目配置(名称、版本、构建依赖) ├── tsconfig.base.json ← TypeScript 公共编译选项 -├── tsconfig.server.json ← 服务端 TS 配置 -├── tsconfig.client.json ← 客户端 TS 配置 +├── tsconfig.server.json ← 服务端 TS 配置(引用 server/ 类型) +├── tsconfig.client.json ← 客户端 TS 配置(引用 client/ 类型) ├── build.mjs ← 构建脚本(esbuild + Babel) ├── eslint.config.mjs ← ESLint 规则 -├── types/ +├── types/ ← ★ 类型声明文件(API 的说明书) │ ├── shared.d.ts ← 服务端&客户端共享类型 │ ├── server/ │ │ ├── index.d.ts ← 服务端类型入口 -│ │ ├── server.d.ts -│ │ ├── entity.d.ts -│ │ ├── player.d.ts -│ │ ├── world.d.ts -│ │ └── voxels.d.ts +│ │ ├── server.d.ts ← world, remoteChannel, registries +│ │ ├── entity.d.ts ← GameEntity 接口 +│ │ ├── player.d.ts ← GamePlayer 接口 +│ │ ├── world.d.ts ← GameWorld 接口 +│ │ └── voxels.d.ts ← GameVoxels 接口 │ └── client/ │ ├── index.d.ts ← 客户端类型入口 -│ ├── client.d.ts -│ ├── audio.d.ts -│ ├── input.d.ts -│ ├── ui.d.ts -│ ├── chat.d.ts -│ └── gui.d.ts +│ ├── client.d.ts ← GameClient, RemoteChannel +│ ├── audio.d.ts ← GameAudio +│ ├── input.d.ts ← GameInput +│ ├── ui.d.ts ← GameUI +│ ├── chat.d.ts ← GameChat +│ └── gui.d.ts ← GameGUI, GuiController ├── src/ │ ├── server/ │ │ └── app.ts ← ★ 服务端入口(你写代码的地方) │ └── client/ │ └── app.ts ← 客户端入口 -└── registries/ ← 自定义内容(方块/物品/音效 JSON) +├── registries/ ← 自定义内容(方块/物品/音效 JSON) +└── assets/lang/ ← 自定义内容本地化文本 ``` +**关键理解:** + +- `types/` 下的 `.d.ts` 文件是 API 的说明书 — VS Code 靠它们提供智能提示 +- `tsconfig.server.json` 和 `tsconfig.client.json` 是**互斥的** — 服务端代码中不会出现 `client`、`input` 等客户端全局对象 +- 你只需要在 `src/server/app.ts` 里写代码,构建工具处理剩下的一切 + ### 安装依赖 打开终端,进入项目目录: @@ -140,69 +120,671 @@ npm install `npm install` 只需执行一次(安装 esbuild、Babel、TypeScript 等构建工具)。 ---- +## 第一个脚本:逐行详解 -## 第一个脚本 +打开 `src/server/app.ts`,清空已有内容,写入以下代码。我们来逐行理解。 -打开 `src/server/app.ts`,清空已有内容,写入: +### 1. 启动日志 ```js -// 1. 启动时输出日志 console.log("MyGame 脚本已启动!"); +``` + +**发生了什么:** `console` 是 Box3JS 注入的全局对象(不需要 `import`)。它背后是一个 Java 类 `Box3JSConsole`,通过 Rhino 桥接到 JS。`console` 支持 `log`、`warn`、`error`、`debug`、`clear`、`assert` 六个方法。 -// 2. 玩家加入时欢迎 +### 2. 玩家加入欢迎 + +```js world.onPlayerJoin((entity) => { const p = entity.player; - p.directMessage("§a欢迎 " + p.name + " 来到服务器!"); + + // 全服广播 + world.say(`§e${p.name} §7加入了服务器`); + + // 私密消息 + p.directMessage(`§a欢迎来到服务器,${p.name}!`); // 粒子欢迎特效 - const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + const { position: pos } = p; + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); +``` + +**逐行理解:** + +- `world.onPlayerJoin(...)` — 注册一个"玩家加入"事件监听器。返回 `GameEventHandlerToken`(本例中没有保存,意味着这个监听器在脚本重载前一直有效)。 +- `entity.player` — `entity` 是回调参数,代表加入的实体。`.player` 获取该实体的玩家包装对象(如果实体不是玩家,`.player` 为 `undefined`)。 +- `p.directMessage(...)` — 发送私密消息,只有该玩家能看到。 +- `§a` 是 Minecraft 颜色代码(绿色)。`§e` = 黄色,`§6` = 金色,`§7` = 灰色,`§c` = 红色。 +- `p.position` — 返回 `GameVector3` 对象,包含 `.x`、`.y`、`.z` 属性。 +- `world.spawnParticleCircle(...)` — 在玩家位置生成一圈粒子效果。 +- `world.playSound(...)` — 在玩家位置播放音效。 + +### 3. 聊天命令 -// 3. 聊天命令 +```js world.onChat((entity, message) => { const p = entity.player; if (message === "!hello") { - p.directMessage("§e你好," + p.name + "!"); + p.directMessage(`§e你好,${p.name}!`); return false; // 阻止消息显示在聊天栏 } if (message === "!pos") { - const pos = p.position; - p.directMessage("§e你的坐标: §f" + - Math.floor(pos.x) + ", " + - Math.floor(pos.y) + ", " + - Math.floor(pos.z)); + const { position: pos2 } = p; + p.directMessage( + `§e你的坐标: §f${Math.floor(pos2.x)}, ${Math.floor(pos2.y)}, ${Math.floor(pos2.z)}`, + ); + return false; + } + + if (message === "!help") { + p.directMessage("§e可用命令: !help, !hello, !pos, !home, !shop"); return false; } return true; // 不是命令的消息正常发送 }); +``` + +**关键概念:** + +- `return false` — 阻止事件继续(消息不会广播到聊天栏)。这是 Box3JS 事件的通用约定:**返回 false 阻断默认行为**。 +- `return true` — 放行,消息正常广播。 +- `Math.floor()` — 常规 JS,坐标向下取整,更易读。 + +### 4. 定时公告 -// 4. 定时公告 -world.setInterval(() => { +```js +setInterval(() => { const count = world.querySelectorAll("*").length; - world.say("§7[公告] §f当前在线: " + count + " 人"); + world.say(`§7[公告] §f当前在线: ${String(count)} 人`); }, 6000); // 6000 ticks = 5 分钟 ``` -### 关键概念 +**关键理解:** -- **全局对象不需要 import** — `world`、`console`、`player` 等由 Box3JS 注入 -- **事件回调返回 false 阻止默认行为** — `onChat` 返回 false 阻止消息广播 -- **Tick 是 MC 的时间单位** — 1 秒 = 20 ticks,`setInterval` 参数是 ticks -- **§ 是 MC 颜色代码** — `§a` = 绿色, `§e` = 黄色, `§6` = 金色, `§7` = 灰色 +- `setInterval` 是**全局函数**(不是 `world.setInterval`),和浏览器/Node.js 一致。 +- 第二个参数是 **ticks**(Minecraft 时间单位),不是毫秒。1 秒 = 20 ticks。 +- `setInterval` 返回 `GameEventHandlerToken`,可以调用 `.cancel()` 取消。 +- `world.querySelectorAll("*")` 返回所有在线实体列表。 +- `world.say(...)` 向全服广播消息。 ---- +### 完整的 Tick 换算表 -## 开发循环 +| 时长 | Ticks | +| ------- | ------ | +| 1 秒 | 20 | +| 5 秒 | 100 | +| 30 秒 | 600 | +| 1 分钟 | 1,200 | +| 5 分钟 | 6,000 | +| 10 分钟 | 12,000 | +| 30 分钟 | 36,000 | + +## 核心设计理念:为什么这样设计 API + +理解 Box3JS API 的设计理念,能让你写出更高效、更安全的脚本。以下是最重要的几个设计决策及其原因。 + +### 设计 1:全局对象注入,不需要 import + +```js +// Box3JS 中直接使用,不需要 import +world.onTick(() => { ... }); +console.log("hello"); +storage.getDataStorage("coins"); + +// 对比:如果在 Node.js 中 +// const { world } = require("box3js"); ← 不需要! +``` + +**为什么?** Rhino 是一个裸的 ECMAScript 引擎,不支持 CommonJS `require()` 或 ES Module `import`。Box3JS 通过 Java 层在 Rhino 作用域初始化时,直接把所有 API 对象注入为全局变量。TypeScript 的 `.d.ts` 文件用 `declare` 声明这些全局对象,让你在 VS Code 中获得完整的类型提示。 + +### 设计 2:Tick 制时间,不是毫秒 + +```js +// Box3JS 的时间单位是 tick(1/20 秒) +setTimeout(() => { ... }, 100); // 100 ticks = 5 秒后 + +// 对比浏览器: +// setTimeout(() => { ... }, 5000); // 5000 毫秒 = 5 秒后 +``` + +**为什么?** Box3JS 的定时器是**在主线程的游戏 Tick 循环中**执行的,不创建任何 Java 线程。每次游戏 Tick(1/20 秒),引擎检查所有定时器,递减剩余 tick 数,到 0 时触发回调。这样做的好处是: + +- **线程安全** — 回调总是在主线程执行,你可以安全地调用任何 Minecraft API +- **精确同步** — 定时器与游戏世界完全同步,不会出现"服务器卡了但定时器还在走"的情况 +- **零开销** — 不创建额外的线程或线程池 + +### 设计 3:GameEventHandlerToken — 统一的取消模式 + +```js +// 所有 onXxx() 和 setTimeout/setInterval 都返回 GameEventHandlerToken +const token = world.onTick(() => { + // 每 tick 执行 +}); + +// 取消监听(两种方式等效) +token.cancel(); + +// 检查是否仍活跃 +if (token.active()) { + // ... +} +``` + +**为什么?** 早期设计为每种事件提供独立的取消方法(如 `removeTickListener`、`removeChatListener`),但这样会导致: + +1. 需要记住每种事件的取消 API 名称 +2. 无法统一管理(你想在脚本停止时批量取消怎么办?) + +统一返回 `GameEventHandlerToken` 后: + +- **一个模式适用所有** — 不管是 `onTick`、`onPlayerJoin`、`onChat` 还是 `setInterval`,都用 `.cancel()` +- **脚本重载自动清理** — 停止项目时,引擎遍历所有 token 批量取消,无遗漏 +- **链式管理** — 你可以把多个 token 放进数组,统一 `.cancel()` + +### 设计 4:项目作用域隔离 + +```text +服务端同时运行 3 个脚本项目,互不影响: + +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Scope A │ │ Scope B │ │ Scope C │ +│ "mygame" │ │ "lobby" │ │ "survival" │ +│ │ │ │ │ │ +│ var x = 1 │ │ var x = 2 │ │ var x = 3 │ +│ 自己的事件 │ │ 自己的事件 │ │ 自己的事件 │ +│ 自己的存储 │ │ 自己的存储 │ │ 自己的存储 │ +│ 自己的定时器 │ │ 自己的定时器 │ │ 自己的定时器 │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +**为什么?** 一个服务器可能同时运行多个脚本(大厅系统、小游戏、经济系统……)。如果没有隔离: + +- 变量名冲突(两个脚本都定义了 `var playerCount`) +- 事件回调互相干扰(`/box3script reload lobby` 意外清除了 survival 的回调) +- 数据泄露(lobby 的脚本读到了 survival 的存储数据) + +Box3JS 给每个项目分配**独立的 Rhino 顶级作用域**,存储在独立的 `Box3JSEventBus` 命名空间中。停止或重载一个项目完全不影响其他项目。 + +### 设计 5:双端架构 + remoteChannel + +```text +┌──────────────────────┐ ┌──────────────────────┐ +│ 服务端 (Server) │ │ 客户端 (Client) │ +│ │ │ │ +│ world.* │ │ client.* │ +│ voxels.* │ JSON │ audio.* │ +│ entity.* │ ←─────→ │ input.* │ +│ player.* │ 事件 │ ui.* │ +│ storage/db/http │ │ chat.* / gui.* │ +│ remoteChannel ──────┼─────────┼── remoteChannel │ +│ │ │ storage/db/http │ +└──────────────────────┘ └──────────────────────┘ +``` + +**为什么分开?** + +- **安全性** — 服务端是世界权威(方块、实体、数据),客户端只能操作本地表现(UI、音效、输入) +- **性能** — 客户端脚本运行在玩家自己的电脑上,不消耗服务器资源 +- **灵活性** — 你可以只写服务端脚本(大多数场景),或增加客户端脚本来提升体验 + +**remoteChannel 通信规则:** + +```js +// 服务端 → 单个客户端 +remoteChannel.sendClientEvent(player, { type: "welcome", msg: "hi" }); + +// 服务端 → 所有客户端 +remoteChannel.broadcastClientEvent({ type: "game_start" }); + +// 客户端 → 服务端 +remoteChannel.sendServerEvent({ key: "space", pressed: true }); +``` + +**重要限制:** 跨网络传输的数据必须是 **JSON 可序列化**的。不能传函数、`GameVector3` 实例、Java 对象。如果需要传坐标,用 `{ x: 1, y: 2, z: 3 }` 而不是 `new GameVector3(1, 2, 3)`。 + +### 设计 6:TypeScript 源码 + Babel 编译为 ES5 + +```text +src/server/app.ts Babel esbuild dist/server.js +(TypeScript, ES2020) ───→ ES5 JavaScript ───→ bundle ───→ (一个文件) +``` + +**为什么需要构建步骤?** + +- **Rhino 1.9.1 只支持 ES5** — `let`、`const`、箭头函数、模板字符串、`class` 都是 ES6+ 语法,Rhino 不认识 +- **Babel 负责降级** — 把现代语法转成 `var`、`function`、字符串拼接等 ES5 写法 +- **esbuild 负责打包** — 虽然你可以写多个 `.ts` 文件,但 Rhino 没有模块系统。esbuild 把所有文件合并为一个 IIFE + +**你可以放心在源码中用:** + +- `const` / `let`(转为 `var`) +- 箭头函数 `() => {}`(转为 `function(){}`) +- 模板字符串 `` `hello ${name}` ``(转为 `"hello " + name`) +- `class`(转为 `function` + prototype) +- `async/await`(通过 regenerator 转换) + +### 设计 7:事件回调返回 false 阻断 + +```js +world.onChat((entity, message) => { + if (message.startsWith("!")) { + // 这是命令,不要广播 + return false; + } + return true; // 正常消息放行 +}); +``` + +**为什么?** 借鉴了浏览器 DOM 事件的 `preventDefault` 模式。Minecraft 事件通常有"默认行为"(如聊天消息广播给所有人)。`return false` 告诉引擎:"我已经处理了这个事件,不要执行默认行为"。 + +### 设计 8:沙盒模式 — 安全测试 + +```js +/box3script sandbox mygame # 开启沙盒 +# ... 测试脚本(生成实体、修改方块、爆炸)... +/box3script sandbox mygame # 关闭 → 自动回滚所有修改 +``` + +**为什么?** 一旦脚本修改了世界,这些修改是永久性的(方块被替换、实体被生成)。沙盒模式追踪脚本对世界的所有修改,关闭时自动回滚。这让开发者可以大胆测试破坏性操作,不用担心搞坏正式服。 + +## API 实战速览 + +以下按"我想做什么"组织,覆盖最常用的 API。完整的 API 参考见 [API 文档](../api/README.md)。 + +### 消息与聊天 + +```js +// 全服广播 +world.say("§6服务器将在 5 分钟后重启"); + +// 私密消息(只有目标玩家看到) +player.directMessage("§a你的余额: 100 金币"); + +// 快捷栏上方文字 +player.actionBar("§e按 F 键打开菜单"); + +// 屏幕中央大标题 +player.title("§6BOSS 战", "§c远古巨龙 已苏醒"); + +// 拦截聊天(做命令系统) +world.onChat((entity, message) => { + if (message === "!help") { + entity.player.directMessage("§e可用命令: !help, !home, !shop"); + return false; + } + return true; +}); +``` + +### 玩家属性与控制 + +```js +// 获取玩家信息 +const name = player.name; +const pos = player.position; // GameVector3 { x, y, z } +const hp = player.hp; +const mode = player.gameMode; // "survival", "creative", "adventure", "spectator" + +// 修改玩家状态 +player.hp = 20; // 回满血 +player.maxHp = 40; // 增加最大生命值 +player.food = 20; // 回满饱食度 +player.gameMode = "creative"; // 切换创造模式 +player.canFly = true; // 允许飞行 +player.flying = true; // 开始飞行 + +// 传送 +player.teleport(new GameVector3(100, 64, 100)); + +// 踢出 +player.kick("你已被管理员踢出"); + +// 以玩家身份执行原版命令 +player.runCommand("effect give @s minecraft:speed 30 1"); +``` -每次修改代码后的标准流程: +### 物品与背包 +```js +// 给物品 +player.giveItem("minecraft:diamond", 64); +player.giveItem("minecraft:diamond_sword", 1); + +// 给带名称的物品(第 4 个参数为描述文字数组) +player.giveNamedItem("minecraft:stick", 1, "§6魔法棒", ["右键使用"]); + +// 给带附魔的物品(附魔为 { 附魔ID: 等级 } 对象) +player.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:unbreaking": 3, +}); + +// 查询手持物品 +const held = player.getHeldItem(); + +// 清空背包 +player.clearInventory(); ``` + +### 事件系统 + +```js +// 每 tick(谨慎使用!每 tick 中的重操作会拖慢服务器) +const tickToken = world.onTick(() => { + // 每 tick 执行 +}); + +// 玩家事件 +world.onPlayerJoin((entity) => { + entity.player.directMessage("欢迎!"); +}); + +world.onPlayerLeave((entity, _tick) => { + world.say(`${entity.player.name} 离开了服务器`); +}); + +world.onPlayerRespawn((entity, _tick) => { + entity.player.teleport(new GameVector3(0, 100, 0)); + entity.player.directMessage("你重生了!"); +}); + +// 实体事件 +world.onEntityDeath((entity, _killer, _tick) => { + if (entity.isPlayer()) { + world.say(`${entity.player.name} 死了`); + } +}); + +world.onEntityDamage((entity, amount, source, _attacker, _tick) => { + if (amount > 10) { + console.log(`高额伤害: ${String(amount)} 来源: ${source}`); + } +}); + +// 交互事件 +world.onInteract((entity, target, _tick) => { + // 玩家右键实体 + if (target.hasTag("npc")) { + entity.player.directMessage("你好!"); + } +}); + +world.onBlockActivate((entity, x, y, z, voxel, _tick) => { + // 玩家右键方块 + if (voxel === "minecraft:chest") { + entity.player.directMessage( + `你点击了位于 ${String(x)}, ${String(y)}, ${String(z)} 的箱子`, + ); + } +}); + +world.onBlockPlace((entity, x, y, z, voxel, _voxelId, _tick) => { + // 玩家放置方块 + console.log( + `${entity.player.name} 在 ${String(x)}, ${String(y)}, ${String(z)} 放置了 ${voxel}`, + ); +}); + +world.onVoxelDestroy((entity, _x, _y, _z, voxel, _tick) => { + // 玩家破坏方块 + if (voxel === "minecraft:diamond_block") { + entity.player.directMessage("§c不能破坏钻石块!"); + return false; // 阻止破坏 + } +}); + +// 定时器(全局函数) +const timer = setTimeout(() => { + world.say("30 秒到了!"); +}, 600); // 600 ticks = 30 秒 + +const interval = setInterval(() => { + world.say("每分钟公告"); +}, 1200); // 1200 ticks = 1 分钟 + +// 取消定时器 +timer.cancel(); +interval.cancel(); +``` + +### 实体操控 + +```js +// 生成实体(返回 GameEntity | null) +const zombie = world.spawnEntity( + "minecraft:zombie", + new GameVector3(100, 64, 100), +); +if (zombie) { + // 使用 zombie ... +} + +// 带完整配置创建实体(nameTag/glowing/equipment 需创建后设置) +const boss = world.createEntity({ + type: "minecraft:zombie", + position: new GameVector3(100, 64, 100), + hp: 200, + maxHp: 200, + tags: ["boss"], +}); +if (boss) { + boss.setNameTag("§c远古僵尸王"); + boss.glowing = true; + boss.setEquipment("head", "minecraft:diamond_helmet"); + boss.setEquipment("chest", "minecraft:diamond_chestplate"); + + // 操控实体 + boss.setAI(false); // 关闭 AI(原地不动) + boss.invulnerable = true; // 无敌 + boss.navigateTo(110, 64, 100, 1.5); // 导航到目标位置 + + // 药水效果 + boss.addEffect("minecraft:strength", 600, 2, false); + boss.addEffect("minecraft:speed", 600, 1, true); + boss.clearEffects(); + + // 装备 + boss.setEquipment("head", "minecraft:iron_helmet"); + boss.setEquipment("mainhand", "minecraft:iron_sword"); + + // 标签(用于标记和查询) + boss.addTag("boss"); + boss.addTag("stage_1"); + boss.hasTag("boss"); // → true +} + +// 查询实体 +const nearby = world.entitiesInRadius(pos, 10); // 半径 10 格内 +const all = world.querySelectorAll("*"); // 所有实体 +const players = world.querySelectorAll("player"); // 所有玩家 +const monsters = world.querySelectorAll("monster"); // 所有怪物 +``` + +### 方块操作 + +```js +// 读取方块 +const block = voxels.getVoxel(100, 64, 100); + +// 放置方块 +voxels.setVoxel(100, 64, 100, "minecraft:stone"); +voxels.setVoxel(100, 65, 100, "minecraft:torch"); + +// 区域填充 +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:glass"); + +// 替换方块(只替换指定类型) +voxels.fillVoxel(0, 64, 0, 10, 70, 10, "minecraft:air", "minecraft:stone"); +``` + +### 数据持久化 + +```js +// JSON 存储(每个项目独立命名空间) +const store = storage.getDataStorage("coins"); +store.set("player1", 100); +const coins = store.get("player1"); // → 100 +store.delete("player1"); +const keys = store.keys(); // → 所有 key 的数组 + +// SQLite 数据库 +db.sql("CREATE TABLE IF NOT EXISTS players (name TEXT, score INT)"); +db.sql("INSERT INTO players VALUES ('Steve', 100)"); + +const result = db.sql("SELECT * FROM players WHERE score > 50"); +// result.rows[0] → { name: "Steve", score: 100 } +// result.firstRow → { name: "Steve", score: 100 } +// result.rowCount → 1 +// result.columnNames → ["name", "score"] +``` + +### 游戏系统 + +```js +// 计分板 +world.addScoreboard("kills"); +world.setScore("Steve", "kills", 42); +world.showScoreboard("sidebar", "kills"); + +// BossBar +world.showBossbar("boss1", "§c远古巨龙", 0.8, "red"); +world.setBossbar("boss1", "§c远古巨龙 §7[80%]", 0.5); + +// 队伍 +world.createTeam("red", "red"); +world.createTeam("blue", "blue"); +world.joinTeam(entity, "red"); + +// 世界边界 +world.borderSize = 500; // 设置边界大小 +world.shrinkBorder(100, 1200); // 在 1200 ticks 内缩到 100 + +// 天气和时间 +world.time = 6000; // 设置时间(0=日出, 6000=正午, 12000=日落, 18000=午夜) +world.rainDensity = 0; // 停雨 +world.clearWeather(); // 晴天 +world.thunderDensity = 1; // 雷暴 + +// 游戏规则 +world.setGameRule("keepInventory", true); +world.setGameRule("doDaylightCycle", false); +``` + +### 视觉效果 + +```js +const pos = new GameVector3(100, 64, 100); + +// 粒子 +world.spawnParticle("minecraft:flame", pos.x, pos.y, pos.z, 0, 0, 0, 1, 10); +world.spawnParticleCircle(pos.x, pos.y, pos.z, 2, "minecraft:heart", 30); + +// 烟花 +world.launchFirework(pos.x, pos.y, pos.z, "red", "large_ball"); +world.launchFirework(pos.x, pos.y, pos.z, "green", "star"); + +// 闪电和爆炸 +world.strikeLightning(pos.x, pos.y, pos.z); +world.explode(pos.x, pos.y, pos.z, 4); // 威力 4 的爆炸 + +// 音效 +world.playSound("minecraft:entity.ender_dragon.growl", pos, 1.0, 1.0); +player.playSound("minecraft:block.note_block.pling", 1.0, 2.0); // 只有该玩家听到 +``` + +### HTTP 请求 + +```js +// GET 请求 +const resp = http.fetch("https://api.example.com/data"); +if (resp.ok) { + const data: unknown = resp.json(); + console.log(data); +} + +// POST JSON +const resp2 = http.fetch("https://api.example.com/webhook", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: "服务器重启了" }) +}); + +// 超时设置 +const resp3 = http.fetch("https://slow-api.com/data", { + timeout: 5000 // 5 秒超时 +}); +``` + +### 客户端脚本(需 Box3JS 客户端 Mod) + +```js +// 客户端入口: src/client/app.ts + +// 每帧执行 +client.onTick(() => { + // 客户端 tick 回调 +}); + +// 键盘输入 +if (input.isKeyDown("space")) { + // 空格键正在被按住 +} + +input.onKeyPress("f", () => { + // F 键被按下时触发 + remoteChannel.sendServerEvent({ action: "open_menu" }); +}); + +// 屏幕 UI +ui.showOverlay("§e按 F 打开菜单"); // 快捷栏上方 +ui.showTitle("§6BOSS 出现!", "§c准备战斗"); // 屏幕中央 + +// 客户端音效 +audio.playSound("minecraft:block.note_block.pling", 1.0, 1.0); +audio.playMusic("minecraft:music.game", 0.5, 1.0); +audio.stopAll(); + +// 雾效控制(客户端渲染) +client.setFogColor(255, 100, 50); // 红雾外观 +client.setFogStartDistance(10); // 雾从 10 格开始 +client.setFogEndDistance(50); // 50 格外完全遮挡 +client.resetFog(); // 恢复默认 + +// 聊天 +chat.sendMessage("大家好!"); +chat.onMessage((msg, _sender, _isSystem) => { + if (msg.includes("秘密")) { + // 处理包含"秘密"的消息 + } +}); + +// 接收服务端事件 +remoteChannel.onClientEvent((event) => { + if (event.args.type === "boss_spawned") { + audio.playSound("minecraft:entity.ender_dragon.growl", 1.0, 1.0); + } +}); +``` + +## 开发循环 + +### 标准流程 + +每次修改代码后: + +```js 改代码 → npm run build → /box3script reload mygame → 测试 ``` @@ -214,35 +796,47 @@ npm run build 输出: -``` +```js dist/server.js 7.1kb ⚡ Done in 240ms ``` 构建做了什么: -1. **Babel** 将 TypeScript 编译为 ES5 JavaScript -2. **esbuild** 将所有模块打包为一个文件 +1. **Babel** 将 TypeScript 编译为 ES5 JavaScript(因为 Rhino 只支持 ES5) +2. **esbuild** 将所有模块打包为一个文件(因为 Rhino 没有 `require()`) 3. 输出到 `dist/server.js` 和 `dist/client.js` -### 加载 +### 加载与重载 在游戏内: -``` +```js /box3script start mygame # 首次启动 /box3script reload mygame # 修改后重载(无需重启服务器) ``` +`reload` 是原子的:先停止旧脚本(清理所有事件回调、计时器、计分板),再加载新脚本。 + ### 自动热重载 开启文件监控后,保存代码 + build 会自动触发 reload: -``` +```js /box3script watch ``` ---- +**注意**:`watch` 监控的是 `dist/` 下的编译产物(`.js`),不是 `src/` 下的源码。所以你需要先 `npm run build` 生成新的 `dist/` 文件,watch 才会检测到变化。配合 `npm run build -- --watch` 可以实现保存即热重载。 + +### 多项目管理 + +```js +/box3script start mygame lobby # 同时启动多个项目 +/box3script stop mygame # 停止单个 +/box3script stopall # 停止全部 +/box3script reload mygame # 重载单个 +/box3script # 查看所有项目状态 +``` ## 调试技巧 @@ -254,44 +848,66 @@ npm run build 2. **看状态** — `/box3script` 检查项目是否是 `◉`(已加载) 3. **看构建** — `npm run build` 是否报错 4. **加日志** — 用 `console.log()` 在关键位置打印变量值 -5. **看行号** — Java 异常栈会包含 JS 文件名和行号 +5. **看行号** — Java 异常栈会包含 JS 文件名和行号(因为脚本被 Rhino 解释执行,行号对应编译后的 `dist/server.js`,不是 `.ts` 源码) ### 常见错误 -| 错误 | 原因 | 解决 | -|------|------|------| -| `console is not defined` | JS 引擎初始化失败 | 检查模组是否正确安装 | -| `world is not defined` | 作用域问题 | 确保代码在全局作用域,不在函数内 | -| `Cannot find name 'xxx'` | TypeScript 类型错误 | 检查拼写,或查看 `.d.ts` 中的正确 API 名 | -| `npm run build` 报错 | JS 语法错误 | 检查 ESLint 输出 | -| 脚本不执行 | 项目未启用 | `/box3script` 查看状态 | +| 错误 | 原因 | 解决 | +| ------------------------ | ------------------- | ------------------------------------------------------ | +| `console is not defined` | JS 引擎初始化失败 | 检查模组是否正确安装 | +| `world is not defined` | 作用域问题 | 确保代码在全局作用域,不在嵌套函数内定义后又引用 | +| `Cannot find name 'xxx'` | TypeScript 类型错误 | 检查拼写,或查看 `types/` 下的 `.d.ts` 中的正确 API 名 | +| `npm run build` 报错 | JS 语法错误 | 检查 ESLint 输出,或看终端错误行号 | +| 脚本不执行 | 项目未启用 | `/box3script` 查看状态 | +| 定时器不触发 | tick 数算错了 | 记住 1 秒 = 20 ticks,不是 1000 | +| 客户端脚本无效 | 玩家没装客户端 Mod | Box3JS 客户端 Mod 必须安装 | +| remoteChannel 没收到 | 数据不是 JSON | 确保传的是纯对象,不是 Java 对象或 `GameVector3` 实例 | ### 沙盒测试 沙盒模式允许安全测试:开启后所有世界修改被追踪,关闭时一键回滚。 -``` +```js /box3script sandbox mygame # 开启沙盒 -# ... 测试脚本(生成实体、修改方块等)... +# ... 测试脚本(生成实体、修改方块、爆炸等)... /box3script sandbox mygame # 关闭 → 自动回滚所有修改 ``` ---- +**适用场景:** + +- **新脚本首次测试** — 不确定脚本会做什么,先沙盒测试 +- **玩家试玩** — 让玩家试玩新功能,结束时回滚不影响正式服 +- **调试破坏性操作** — 测试 `fillVoxel`、`explode` 等操作 + +### 性能注意事项 + +Box3JS 脚本运行在服务器主线程上,不合理的代码会影响 TPS: + +1. **`onTick` 中避免大循环** — 遍历所有实体请在条件触发时做,不要每 tick 做 +2. **缓存查询结果** — 不要把 `querySelectorAll` 放在每 tick +3. **用 `setInterval` 代替 `onTick`** — 如果不需要 20 次/秒,用更长的间隔(比如 100 ticks = 5 秒) +4. **避免 JS ↔ Java 频繁跨越** — 批量操作比逐个操作快 + +一个跑酷脚本的性能消耗通常 < 0.5ms/tick,对服务器 TPS 几乎无影响。 ## 发布部署 +### 开发模式 → 生产发布 + 开发完成后,将脚本编译为**独立 JAR 模组**: -``` +```js /box3script compile mygame ``` 生成 `mygame-1.0.0.jar`(版本号从 `package.json` 读取),放入任意 NeoForge 服务端的 `mods/` 目录即可运行。 **注意:** + - 需要 Box3JS 作为依赖模组(提供 Rhino 运行时) - 如果使用了 `registries`(自定义方块/物品),客户端也需要安装 JAR - JAR 中包含编译后的 JS,无需原始源码 +- 编译后的 JAR 是一个独立的 NeoForge 模组,有自己的 `mods.toml` ### package.json 配置 @@ -308,14 +924,25 @@ npm run build } ``` -这些元数据会被写入 JAR 的 `mods.toml`。 +这些元数据会被写入 JAR 的 `mods.toml`,在游戏的模组列表中显示。 + +### 开发模式 vs 编译模式 ---- +| | 开发模式 (`/box3script start`) | 编译模式 (`/box3script compile`) | +| ------------ | ------------------------------ | -------------------------------- | +| 修改代码 | 热重载,无需重启 | 需重新编译 | +| `registries` | `undefined` | ✅ 可用 | +| 分发 | 需要源码 | 只需 JAR | +| 适用场景 | 开发、测试 | 发布、分发 | ## 下一步 -- **学 API**: 看 [API 功能速查](../api/README.md) — 按"我想做什么"查找对应 API -- **学事件**: 看 [教程三:事件系统与实体操控](../tutorial/03-events-entities.md) +现在你已经理解了 Box3JS 的核心设计理念和基本 API 用法。接下来: + +- **学 API 细节**: 看 [API 功能速查](../api/README.md) — 按"我想做什么"查找对应 API +- **学事件系统**: 看 [教程三:事件系统与实体操控](../tutorial/03-events-entities.md) - **学客户端**: 看 [客户端 API 文档](../api/client.md) — 按键监听、屏幕 UI、客户端音效 -- **懂原理**: 看 [运行原理](architecture.md) — Rhino 引擎、作用域、构建管线 +- **懂原理**: 看 [运行原理](architecture.md) — Rhino 引擎、作用域、构建管线、网络通信 - **选技术**: 看 [JS vs Java 对比](js-vs-java.md) — Box3JS 与原生模组怎么选 +- **常见问题**: 看 [FAQ](faq.md) +- **实战菜谱**: 看 [代码片段与菜谱](recipes.md) — 复制即用的常见功能实现 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md b/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md deleted file mode 100644 index 0272332..0000000 --- a/Box3JS-NeoForge-1.21.1/docs/guide/getting-started_en.md +++ /dev/null @@ -1,302 +0,0 @@ -# Quick Start: From Zero to Your First Box3JS Script - -This guide is for readers with **zero modding experience**. If you know JavaScript, you can write your first Minecraft server script in 10 minutes. - -## Table of Contents - -1. [What is Box3JS](#what-is-box3js) -2. [Setup](#setup) -3. [Create a Project](#create-a-project) -4. [Your First Script](#your-first-script) -5. [Dev Cycle](#dev-cycle) -6. [Debugging](#debugging) -7. [Deployment](#deployment) -8. [Next Steps](#next-steps) - ---- - -## What is Box3JS - -Box3JS is a **server-side scripting engine mod** for NeoForge 1.21.1. It embeds a JavaScript runtime (Mozilla Rhino) inside the Minecraft server, letting you write gameplay logic in JS/TypeScript. - -### What You Can Do - -| Category | Examples | -|----------|---------| -| Chat Commands | `!heal`, `!home`, `!shop` | -| Event Response | Welcome on join, death penalty, block break logging | -| Entity Control | Spawn mobs, set AI, custom bosses | -| Mini-Games | PvP arena, parkour, wave survival | -| World Manipulation | Place/replace blocks, fill regions, change weather/time | -| Data Persistence | JSON storage, SQLite database | -| Game Systems | Scoreboards, BossBars, teams, world borders | -| HTTP Requests | Web API calls, webhook notifications | -| Client Scripts | Key listeners, screen UI, client audio | - -### What You Can't Do - -- **Render custom models/particles** — requires a client resource pack or Java mod -- **Add new blocks/items at runtime** — requires compiling to a JAR (`/box3script compile`) -- **Modify vanilla mechanics** — changing recipes, mob behavior requires Mixin - -### Core Design - -``` -┌──────────────┐ ┌──────────────┐ ┌──────────────┐ -│ TypeScript │ ───→ │ Babel ES5 │ ───→ │ Rhino Engine │ -│ Source │ │ Compile │ │ (in JVM) │ -└──────────────┘ └──────────────┘ └──────────────┘ - │ - ┌───────┴───────┐ - │ Minecraft │ - │ NeoForge API │ - └───────────────┘ -``` - -- **Runs inside the server JVM**, directly calling Minecraft/NeoForge APIs -- **TypeScript source** compiled to ES5 via Babel, targeting Rhino -- **Hot reload** — no server restart when you change code -- **Sandbox isolation** — each project has an independent scope - ---- - -## Setup - -### What You Need - -1. **Minecraft server** with Box3JS + NeoForge 1.21.1 installed -2. **Node.js** 18+ (for local builds only — not needed on the server) -3. A text editor (VS Code recommended) - -### Verify Installation - -In-game, run: - -``` -/box3script -``` - -If you see the project status panel, Box3JS is running. - ---- - -## Create a Project - -In-game: - -``` -/box3script create mygame -``` - -This generates a complete TypeScript project at `config/box3/script/mygame/`: - -``` -config/box3/script/mygame/ -├── package.json ← Project config (name, version, build deps) -├── tsconfig.base.json ← Shared TS compiler options -├── tsconfig.server.json ← Server TS config -├── tsconfig.client.json ← Client TS config -├── build.mjs ← Build script (esbuild + Babel) -├── eslint.config.mjs ← ESLint rules -├── types/ -│ ├── shared.d.ts ← Shared server & client types -│ ├── server/ -│ │ ├── index.d.ts ← Server type entry point -│ │ └── ... -│ └── client/ -│ ├── index.d.ts ← Client type entry point -│ └── ... -├── src/ -│ ├── server/ -│ │ └── app.ts ← ★ Server entry point (where you write code) -│ └── client/ -│ └── app.ts ← Client entry point -└── registries/ ← Custom content (blocks/items/sounds JSON) -``` - -### Install Dependencies - -```bash -cd config/box3/script/mygame -npm install -``` - -`npm install` only needs to run once (installs esbuild, Babel, TypeScript build tooling). - ---- - -## Your First Script - -Open `src/server/app.ts`, clear the contents, and write: - -```js -// 1. Startup log -console.log("MyGame script started!"); - -// 2. Welcome players on join -world.onPlayerJoin((entity) => { - const p = entity.player; - p.directMessage("Welcome, " + p.name + "!"); - - // Particle welcome effect - const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); - world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); -}); - -// 3. Chat commands -world.onChat((entity, message) => { - const p = entity.player; - - if (message === "!hello") { - p.directMessage("Hello, " + p.name + "!"); - return false; // suppress chat message - } - - if (message === "!pos") { - const pos = p.position; - p.directMessage("Your position: " + - Math.floor(pos.x) + ", " + - Math.floor(pos.y) + ", " + - Math.floor(pos.z)); - return false; - } - - return true; // normal chat messages pass through -}); - -// 4. Periodic announcement -world.setInterval(() => { - const count = world.querySelectorAll("*").length; - world.say("[Info] Players online: " + count); -}, 6000); // 6000 ticks = 5 minutes -``` - -### Key Concepts - -- **Globals need no import** — `world`, `console`, `player` are injected by Box3JS -- **Return false to block default behavior** — `onChat` returning false suppresses the message -- **Ticks are MC time units** — 1 second = 20 ticks, `setInterval` uses ticks -- **§ codes are MC color codes** — `§a` = green, `§e` = yellow, `§6` = gold, `§7` = gray - ---- - -## Dev Cycle - -Standard flow after each code change: - -``` -Edit code → npm run build → /box3script reload mygame → test -``` - -### Build - -```bash -npm run build -``` - -Output: - -``` - dist/server.js 7.1kb -Done in 240ms -``` - -What the build does: - -1. **Babel** compiles TypeScript to ES5 JavaScript -2. **esbuild** bundles all modules into a single file -3. Outputs to `dist/server.js` and `dist/client.js` - -### Load - -In-game: - -``` -/box3script start mygame # first launch -/box3script reload mygame # reload after changes (no server restart) -``` - -### Auto Hot-Reload - -Enable file watching so build + save auto-triggers reload: - -``` -/box3script watch -``` - ---- - -## Debugging - -### Troubleshooting Order - -1. **Check console** — server logs show errors with `[Box3JS] [projectName]` prefix -2. **Check status** — `/box3script` to see if project shows `◉` (loaded) -3. **Check build** — `npm run build` should complete without errors -4. **Add logging** — use `console.log()` at key points to print variable values -5. **Read line numbers** — Java exception stacks include JS filenames and line numbers - -### Common Errors - -| Error | Cause | Fix | -|-------|-------|-----| -| `console is not defined` | Engine init failed | Check mod installation | -| `world is not defined` | Scope issue | Ensure code is at global scope, not inside a function | -| `Cannot find name 'xxx'` | TypeScript type error | Check spelling or look up the correct API name in `.d.ts` | -| `npm run build` fails | JS syntax error | Check ESLint output | -| Script not executing | Project not enabled | Check `/box3script` status | - -### Sandbox Testing - -Sandbox mode enables safe testing: all world modifications are tracked and rolled back on close. - -``` -/box3script sandbox mygame # enable sandbox -# ... test script (spawn entities, modify blocks, etc.)... -/box3script sandbox mygame # disable → auto-rollback all changes -``` - ---- - -## Deployment - -Once development is done, compile your script into a **standalone JAR mod**: - -``` -/box3script compile mygame -``` - -Generates `mygame-1.0.0.jar` (version from `package.json`). Drop it into any NeoForge server's `mods/` directory. - -**Notes:** -- Box3JS must also be installed as a dependency (provides the Rhino runtime) -- If you use `registries` (custom blocks/items), clients must also install the JAR -- The JAR contains compiled JS — no source code needed - -### package.json Config - -```json -{ - "name": "mygame", - "displayName": "My Game", - "version": "1.0.0", - "description": "A custom mini-game", - "author": "YourName", - "license": "MIT", - "homepage": "https://example.com", - "logoFile": "logo.png" -} -``` - -These metadata fields are written into the JAR's `mods.toml`. - ---- - -## Next Steps - -- **Learn APIs**: [API by Task](../api/README_en.md) — find APIs by "I want to..." -- **Learn Events**: [Tutorial 3: Events & Entities](../tutorial/03-events-entities.md) -- **Learn Client**: [Client API](../api/client_en.md) — key listeners, screen UI, client audio -- **Understand Internals**: [Architecture](architecture_en.md) — Rhino engine, scopes, build pipeline -- **Tech Decision**: [JS vs Java](js-vs-java_en.md) — Box3JS vs native modding diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md b/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md index b3e3368..cf2457b 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/js-vs-java.md @@ -1,26 +1,22 @@ # JS 脚本 vs 原生 Java 模组开发对比 -本文帮助你判断:**什么时候用 Box3JS 写脚本,什么时候用 Java 写原生模组。** - ## 总览 -| 维度 | Box3JS (JS/TS) | 原生 Java 模组 | -|------|---------------|---------------| -| **上手门槛** | 会 JavaScript 即可 | 需要 Java + Gradle + Minecraft 模组开发知识 | -| **开发速度** | 改代码 → build → reload(秒级) | 改代码 → 编译 → 重启 MC(分钟级) | -| **热重载** | 支持(`/box3script reload`) | 不支持,每次改代码需重启客户端/服务端 | -| **发布方式** | `/box3script compile` 生成 JAR | `gradlew build` 生成 JAR | -| **执行性能** | 中等(Rhino 解释执行) | 高(JIT 编译为字节码) | -| **API 覆盖面** | 高层封装 API(100+ 方法) | 完整 Minecraft/NeoForge API | -| **类型安全** | TypeScript 类型声明 | Java 静态类型 | -| **调试工具** | console.log + 控制台输出 | IDE 断点调试 | -| **依赖管理** | npm(仅构建时) | Gradle/Maven | -| **客户端功能** | 有限(UI/输入/音效/聊天) | 完整(渲染、模型、GUI、网络协议) | -| **自定义方块/物品** | JSON 配置 + 编译时生成 | Java 类 + 注册 | -| **修改原版行为** | 不支持(无 Mixin) | 支持(Mixin/ASM/CoreMod) | -| **多人协作** | JS 源码 + Git | Java 源码 + Git + Gradle | - ---- +| 维度 | Box3JS (JS/TS) | 原生 Java 模组 | +| ------------------- | ------------------------------- | ------------------------------------------- | +| **上手门槛** | 会 JavaScript 即可 | 需要 Java + Gradle + Minecraft 模组开发知识 | +| **开发速度** | 改代码 → build → reload(秒级) | 改代码 → 编译 → 重启 MC(分钟级) | +| **热重载** | 支持(`/box3script reload`) | 不支持,每次改代码需重启客户端/服务端 | +| **发布方式** | `/box3script compile` 生成 JAR | `gradlew build` 生成 JAR | +| **执行性能** | 中等(Rhino 解释执行) | 高(JIT 编译为字节码) | +| **API 覆盖面** | 高层封装 API(100+ 方法) | 完整 Minecraft/NeoForge API | +| **类型安全** | TypeScript 类型声明 | Java 静态类型 | +| **调试工具** | console.log + 控制台输出 | IDE 断点调试 | +| **依赖管理** | npm(仅构建时) | Gradle/Maven | +| **客户端功能** | 有限(UI/输入/音效/聊天) | 完整(渲染、模型、GUI、网络协议) | +| **自定义方块/物品** | JSON 配置 + 编译时生成 | Java 类 + 注册 | +| **修改原版行为** | 不支持(无 Mixin) | 支持(Mixin/ASM/CoreMod) | +| **多人协作** | JS 源码 + Git | Java 源码 + Git + Gradle | ## 开发体验对比 @@ -64,11 +60,11 @@ public class HealMod { 这是 Box3JS **最大的生产力优势**。 -| 操作 | Box3JS | Java 模组 | -|------|--------|---------| -| 修改一行代码 | build(3s) + reload(1s) = **4 秒** | 编译(10-60s) + 重启MC(30-120s) = **40-180 秒** | -| 测试一个聊天命令 | 改代码 → build → 游戏内 reload | 改代码 → 编译 → 重启MC → 进入世界 | -| 一天迭代次数 | **50+** | 5-10 | +| 操作 | Box3JS | Java 模组 | +| ---------------- | --------------------------------- | ---------------------------------------------- | +| 修改一行代码 | build(3s) + reload(1s) = **4 秒** | 编译(10-60s) + 重启MC(30-120s) = **40-180 秒** | +| 测试一个聊天命令 | 改代码 → build → 游戏内 reload | 改代码 → 编译 → 重启MC → 进入世界 | +| 一天迭代次数 | **50+** | 5-10 | 对于玩法脚本(小游戏、RPG 机制、经济系统),热重载是**不可替代的**——玩法需要反复调参试错,等不起重启。 @@ -106,6 +102,7 @@ world.setScore("Steve", "kills", 5); #### 4. 一站式项目模板 `/box3script create` 生成完整的项目结构,包含: + - TypeScript 配置 + 类型声明 - 构建管线(Babel + esbuild) - ESLint 代码检查 @@ -117,27 +114,25 @@ world.setScore("Steve", "kills", 5); 在正式写 Java 模组前,用 Box3JS 快速验证玩法设计: -``` +```js 想法 → 30分钟写Box3JS脚本 → 和朋友试玩 → 调整 → 确认玩法可行 ↓ 决定做完整模组 → 用 Java 重写 ``` ---- - ### Box3JS 的劣势 #### 1. 性能开销 Rhino 是**解释型** JS 引擎(无 JIT),单线程执行。对性能敏感的操作(如每 tick 扫描大量实体)可能成为瓶颈。 -| 场景 | Box3JS | Java | -|------|--------|------| -| 聊天命令 | 无感知 | 无感知 | -| 每 tick 遍历 100 个实体 | 可接受 | 可接受 | +| 场景 | Box3JS | Java | +| ------------------------- | ------------ | ------ | +| 聊天命令 | 无感知 | 无感知 | +| 每 tick 遍历 100 个实体 | 可接受 | 可接受 | | 每 tick 遍历 10000 个实体 | **可能卡顿** | 可接受 | -| 复杂数学运算(路径算法) | **明显慢** | 快 | -| Y=0 区块全图填充 | **很慢** | 快 | +| 复杂数学运算(路径算法) | **明显慢** | 快 | +| Y=0 区块全图填充 | **很慢** | 快 | **经验法则**:如果 `onTick` 回调耗时超过 1ms,考虑优化或改用 Java。 @@ -145,16 +140,16 @@ Rhino 是**解释型** JS 引擎(无 JIT),单线程执行。对性能敏 Box3JS 封装了 100+ 常用 API,但不是全部: -| 你想做的 | Box3JS | Java | -|---------|--------|------| -| 修改合成表 | ❌ | ✅ `RecipeManager` | -| 自定义 GUI(箱子界面)| ❌ | ✅ `MenuProvider` / `Screen` | -| 修改生物 AI | 部分(setAI/setTarget) | ✅ Brain/Memory 系统 | -| 自定义维度 | ❌ | ✅ `DimensionType` | -| 数据包/战利品表 | ❌ | ✅ 完整支持 | -| 网络协议 | 高层(remoteChannel) | ✅ 底层 `CustomPayload` | -| 修改原版类行为 | ❌ | ✅ Mixin / ASM | -| 渲染自定义模型 | ❌ | ✅ 完整渲染管线 | +| 你想做的 | Box3JS | Java | +| ---------------------- | ----------------------- | ---------------------------- | +| 修改合成表 | ❌ | ✅ `RecipeManager` | +| 自定义 GUI(箱子界面) | ❌ | ✅ `MenuProvider` / `Screen` | +| 修改生物 AI | 部分(setAI/setTarget) | ✅ Brain/Memory 系统 | +| 自定义维度 | ❌ | ✅ `DimensionType` | +| 数据包/战利品表 | ❌ | ✅ 完整支持 | +| 网络协议 | 高层(remoteChannel) | ✅ 底层 `CustomPayload` | +| 修改原版类行为 | ❌ | ✅ Mixin / ASM | +| 渲染自定义模型 | ❌ | ✅ 完整渲染管线 | #### 3. 无断点调试 @@ -163,12 +158,14 @@ Box3JS 封装了 100+ 常用 API,但不是全部: #### 4. 客户端功能有限 客户端脚本可以做: + - 键盘输入检测 - 屏幕 UI 显示 - 音效/音乐播放 - 聊天收发 但不能做: + - 自定义渲染(模型、粒子、GUI) - 修改 HUD - 自定义着色器 @@ -177,6 +174,7 @@ Box3JS 封装了 100+ 常用 API,但不是全部: #### 5. ES5 限制 Rhino 1.9.1 仅支持 ES5 语法。不能使用: + - `let` / `const`(Babel 编译为 `var`) - 箭头函数(Babel 编译为 `function`) - `async` / `await` @@ -191,11 +189,9 @@ Rhino 1.9.1 仅支持 ES5 语法。不能使用: 编译后的 JAR 依赖 Box3JS 作为运行时。用户需要同时安装 Box3JS + 你的 JAR。而纯 Java 模组是自包含的。 ---- - ## 适用场景决策树 -``` +```text 你想做什么? │ ├─ 小游戏(PvP/跑酷/竞速) @@ -234,13 +230,11 @@ Rhino 1.9.1 仅支持 ES5 语法。不能使用: 大量内容 → Java 原生模组 ``` ---- - ## 混合方案 最佳实践:**Box3JS 做玩法,Java 做基础设施**。 -``` +```text ┌──────────────────────────────────┐ │ Java 模组(提供底层能力) │ │ - 自定义方块/物品注册 │ @@ -260,22 +254,21 @@ Rhino 1.9.1 仅支持 ES5 语法。不能使用: ``` 一个真实的示例架构: + - Java 模组添加了自定义武器、自定义怪物、新维度 - Box3JS 脚本定义怪物波次规则、Boss 技能、任务触发条件 - 玩法策划可以独立修改脚本,不需要碰 Java 代码 ---- - ## 总结 -| 选 Box3JS | 选 Java | -|-----------|--------| -| 你主要做玩法/小游戏 | 你需要修改原版机制 | -| 你需要快速迭代试错 | 你需要自定义渲染/模型 | -| 你的团队有 JS 开发者 | 你的团队主要是 Java 开发者 | +| 选 Box3JS | 选 Java | +| ------------------------ | -------------------------------- | +| 你主要做玩法/小游戏 | 你需要修改原版机制 | +| 你需要快速迭代试错 | 你需要自定义渲染/模型 | +| 你的团队有 JS 开发者 | 你的团队主要是 Java 开发者 | | 项目逻辑复杂但不涉及渲染 | 项目包含大量自定义方块/实体/维度 | -| 你想先验证玩法再正式开发 | 你要发布到 CurseForge/Modrinth | -| 你需要热重载 | 你需要极致性能 | -| 项目是服务端为主 | 项目需要客户端渲染 | +| 你想先验证玩法再正式开发 | 你要发布到 CurseForge/Modrinth | +| 你需要热重载 | 你需要极致性能 | +| 项目是服务端为主 | 项目需要客户端渲染 | **没有谁更好,只有谁更适合当前项目。** 对于服务端玩法开发,Box3JS 的生产力优势是压倒性的——热重载 + 低门槛 + 丰富 API。对于需要修改原版机制或自定义渲染的项目,Java 是必须的。 diff --git a/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md b/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md index 44851f0..d546262 100644 --- a/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md +++ b/Box3JS-NeoForge-1.21.1/docs/guide/recipes.md @@ -1,23 +1,6 @@ -# 常用配方:Box3JS 功能模板 +# 常用配方 -本指南是"菜谱"风格——不逐 API 讲解,而是一个个"想实现 X 功能,照这个模板改就行"。所有代码段均经过编译验证。 - -## 目录 - -1. [聊天命令](#聊天命令) -2. [经济系统](#经济系统) -3. [传送系统](#传送系统) -4. [重生保护](#重生保护) -5. [商店/NPC](#商店npc) -6. [每日奖励](#每日奖励) -7. [排行榜](#排行榜) -8. [波次刷怪](#波次刷怪) -9. [缩圈机制](#缩圈机制) -10. [HTTP Webhook](#http-webhook) -11. [客户端 HUD](#客户端-hud) -12. [跨脚本联动](#跨脚本联动) - ---- +"想实现 X 功能,照模板改就行"。所有代码段均经过编译验证。 ## 聊天命令 @@ -73,8 +56,6 @@ if (message === "!admin") { } ``` ---- - ## 经济系统 基于计分板的经济系统,玩家可以用 `/box3script reload` 不丢失数据(计分板独立于脚本生命周期)。 @@ -150,8 +131,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## 传送系统 ### 家传送(内存,重启丢失) @@ -243,24 +222,20 @@ world.onChat((entity, message) => { }); ``` ---- - ## 重生保护 ```js // 玩家重生后给予短暂无敌 world.onPlayerRespawn((entity) => { const p = entity.player; - p.addEffect("minecraft:resistance", 100, 4, true); // 5秒 抗性V(无敌) - p.addEffect("minecraft:regeneration", 100, 2, true); // 5秒 生命恢复III + p.addEffect("minecraft:resistance", 100, 4, true); // 5秒 抗性V(无敌) + p.addEffect("minecraft:regeneration", 100, 2, true); // 5秒 生命恢复III p.addEffect("minecraft:fire_resistance", 100, 0, true); // 5秒 防火 p.directMessage("§a你已获得 5 秒重生保护"); p.playSound("minecraft:block.beacon.activate", 1.0, 1.5); }); ``` ---- - ## 商店/NPC 右键一个实体(如村民)弹出对话/交易: @@ -318,12 +293,11 @@ world.onChat((entity, message) => { }); ``` ---- - ## 每日奖励 ```js -const dailyRewards = storage.getDataStorage<{ lastClaimed: number }>("daily-rewards"); +const dailyRewards = + storage.getDataStorage < { lastClaimed: number } > "daily-rewards"; world.onChat((entity, message) => { const p = entity.player; @@ -331,9 +305,9 @@ world.onChat((entity, message) => { if (message === "!daily") { const now = Date.now(); const record = dailyRewards.get(p.userId); - const cooldown = 24 * 60 * 60 * 1000; // 24 小时 + const cooldown = 24 * 60 * 60 * 1000; // 24 小时 - if (record && (now - record.lastClaimed) < cooldown) { + if (record && now - record.lastClaimed < cooldown) { const hours = Math.ceil((record.lastClaimed + cooldown - now) / 3600000); p.directMessage(`§c请等待 ${hours} 小时后再领取`); return false; @@ -354,8 +328,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## 排行榜 ```js @@ -382,8 +354,6 @@ world.onChat((entity, message) => { }); ``` ---- - ## 波次刷怪 完整波次系统,难度递增: @@ -403,7 +373,7 @@ function spawnWave(pos: GameVector3): void { world.say(`§c§l⚔ 第 ${wave} 波开始!§f ${count} 只怪物`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 12; const z = pos.z + (Math.random() - 0.5) * 12; const type = types[Math.floor(Math.random() * types.length)]; @@ -424,8 +394,6 @@ function spawnWave(pos: GameVector3): void { } ``` ---- - ## 缩圈机制 ```js @@ -445,10 +413,10 @@ function startShrinkPhase(centerX: number, centerZ: number, stages: { size: numb world.say(`§c边界缩小至 ${stage.size} 格!(${stage.duration} 秒)`); world.shrinkBorder(stage.size * 2, stage.duration); stageIndex++; - world.setTimeout(nextStage, stage.duration * 20); + setTimeout(nextStage, stage.duration * 20); } - world.setTimeout(nextStage, 100); // 5 秒后开始 + setTimeout(nextStage, 100); // 5 秒后开始 } // 用法:100→50→25→10,每段 60 秒 @@ -460,8 +428,6 @@ startShrinkPhase(0, 0, [ ]); ``` ---- - ## HTTP Webhook ```js @@ -478,8 +444,12 @@ world.onEntityDeath((entity, killer) => { }), timeout: 5000, async: true, - onResponse: (resp) => { console.log(`Webhook sent: ${resp.status}`); }, - onError: (err) => { console.warn(`Webhook failed: ${err}`); }, + onResponse: (resp) => { + console.log(`Webhook sent: ${resp.status}`); + }, + onError: (err) => { + console.warn(`Webhook failed: ${err}`); + }, }); } }); @@ -488,9 +458,9 @@ world.onEntityDeath((entity, killer) => { const SERVER_NAME = "My Server"; const WEBHOOK_URL = "https://discord.com/api/webhooks/YOUR_ID"; -world.setInterval(() => { +setInterval(() => { const playerCount = world.querySelectorAll("*").length; - const tps = "20"; // 正常情况 + const tps = "20"; // 正常情况 http.fetch(WEBHOOK_URL, { method: "POST", @@ -504,8 +474,6 @@ world.setInterval(() => { }, 6000); ``` ---- - ## 客户端 HUD 结合 `remoteChannel` 实现客户端自定义 HUD(服务端提供数据,客户端显示): @@ -560,8 +528,6 @@ remoteChannel.onClientEvent((event) => { }); ``` ---- - ## 跨脚本联动 多个脚本项目之间通信: @@ -590,6 +556,4 @@ function endGame(): void { } ``` ---- - 每个配方都是独立的,按需取用。更多细节参见 [API 文档](../api/README.md) 和 [教程系列](../tutorial/README.md)。 diff --git a/Box3JS-NeoForge-1.21.1/docs/index.md b/Box3JS-NeoForge-1.21.1/docs/index.md new file mode 100644 index 0000000..01e61bb --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/index.md @@ -0,0 +1,78 @@ +--- +layout: home + +hero: + name: "Box3JS" + text: "Minecraft JS/TS 脚本引擎" + tagline: 神奇代码岛同款编程体验,用 JS/TS 在 Minecraft 里创造小游戏 + actions: + - theme: brand + text: 快速开始 + link: /guide/getting-started + +features: + - icon: 🎮 + title: 服务端 & 客户端双端脚本 + details: 服务端:世界操作、实体、合成配方。客户端:键盘输入、屏幕 UI、音效、SQLite 存储、HTTP 请求。 + - icon: 📦 + title: TypeScript 优先 + details: 17 个全局对象全部提供 DTS 类型定义。内置 esbuild + Babel 构建管线,将现代 TS 转译为 Rhino 兼容的 ES5。 + - icon: 🔄 + title: 热重载 + details: 修改脚本即时生效,无需重启服务端。文件监听器在保存时自动重载。 + - icon: 🌐 + title: 双向通信 + details: remoteChannel 实现服务端↔客户端事件消息传递。服务端广播至所有玩家,客户端独立回复。 + - icon: 🗄️ + title: 双端存储 & 数据库 + details: 服务端和客户端均支持 JSON 文件持久化与 SQLite。分页查询、原子更新、计数器、标签模板查询。 + - icon: 🧩 + title: 自定义方块 & 物品 + details: 方块纹理、物品模型、装备、音效、创造标签页——全部通过 JSON 配置注册(独立/JAR 模式)。 + - icon: 📚 + title: 完善文档 + details: 50+ 页面,涵盖 API 参考、渐进式教程、常用配方、架构深入解析和常见问题——支持中英双语。 + - icon: 🚀 + title: 独立 JAR 模式 + details: 将脚本项目编译为独立 JAR 模组,无需依赖 Box3JS 运行时——放入 mods 文件夹即可使用。 + +--- + +## 快速开始 + +```bash +# 游戏内:创建新项目 +/box3script create mygame + +# 构建并监听 +cd config/box3/script/mygame +npm install +npm run build -- --watch + +# TypeScript 类型检查 +npm run check +``` + +```ts +// src/server/app.ts — 你的第一个脚本 +world.onChat((entity, message) => { + if (message === "!hello") { + entity.player.directMessage(`你好,${entity.player.name}!`); + return false; + } + return true; +}); +``` + +[查看完整文档 →](/guide/getting-started) + +## 版本信息 + +| 组件 | 版本 | +|------|------| +| Minecraft | 1.21.1 | +| 模组加载器 | NeoForge | +| Java | 21 | +| JS 引擎 | Mozilla Rhino 1.9.1 (ES5) | +| TypeScript | 通过 Babel → ES5 | + diff --git a/Box3JS-NeoForge-1.21.1/docs/README.md b/Box3JS-NeoForge-1.21.1/docs/overview.md similarity index 100% rename from Box3JS-NeoForge-1.21.1/docs/README.md rename to Box3JS-NeoForge-1.21.1/docs/overview.md diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md index 10bc2eb..91ed1da 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md @@ -11,7 +11,7 @@ 在游戏内执行一条命令: -``` +```js /box3script create hello ``` @@ -46,7 +46,7 @@ world.onPlayerJoin((entity) => { 回到游戏内: -``` +```js /box3script start hello ``` @@ -57,19 +57,17 @@ world.onPlayerJoin((entity) => { 试着把欢迎消息改成: ```js -entity.player.directMessage("§6你好," + entity.player.name + "!"); +entity.player.directMessage(`§6你好,${entity.player.name}!`); ``` 保存后执行 `npm run build`,然后在游戏内: -``` +```js /box3script reload hello ``` 不需要重启服务器,改动立刻生效。 ---- - 以上 5 步就是完整的开发循环:**改代码 → build → reload**。下文深入讲解你能用的所有能力。 ## 消息系统 @@ -94,22 +92,22 @@ player.title("§6§l主标题", "§7副标题"); player.title("§c§lBOSS", "远古巨龙", 10, 60, 10); ``` -| 方法 | 位置 | 可见范围 | -|------|------|---------| -| `world.say()` | 聊天栏 | 全服 | -| `player.directMessage()` | 聊天栏 | 单人 | -| `player.actionBar()` | 快捷栏上方 | 单人 | -| `player.title()` | 屏幕中央 | 单人 | +| 方法 | 位置 | 可见范围 | +| ------------------------ | ---------- | -------- | +| `world.say()` | 聊天栏 | 全服 | +| `player.directMessage()` | 聊天栏 | 单人 | +| `player.actionBar()` | 快捷栏上方 | 单人 | +| `player.title()` | 屏幕中央 | 单人 | ### console 日志 `console` 输出到服务端控制台,格式为 `[Box3JS] [项目名] message`: ```js -console.log("普通日志"); // [Box3JS] [hello] 普通日志 +console.log("普通日志"); // [Box3JS] [hello] 普通日志 console.debug("调试信息"); // [Box3JS] [hello] [DEBUG] 调试信息 -console.warn("警告"); // [Box3JS] [hello] [WARN] 警告 -console.error("错误"); // [Box3JS] [hello] [ERROR] 错误 +console.warn("警告"); // [Box3JS] [hello] [WARN] 警告 +console.error("错误"); // [Box3JS] [hello] [ERROR] 错误 ``` ## 聊天命令系统 @@ -128,7 +126,7 @@ world.onChat((entity, message) => { p.directMessage("§f!pos §7- 查看坐标"); p.directMessage("§f!day §7- 设为白天"); p.directMessage("§f!clear §7- 清除天气"); - return false; // ★ 返回 false 阻止消息显示在聊天栏 + return false; // ★ 返回 false 阻止消息显示在聊天栏 case "!hello": p.directMessage(`§e你好,${p.name}!`); @@ -141,7 +139,7 @@ world.onChat((entity, message) => { case "!pos": { const pos = p.position; p.directMessage( - `§e你的位置: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}` + `§e你的位置: §f${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}`, ); return false; } @@ -156,7 +154,7 @@ world.onChat((entity, message) => { world.say(`§e${p.name} §f清除了天气`); return false; } - return true; // 不是命令的消息正常发送 + return true; // 不是命令的消息正常发送 }); ``` @@ -175,7 +173,14 @@ world.onPlayerJoin((entity) => { // 粒子圈 + 音效 const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); ``` @@ -186,44 +191,44 @@ world.onPlayerJoin((entity) => { ```js // 每 5 分钟广播一次在线人数 -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7在线: §f${count} §7人`); -}, 6000); // 6000 ticks = 5 分钟 +}, 6000); // 6000 ticks = 5 分钟 // 30 秒后执行一次 -world.setTimeout(() => { +setTimeout(() => { world.say("§6服务器已运行 30 秒"); -}, 600); // 600 ticks = 30 秒 +}, 600); // 600 ticks = 30 秒 ``` **Tick 换算:** 20 ticks = 1 秒 -| 时长 | Ticks | -|------|-------| -| 1 秒 | 20 | -| 5 秒 | 100 | -| 30 秒 | 600 | -| 1 分钟 | 1200 | -| 5 分钟 | 6000 | +| 时长 | Ticks | +| ------ | ----- | +| 1 秒 | 20 | +| 5 秒 | 100 | +| 30 秒 | 600 | +| 1 分钟 | 1200 | +| 5 分钟 | 6000 | ## 世界属性 ```js // 时间 -world.time = 6000; // 正午 (0=日出, 6000=正午, 12000=日落, 18000=午夜) +world.time = 6000; // 正午 (0=日出, 6000=正午, 12000=日落, 18000=午夜) // 天气 -world.rainDensity = 1.0; // 满强度下雨 +world.rainDensity = 1.0; // 满强度下雨 world.thunderDensity = 0.5; // 雷暴 -world.clearWeather(); // 晴天 +world.clearWeather(); // 晴天 // 难度 -world.difficulty = "hard"; // peaceful / easy / normal / hard +world.difficulty = "hard"; // peaceful / easy / normal / hard // 游戏规则 -world.setGameRule("keepInventory", true); // 死亡不掉落 -world.setGameRule("doFireTick", false); // 火焰不蔓延 +world.setGameRule("keepInventory", true); // 死亡不掉落 +world.setGameRule("doFireTick", false); // 火焰不蔓延 world.setGameRule("doMobSpawning", false); // 禁止刷怪 ``` @@ -243,12 +248,19 @@ world.onPlayerJoin((entity) => { const p = entity.player; p.title("§6§l欢迎来到服务器!", "§7输入 §f!help §7查看命令", 5, 70, 10); const pos = p.position; - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); }); // ── 定时公告 ── -world.setInterval(() => { +setInterval(() => { const count = world.querySelectorAll("*").length; if (count > 0) world.say(`§7在线: §f${count} §7人`); }, 6000); @@ -268,7 +280,9 @@ world.onChat((entity, message) => { return false; case "!pos": { const pos = p.position; - p.directMessage(`§e位置: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`); + p.directMessage( + `§e位置: §f${Math.floor(pos.x)} ${Math.floor(pos.y)} ${Math.floor(pos.z)}`, + ); return false; } case "!online": @@ -291,13 +305,13 @@ world.onChat((entity, message) => { ### 开发循环 -``` +```js 改代码 → npm run build → /box3script reload hello → 测试 ``` 开启文件监控自动热重载(无需手动 reload): -``` +```js /box3script watch ``` @@ -305,7 +319,7 @@ world.onChat((entity, message) => { 开启沙盒后,脚本对世界的所有修改都会被追踪,关闭时一键回滚: -``` +```js /box3script sandbox hello # 开启 # ... 测试脚本 ... /box3script sandbox hello # 关闭 → 回滚所有修改 @@ -314,6 +328,7 @@ world.onChat((entity, message) => { ### 调试技巧 遇到问题时的排查顺序: + 1. 检查服务端控制台是否有报错(`console.log` 输出会出现在这里) 2. 确认脚本已加载:`/box3script` 看项目是否显示为 `◉`(已加载运行中) 3. 确认 build 成功:`npm run build` 应该没有错误 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md index 5c48e75..57af4a1 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md @@ -26,7 +26,7 @@ ```js const token = world.onTick((info) => { - console.log("Tick: " + info.tick); + console.log(`Tick: ${info.tick}`); }); token.cancel(); // 取消监听 @@ -164,9 +164,10 @@ const entity = world.createEntity({ maxHp: 40, tags: ["elite", "undead"], }); +if (!entity) return; // createEntity 可能返回 null entity.setEquipment("mainhand", "minecraft:bow"); -entity.setTarget(somePlayerEntity); // 设置攻击目标 +// entity.setTarget(targetEntity); // 设置攻击目标(需要先获取实体引用) entity.clearTarget(); // 清除目标 entity.navigateTo(10, 100, 10, 0.5); // 导航到指定位置 entity.setPersistent(true); // 持久化(不会被卸载) @@ -199,9 +200,9 @@ function createPatrol( guard.setAI(true); let wpIndex = 0; - const tid = world.setInterval(() => { + const tid = setInterval(() => { if (guard.destroyed) { - world.clearInterval(tid); + tid.cancel(); return; } // 到达当前路点 → 下一个 @@ -251,20 +252,20 @@ if (entity.hasTag("boss")) { const tags = entity.tags(); // ["boss", "undead"] // 实体碰撞 -world.onEntityContact((entityA, entityB, tick) => { +world.onEntityContact((entityA, entityB, _tick) => { if (entityA.isPlayer() && entityB.hasTag("boss")) { entityA.player.actionBar("§c小心 Boss!"); } }); -world.onEntitySeparate((entityA, entityB, tick) => { +world.onEntitySeparate((entityA, entityB, _tick) => { // 两个实体分离 }); ``` ## 3.8 常用实体类型 -``` +```js minecraft:zombie 僵尸 minecraft:skeleton 骷髅 minecraft:creeper 苦力怕 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md index 2ac8de6..1005e31 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md @@ -38,7 +38,7 @@ world.addScoreboard("playtime", "dummy"); world.showScoreboard("sidebar", "playtime"); // 每分钟 +1 -world.setInterval(() => { +setInterval(() => { world.querySelectorAll("*").forEach((entity) => { if (!entity.isPlayer()) { return; } const p = entity.player; @@ -94,11 +94,11 @@ world.removeBossbar("my_bar"); let timeLeft = 30; world.showBossbar("demo_timer", "§e倒计时演示", 1.0, "green"); -const timerId = world.setInterval(() => { +const timerId = setInterval(() => { timeLeft--; if (timeLeft <= 0) { world.removeBossbar("demo_timer"); - world.clearInterval(timerId); + timerId.cancel(); world.say("§c⏰ 时间到!"); world.playSound("minecraft:block.note_block.pling", new GameVector3(0, 70, 0), 1.0, 0.5); return; @@ -201,7 +201,7 @@ world.borderSize = 200; world.setBorderDamage(1); world.setBorderWarning(10); -world.setTimeout(() => { +setTimeout(() => { world.say("§c边界缩小至 50 格!"); world.shrinkBorder(50, 60); world.playSound( diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md index cc6c0de..3b11b30 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md @@ -19,28 +19,28 @@ world.spawnParticleCircle(0, 100, 0, 4.0, "minecraft:end_rod", 36); 常用粒子: -| 粒子 ID | 效果 | -|---------|------| -| `minecraft:flame` | 火焰 | -| `minecraft:cloud` | 烟雾 | -| `minecraft:happy_villager` | 绿色粒子(正面) | -| `minecraft:witch` | 紫色粒子 | -| `minecraft:portal` | 传送门 | -| `minecraft:end_rod` | 末地烛光 | -| `minecraft:heart` | 爱心 | -| `minecraft:note` | 音符 | -| `minecraft:dragon_breath` | 龙息 | -| `minecraft:angry_villager` | 愤怒粒子(红色) | +| 粒子 ID | 效果 | +| --------------------------- | ---------------- | +| `minecraft:flame` | 火焰 | +| `minecraft:cloud` | 烟雾 | +| `minecraft:happy_villager` | 绿色粒子(正面) | +| `minecraft:witch` | 紫色粒子 | +| `minecraft:portal` | 传送门 | +| `minecraft:end_rod` | 末地烛光 | +| `minecraft:heart` | 爱心 | +| `minecraft:note` | 音符 | +| `minecraft:dragon_breath` | 龙息 | +| `minecraft:angry_villager` | 愤怒粒子(红色) | | `minecraft:soul_fire_flame` | 灵魂火焰(蓝色) | -| `minecraft:redstone` | 红石粒子 | -| `minecraft:explosion` | 爆炸粒子 | +| `minecraft:redstone` | 红石粒子 | +| `minecraft:explosion` | 爆炸粒子 | ### 螺旋上升粒子 ```js function spiralEffect(pos: GameVector3): void { for (let i = 0; i < 40; i++) { - world.setTimeout(() => { + setTimeout(() => { const angle = (i / 40) * Math.PI * 4; const radius = 2.0; const px = pos.x + Math.cos(angle) * radius; @@ -70,18 +70,28 @@ world.launchFirework(0, 100, 0, "green", "creeper"); ### 连续烟花秀 ```js -const colors = ["red", "gold", "green", "blue", "purple", "white", "pink", "aqua"]; +const colors = [ + "red", + "gold", + "green", + "blue", + "purple", + "white", + "pink", + "aqua", +]; const shapes = ["ball", "large_ball", "star", "creeper", "burst"]; for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const c = colors[i % colors.length]; const s = shapes[i % shapes.length]; world.launchFirework( pos.x + (Math.random() - 0.5) * 10, pos.y + 5 + Math.random() * 8, pos.z + (Math.random() - 0.5) * 10, - c, s + c, + s, ); }, i * 300); } @@ -91,13 +101,13 @@ for (let i = 0; i < 8; i++) { ```js // 闪电: (x, y, z, 伤害) -world.strikeLightning(0, 100, 0); // 默认伤害 -world.strikeLightning(0, 100, 0, 10); // 10 点伤害 -world.strikeLightning(0, 100, 0, 0); // 无伤害,纯视觉效果 +world.strikeLightning(0, 100, 0); // 默认伤害 +world.strikeLightning(0, 100, 0, 10); // 10 点伤害 +world.strikeLightning(0, 100, 0, 0); // 无伤害,纯视觉效果 // 在玩家周围召唤闪电 for (let i = 0; i < 3; i++) { - world.setTimeout(() => { + setTimeout(() => { const lx = pos.x + (Math.random() - 0.5) * 12; const lz = pos.z + (Math.random() - 0.5) * 12; world.strikeLightning(lx, pos.y, lz, 0); @@ -110,14 +120,24 @@ world.playSound("minecraft:entity.lightning_bolt.thunder", pos, 1.0, 1.0); ```js // 爆炸: (x, y, z, 威力, 是否引火) -world.explode(0, 100, 0, 4, false); // 威力 4,不引火 -world.explode(0, 100, 0, 8, true); // 威力 8,引火 +world.explode(0, 100, 0, 4, false); // 威力 4,不引火 +world.explode(0, 100, 0, 8, true); // 威力 8,引火 // 玩家引爆自身周围(3 秒倒计时) world.playSound("minecraft:block.note_block.bass", pos, 1.0, 0.5); -world.setTimeout(() => { - world.spawnParticle("minecraft:explosion", pos.x, pos.y, pos.z, 1, 0, 0, 0, 0); - world.setTimeout(() => { +setTimeout(() => { + world.spawnParticle( + "minecraft:explosion", + pos.x, + pos.y, + pos.z, + 1, + 0, + 0, + 0, + 0, + ); + setTimeout(() => { world.explode(pos.x, pos.y, pos.z, 4, false); world.playSound("minecraft:entity.generic.explode", pos, 1.0, 1.0); }, 10); @@ -137,22 +157,22 @@ player.playSound("minecraft:entity.player.levelup", 1.0, 1.0); 常用音效: -| 音效 ID | 用途 | -|---------|------| -| `minecraft:block.note_block.pling` | 铃铛提示 | -| `minecraft:block.note_block.bass` | 低音提示 | -| `minecraft:entity.experience_orb.pickup` | 经验球拾取 | -| `minecraft:entity.player.levelup` | 升级 | -| `minecraft:entity.ender_dragon.growl` | 龙吼(Boss 出场) | -| `minecraft:entity.wither.spawn` | 凋零生成(压迫感) | -| `minecraft:entity.lightning_bolt.thunder` | 雷鸣 | -| `minecraft:entity.generic.explode` | 爆炸 | -| `minecraft:entity.witch.throw` | 药水投掷 | -| `minecraft:block.beacon.activate` | 信标激活 | -| `minecraft:block.anvil.land` | 铁砧落地 | -| `minecraft:ui.toast.challenge_complete` | 挑战完成 | -| `minecraft:entity.player.burp` | 吃食物音效 | -| `minecraft:entity.enderman.teleport` | 传送音效 | +| 音效 ID | 用途 | +| ----------------------------------------- | ------------------ | +| `minecraft:block.note_block.pling` | 铃铛提示 | +| `minecraft:block.note_block.bass` | 低音提示 | +| `minecraft:entity.experience_orb.pickup` | 经验球拾取 | +| `minecraft:entity.player.levelup` | 升级 | +| `minecraft:entity.ender_dragon.growl` | 龙吼(Boss 出场) | +| `minecraft:entity.wither.spawn` | 凋零生成(压迫感) | +| `minecraft:entity.lightning_bolt.thunder` | 雷鸣 | +| `minecraft:entity.generic.explode` | 爆炸 | +| `minecraft:entity.witch.throw` | 药水投掷 | +| `minecraft:block.beacon.activate` | 信标激活 | +| `minecraft:block.anvil.land` | 铁砧落地 | +| `minecraft:ui.toast.challenge_complete` | 挑战完成 | +| `minecraft:entity.player.burp` | 吃食物音效 | +| `minecraft:entity.enderman.teleport` | 传送音效 | ## 5.6 玩家进出特效 @@ -160,12 +180,29 @@ player.playSound("minecraft:entity.player.levelup", 1.0, 1.0); world.onPlayerJoin((entity, _tick) => { const pos = entity.position; world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); - world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 15); + world.spawnParticleCircle( + pos.x, + pos.y, + pos.z, + 1.5, + "minecraft:happy_villager", + 15, + ); }); world.onPlayerLeave((entity, _tick) => { const pos = entity.position; - world.spawnParticle("minecraft:cloud", pos.x, pos.y, pos.z, 10, 0.3, 0.3, 0.3, 0.01); + world.spawnParticle( + "minecraft:cloud", + pos.x, + pos.y, + pos.z, + 10, + 0.3, + 0.3, + 0.3, + 0.01, + ); }); ``` @@ -174,6 +211,7 @@ world.onPlayerLeave((entity, _tick) => { 这是教程四中设计模式的实际应用——一个完整的红蓝两队 PvP 小游戏,整合了事件、BossBar、计分板、队伍、粒子、烟花、边界缩圈、空投等所有系统。 **命令:** + - `!pvp join` — 加入游戏 - `!pvp leave` — 退出等待 - `!pvp start` — (OP) 开始游戏 @@ -181,6 +219,7 @@ world.onPlayerLeave((entity, _tick) => { - `!pvp status` — 查看状态 **特性:** + - 大厅倒计时 30 秒 → 游戏时长 300 秒 - 红蓝两队自动分配 + 队伍前缀 - 击杀计分 + 全局播报 + 烟花特效 @@ -215,9 +254,9 @@ const state: PvPState = { blueScore: 0, }; -let pvpGameTimer: number | null = null; -let pvpAirdropTimer: number | null = null; -let pvpLobbyTimer: number | null = null; +let pvpGameTimer: GameEventHandlerToken | null = null; +let pvpAirdropTimer: GameEventHandlerToken | null = null; +let pvpLobbyTimer: GameEventHandlerToken | null = null; // ── 初始化 ── world.setGameRule("keepInventory", false); @@ -269,9 +308,9 @@ world.onChat((entity, message, _tick) => { function startLobby(): void { state.phase = "starting"; let cd = 30; - pvpLobbyTimer = world.setInterval(() => { + pvpLobbyTimer = setInterval(() => { cd--; - if (cd <= 0 && pvpLobbyTimer) { world.clearInterval(pvpLobbyTimer); beginPvPGame(); } + if (cd <= 0 && pvpLobbyTimer) { pvpLobbyTimer.cancel(); beginPvPGame(); } else if (cd <= 5) { world.say(`§e游戏将在 §c${cd} §e秒后开始!`); } else if (cd % 10 === 0) { world.say(`§7游戏将在 ${cd} 秒后开始...`); } }, 20); @@ -322,7 +361,7 @@ function beginPvPGame(): void { // 游戏倒计时 let remaining = DURATION; - pvpGameTimer = world.setInterval(() => { + pvpGameTimer = setInterval(() => { remaining--; const progress = remaining / DURATION; const mins = Math.floor(remaining / 60); @@ -343,20 +382,20 @@ function beginPvPGame(): void { if (remaining === 60) { world.say("§c最后一分钟!"); } if (remaining === 30) { world.strikeLightning(ARENA.x, ARENA.y, ARENA.z, 0); } if (remaining <= 0 && pvpGameTimer) { - world.clearInterval(pvpGameTimer); + pvpGameTimer.cancel(); endPvPGame(); } }, 20); // 空投 - pvpAirdropTimer = world.setInterval(() => { + pvpAirdropTimer = setInterval(() => { if (state.phase !== "playing") return; const angle = Math.random() * Math.PI * 2; const dist = Math.random() * ARENA_RADIUS * 0.6; const dx = ARENA.x + Math.cos(angle) * dist; const dz = ARENA.z + Math.sin(angle) * dist; world.strikeLightning(dx, ARENA.y + 30, dz, 0); - world.setTimeout(() => { + setTimeout(() => { world.dropItem(dx, ARENA.y + 1, dz, "minecraft:ender_pearl", 2); world.dropItem(dx, ARENA.y + 1, dz, "minecraft:golden_apple", 2); world.launchFirework(dx, ARENA.y + 3, dz, "yellow", "ball"); @@ -408,7 +447,7 @@ world.onPlayerRespawn((entity, _tick) => { function endPvPGame(): void { state.phase = "ending"; world.removeBossbar("pvp_timer"); - if (pvpAirdropTimer) { world.clearInterval(pvpAirdropTimer); } + if (pvpAirdropTimer) { pvpAirdropTimer.cancel(); } let winner = "平局!"; let color = "e"; @@ -427,7 +466,7 @@ function endPvPGame(): void { // 烟花庆祝 for (let i = 0; i < 8; i++) { - world.setTimeout(() => { + setTimeout(() => { const cs = ["red", "gold", "green", "blue", "purple"]; const ss = ["ball", "large_ball", "star", "burst"]; world.launchFirework( @@ -441,7 +480,7 @@ function endPvPGame(): void { } // 30 秒后重置 - world.setTimeout(() => { + setTimeout(() => { state.phase = "waiting"; state.playersReady = 0; state.redScore = 0; state.blueScore = 0; world.hideScoreboard("sidebar"); @@ -546,7 +585,7 @@ function startWave(pos: GameVector3): void { world.say(`§c§l⚔ 第 ${wave} 波开始!§f生成 ${count} 只僵尸`); for (let i = 0; i < count; i++) { - world.setTimeout(() => { + setTimeout(() => { const x = pos.x + (Math.random() - 0.5) * 10; const z = pos.z + (Math.random() - 0.5) * 10; const zombie = world.spawnEntity("minecraft:zombie", new GameVector3(x, pos.y, z)); @@ -565,7 +604,7 @@ world.onEntityDeath((entity, killer, _tick) => { mobsAlive--; if (mobsAlive <= 0) { world.say(`§a§l✔ 第 ${wave} 波清除!`); - world.setTimeout(() => startWave(entity.position), 200); + setTimeout(() => startWave(entity.position), 200); } }); ``` @@ -580,7 +619,7 @@ world.onChat((entity, message, _tick) => { if (message === "!sounds") { const notes = [1.0, 1.2, 1.5, 2.0]; notes.forEach((pitch, i) => { - world.setTimeout(() => { + setTimeout(() => { p.playSound("minecraft:block.note_block.pling", 1.0, pitch); }, i * 100); }); @@ -591,8 +630,6 @@ world.onChat((entity, message, _tick) => { }); ``` ---- - 所有示例代码均已通过 `tsc --noEmit`、`eslint` 和 `node build.mjs` 完整验证。可直接使用。 更多 API 细节请参考 `docs/api/` 目录中的完整 API 文档。 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md index 3c0ad66..2c42e82 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/06-client-scripting.md @@ -34,6 +34,7 @@ colorzone 项目包含完整的客户端脚本示例(`src/client/app.ts`), | ui | 屏幕文字 | F6 显示设置 | | chat | 聊天命令 | `!fav` `!mob` | | audio | 自定义音效 | V 键 | +| fog | 雾颜色和距离控制 | — | ## 6.3 client — 生命周期 @@ -50,6 +51,8 @@ client.onTick(() => { }); ``` +与其他事件 API 一样,`client.onTick()` 会返回 `GameEventHandlerToken`;不再需要监听时调用 `token.cancel()`。 + **性能提示:** 客户端 onTick 也在主线程执行。避免密集循环,用取模运算降低实际执行频率。 ## 6.4 input — 键盘输入 @@ -135,7 +138,30 @@ const musicVol = audio.getVolume("music"); // 读取当前音量 audio.playSound("colorzone:victory_fanfare", 1.0, 1.0); ``` -## 6.8 storage — 客户端本地存储 +## 6.8 fog — 雾效控制 + +覆盖 Minecraft 的雾颜色和渲染距离: + +```js +// 设置雾颜色(RGB 0-255) +client.setFogColor(255, 100, 50); + +// 设置雾距离(单位:方块) +client.setFogStartDistance(10); // 雾从 10 格外开始 +client.setFogEndDistance(50); // 50 格外完全被雾遮挡 + +// 读取当前雾颜色 +const color = client.getFogColor(); // 返回 GameRGBColor 或 null + +// 恢复 Minecraft 默认雾效果 +client.resetFog(); +``` + +::: warning +雾效修改在客户端本地生效。可通过 `remoteChannel` 让服务端指令触发客户端雾效变化,实现服务端控制的天气效果。 +::: + +## 6.9 storage — 客户端本地存储 客户端的 `storage` 与服务端用法相同,但数据存储在玩家本地: @@ -192,7 +218,7 @@ const page = notes.list({ pageSize: 10, ascending: false }); const entries = page.getCurrentPage(); ``` -## 6.9 db — 客户端 SQLite +## 6.10 db — 客户端 SQLite 客户端也支持 SQLite(需要 `minecraft-sqlite-jdbc` 模组): @@ -240,9 +266,11 @@ function searchMobs(keyword: string): void { } ``` -> 未安装 `minecraft-sqlite-jdbc` 时,`db.isAvailable()` 返回 `false`,所有 SQL 调用静默返回空结果。 +::: warning +未安装 `minecraft-sqlite-jdbc` 时,`db.isAvailable()` 返回 `false`,所有 SQL 调用静默返回空结果。 +::: -## 6.10 http — 客户端 HTTP 请求 +## 6.11 http — 客户端 HTTP 请求 ```js // 同步 GET @@ -284,7 +312,7 @@ http.fetch("https://httpbin.org/post", { }); ``` -## 6.11 remoteChannel — 两端通讯 +## 6.12 remoteChannel — 两端通讯 这是客户端脚本最强大的功能:服务端和客户端可以互相发送事件。 @@ -361,22 +389,15 @@ remoteChannel.onServerEvent((event) => { ### 检测客户端兼容性 -```js -// 服务端检测玩家是否安装了 Box3JS 客户端 -if (entity.player.hasBox3JSClientMod()) { - // 可以发送客户端事件 - remoteChannel.sendClientEvent(entity, { type: "custom_ui", ... }); -} else { - // 降级到聊天消息 - entity.player.directMessage("请安装 Box3JS 客户端以获得完整体验"); -} -``` +无需手动检测。`remoteChannel.sendClientEvent()` 使用可选数据包,未安装 Box3JS 客户端的玩家会自动忽略,不会报错或断线。可以放心向所有玩家发送。 ### 通讯数据格式 -> **重要:** 跨网络传输的数据必须是 JSON 可序列化的类型(string、number、boolean、null、普通对象、数组)。不能传函数、Java 对象或 `GameVector3`。 +::: warning +跨网络传输的数据必须是 JSON 可序列化的类型(string、number、boolean、null、普通对象、数组)。不能传函数、Java 对象或 `GameVector3`。 +::: -## 6.12 完整实战:客户端 HUD 状态栏 +## 6.13 完整实战:客户端 HUD 状态栏 综合运用 input、ui、remoteChannel 和 storage 创建一个自定义 HUD: @@ -444,7 +465,7 @@ ui.showTitle("§6自定义 HUD 已启动", "§7F6=坐标 F7=FPS", 10, 40, 10); console.log("[HUD] Client HUD demo loaded"); ``` -## 6.13 客户端脚本调试 +## 6.14 客户端脚本调试 客户端脚本的 `console.log` 输出到**客户端日志**(不是服务端)。在 Minecraft 启动器或日志目录中查看。 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md index bc0085d..d910e37 100644 --- a/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/README.md @@ -1,10 +1,8 @@ # Box3JS 教程 -从零开始学习 Box3JS 脚本开发。每个教程约 10-15 分钟,包含可直接运行的完整代码。 - ## 学习路径 -``` +```text 教程一 教程二 教程三 教程四 教程五 │ │ │ │ │ 从零开始 → 玩家操控 → 事件系统 → 高级游戏系统 → 实战小游戏 @@ -34,7 +32,7 @@ ## 技能进阶路线 -``` +```text 入门 进阶 高级 │ │ │ │ 教程一: 从零开始 │ 教程三: 事件与实体 │ 教程五: 实战小游戏 @@ -80,7 +78,7 @@ world.onChat((entity, message) => { return true; }); -world.setInterval(() => { +setInterval(() => { world.say("当前在线: " + world.querySelectorAll("*").length + " 人"); }, 6000); ``` diff --git a/Box3JS-NeoForge-1.21.1/package-lock.json b/Box3JS-NeoForge-1.21.1/package-lock.json new file mode 100644 index 0000000..bf54d45 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/package-lock.json @@ -0,0 +1,2513 @@ +{ + "name": "box3js", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "box3js", + "version": "1.0.0", + "devDependencies": { + "vitepress": "^1.6.3" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.1.tgz", + "integrity": "sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.1.tgz", + "integrity": "sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.1.tgz", + "integrity": "sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.1.tgz", + "integrity": "sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.1.tgz", + "integrity": "sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.1.tgz", + "integrity": "sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.1.tgz", + "integrity": "sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz", + "integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.1.tgz", + "integrity": "sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.1.tgz", + "integrity": "sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.1.tgz", + "integrity": "sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.1.tgz", + "integrity": "sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.1.tgz", + "integrity": "sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.1.tgz", + "integrity": "sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.82", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.82.tgz", + "integrity": "sha512-4p978qHx8eD/QBOhgBzp/p7uS3OO2KCnVpFPJTUvuhuDXv1Hr4RcxcZ5MWc6ptkf/3Dlb1xb23068OtPyx10mA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", + "integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.18.1", + "@algolia/client-abtesting": "5.52.1", + "@algolia/client-analytics": "5.52.1", + "@algolia/client-common": "5.52.1", + "@algolia/client-insights": "5.52.1", + "@algolia/client-personalization": "5.52.1", + "@algolia/client-query-suggestions": "5.52.1", + "@algolia/client-search": "5.52.1", + "@algolia/ingestion": "1.52.1", + "@algolia/monitoring": "1.52.1", + "@algolia/recommend": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/package.json b/Box3JS-NeoForge-1.21.1/package.json new file mode 100644 index 0000000..de6d00e --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/package.json @@ -0,0 +1,15 @@ +{ + "name": "box3js", + "version": "1.0.0", + "private": true, + "description": "Box3JS — JavaScript/TypeScript scripting engine for Minecraft NeoForge 1.21.1", + "type": "module", + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "devDependencies": { + "vitepress": "^1.6.3" + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java index e450ba4..50216e1 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JS.java @@ -2,27 +2,18 @@ import com.box3lab.box3js.client.Box3JSClientEngine; import com.box3lab.box3js.client.Box3JSGuiProxy; -import com.box3lab.box3js.client.screen.Box3JSScriptContainerScreen; import com.box3lab.box3js.registries.Box3JSRecipeManager; import com.box3lab.box3js.script.Box3ScriptCommand; import com.box3lab.box3js.script.Box3ScriptEngine; import com.box3lab.box3js.script.Box3JSGuiServerHandler; -import com.box3lab.box3js.script.Box3JSScriptContainerMenu; import com.mojang.logging.LogUtils; -import net.minecraft.core.registries.Registries; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.flag.FeatureFlags; -import net.minecraft.world.inventory.MenuType; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; -import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; -import net.neoforged.neoforge.registries.DeferredRegister; -import java.util.function.Supplier; -import java.nio.file.Path; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -45,20 +36,7 @@ public class Box3JS { /** Tracks which connected players have Box3JS installed on their client. */ public static final Set clientsWithBox3JS = ConcurrentHashMap.newKeySet(); - // ── Registries ── - - private static final DeferredRegister> MENU_TYPES = - DeferredRegister.create(Registries.MENU, MODID); - - /** Single MenuType for all script container sizes (1-6 rows). */ - public static final Supplier> SCRIPT_CONTAINER_MENU = - MENU_TYPES.register("script_container", - () -> new MenuType<>(new Box3JSScriptContainerMenu.Factory(), FeatureFlags.DEFAULT_FLAGS)); - public Box3JS(IEventBus modEventBus, ModContainer modContainer) { - // Register registries - MENU_TYPES.register(modEventBus); - // Register custom payloads modEventBus.addListener(RegisterPayloadHandlersEvent.class, event -> { var registrar = event.registrar("1"); @@ -128,10 +106,6 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { ); }); - // Register client-side screen for the script container - modEventBus.addListener(RegisterMenuScreensEvent.class, event -> - event.register(SCRIPT_CONTAINER_MENU.get(), Box3JSScriptContainerScreen::new)); - // Script commands NeoForge.EVENT_BUS.addListener(Box3ScriptCommand::register); @@ -234,7 +208,7 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { event.getSource().getEntity()); }); - // Auto-load scripts from config/box3/script//app.js on server start + // Auto-load server scripts from config/box3/script//dist/server.js on server start NeoForge.EVENT_BUS.addListener((ServerStartedEvent event) -> { Box3ScriptEngine.get().autoLoad(event.getServer()); Box3JSRecipeManager.init(event.getServer()); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java index e4f3976..8d6ff3c 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/Box3JSNetwork.java @@ -208,6 +208,8 @@ public static void sendClientScripts(ServerPlayer player) { Box3JS.LOGGER.error("Failed to send client script '{}': {}", name, e.getMessage()); } }); - } catch (IOException ignored) {} + } catch (IOException e) { + Box3JS.LOGGER.warn("Failed to scan client script directory: {}", scriptDir, e); + } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java index 4108a4d..6cfd972 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientDatabase.java @@ -25,7 +25,11 @@ public class Box3JSClientDatabase extends Box3DatabaseBase { public Box3JSClientDatabase(java.io.File gameDir) { this.dataDir = gameDir.toPath().resolve("box3").resolve("client-db"); - try { Files.createDirectories(dataDir); } catch (IOException ignored) {} + try { + Files.createDirectories(dataDir); + } catch (IOException e) { + LOGGER.warn("Failed to create client database directory: {}", dataDir, e); + } } public void setProjectName(String name) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java index 4f57e6d..56f9fbd 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java @@ -2,6 +2,7 @@ import com.box3lab.box3js.Box3JSNetwork; import com.box3lab.box3js.script.Box3JSQueryResult; +import com.box3lab.box3js.script.Box3Rhino; import com.box3lab.box3js.script.GameBounds3; import com.box3lab.box3js.script.GameEventHandlerToken; import com.box3lab.box3js.script.GameQuaternion; @@ -25,6 +26,7 @@ import org.mozilla.javascript.*; import org.slf4j.Logger; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,6 +41,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.client.multiplayer.ServerData; import net.neoforged.neoforge.client.event.RenderGuiEvent; +import net.neoforged.neoforge.client.event.ViewportEvent; /** * Singleton client-side Rhino engine. @@ -82,6 +85,20 @@ public class Box3JSClientEngine { private Box3JSGuiProxy activeGuiProxy; private volatile boolean dbWarningShown; + // ── Fog state (client-side rendering) ── + private volatile boolean fogColorEnabled; + private volatile float fogColorR = 1.0f; + private volatile float fogColorG = 1.0f; + private volatile float fogColorB = 1.0f; + private volatile boolean fogDistanceEnabled; + private volatile float fogStartDist = -1f; + private volatile float fogEndDist = -1f; + private volatile boolean fogRegistered; + + // ── Timers ── + private final List timers = new CopyOnWriteArrayList<>(); + private final AtomicInteger timerIdCounter = new AtomicInteger(0); + private static final Map KEY_MAP = new HashMap<>(); static { // Letters a-z @@ -105,10 +122,13 @@ public class Box3JSClientEngine { KEY_MAP.put("delete", InputConstants.KEY_DELETE); KEY_MAP.put("left_shift", InputConstants.KEY_LSHIFT); KEY_MAP.put("right_shift", InputConstants.KEY_RSHIFT); + KEY_MAP.put("shift", InputConstants.KEY_LSHIFT); KEY_MAP.put("left_ctrl", InputConstants.KEY_LCONTROL); KEY_MAP.put("right_ctrl", InputConstants.KEY_RCONTROL); + KEY_MAP.put("ctrl", InputConstants.KEY_LCONTROL); KEY_MAP.put("left_alt", InputConstants.KEY_LALT); KEY_MAP.put("right_alt", InputConstants.KEY_RALT); + KEY_MAP.put("alt", InputConstants.KEY_LALT); KEY_MAP.put("up", InputConstants.KEY_UP); KEY_MAP.put("down", InputConstants.KEY_DOWN); KEY_MAP.put("left", InputConstants.KEY_LEFT); @@ -126,8 +146,7 @@ private Box3JSClientEngine() {} private void init() { if (initialized) return; - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); + Context cx = Box3Rhino.enterInterpretedContext(); try { scope = cx.initStandardObjects(); @@ -153,6 +172,28 @@ private void init() { ScriptableObject.putProperty(scope, "Box3JSQueryResult", new NativeJavaClass(scope, Box3JSQueryResult.class)); + // -- setTimeout / setInterval global functions ------------------- + ScriptableObject.putProperty(scope, "setTimeout", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleTimeout(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); + ScriptableObject.putProperty(scope, "setInterval", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleInterval(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); + // -- client global (lifecycle + server-bound actions) ---------- ScriptableObject clientObj = (ScriptableObject) cx.newObject(scope); @@ -162,7 +203,7 @@ private void init() { public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (args.length > 0 && args[0] instanceof Function fn) { - tickCallbacks.add(() -> { + Runnable callback = () -> { Context cx2 = Context.enter(); try { fn.call(cx2, scope, scope, new Object[0]); @@ -171,7 +212,9 @@ public Object call(Context cx, Scriptable scope, } finally { Context.exit(); } - }); + }; + tickCallbacks.add(callback); + return new GameEventHandlerToken(() -> tickCallbacks.remove(callback)); } return Undefined.instance; } @@ -272,6 +315,70 @@ public Object call(Context cx, Scriptable scope, } }); + // client.getFogColor() -> GameRGBColor or null + ScriptableObject.putProperty(clientObj, "getFogColor", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (!fogColorEnabled) return null; + return new GameRGBColor(fogColorR, fogColorG, fogColorB); + } + }); + + // client.setFogColor(r, g, b) — r,g,b each 0-255 + ScriptableObject.putProperty(clientObj, "setFogColor", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 3) return Undefined.instance; + fogColorR = Math.max(0f, Math.min(1f, ((Number) args[0]).floatValue() / 255f)); + fogColorG = Math.max(0f, Math.min(1f, ((Number) args[1]).floatValue() / 255f)); + fogColorB = Math.max(0f, Math.min(1f, ((Number) args[2]).floatValue() / 255f)); + fogColorEnabled = true; + registerFogListener(); + return Undefined.instance; + } + }); + + // client.setFogStartDistance(distanceInBlocks) + ScriptableObject.putProperty(clientObj, "setFogStartDistance", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1) return Undefined.instance; + fogStartDist = Math.max(0f, ((Number) args[0]).floatValue()); + fogDistanceEnabled = true; + registerFogListener(); + return Undefined.instance; + } + }); + + // client.setFogEndDistance(distanceInBlocks) — maps to Box3 maxFog + ScriptableObject.putProperty(clientObj, "setFogEndDistance", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + if (args.length < 1) return Undefined.instance; + fogEndDist = Math.max(0f, ((Number) args[0]).floatValue()); + fogDistanceEnabled = true; + registerFogListener(); + return Undefined.instance; + } + }); + + // client.resetFog() — restore Minecraft default fog + ScriptableObject.putProperty(clientObj, "resetFog", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + fogColorEnabled = false; + fogDistanceEnabled = false; + fogStartDist = -1f; + fogEndDist = -1f; + return Undefined.instance; + } + }); + ScriptableObject.putProperty(scope, "client", clientObj); // -- audio global (sound playback) ------------------------------ @@ -775,8 +882,7 @@ public Object call(Context cx, Scriptable scope, public void loadScript(String projectName, String scriptSource) { if (!initialized) init(); - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); + Context cx = Box3Rhino.enterInterpretedContext(); try { if (!projectName.equals(this.currentProject)) { this.currentProject = projectName; @@ -822,6 +928,64 @@ private void fireTick() { LOGGER.error("Client tick callback error", e); } } + fireTimers(); + } + + // ── Timers ── + + private int scheduleTimeout(Function handler, int ticks) { + int id = timerIdCounter.incrementAndGet(); + timers.add(new TimerEntry(id, handler, ticks, 0)); + return id; + } + + private int scheduleInterval(Function handler, int ticks) { + int id = timerIdCounter.incrementAndGet(); + timers.add(new TimerEntry(id, handler, ticks, ticks)); + return id; + } + + private void clearTimer(int id) { + timers.removeIf(t -> t.id == id); + } + + private void fireTimers() { + var toFire = new ArrayList(); + var toRemove = new ArrayList(); + for (var t : timers) { + if (--t.remaining <= 0) { + toFire.add(t); + if (t.interval == 0) + toRemove.add(t); + else + t.remaining = t.interval; + } + } + timers.removeAll(toRemove); + for (var t : toFire) { + Context cx = Context.enter(); + try { + t.handler.call(cx, scope, scope, new Object[0]); + } catch (Exception e) { + LOGGER.error("Client timer error", e); + } finally { + Context.exit(); + } + } + } + + static class TimerEntry { + final int id; + final Function handler; + int remaining; + final int interval; + + TimerEntry(int id, Function handler, int remaining, int interval) { + this.id = id; + this.handler = handler; + this.remaining = remaining; + this.interval = interval; + } } private void registerKeyListener() { @@ -891,8 +1055,7 @@ public void fireClientEvent(String projectName, long tick, String eventJson) { List handlers = clientEventHandlers.getOrDefault(projectName, List.of()); if (handlers.isEmpty()) return; - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); + Context cx = Box3Rhino.enterInterpretedContext(); try { scope.put("_arg", scope, eventJson); Object args = cx.evaluateString(scope, @@ -981,6 +1144,37 @@ private void registerMouseListener() { }); } + // ── Fog override listener ── + + private void registerFogListener() { + if (fogRegistered) return; + Minecraft.getInstance().execute(() -> { + if (fogRegistered) return; + // Fog color override + NeoForge.EVENT_BUS.addListener(ViewportEvent.ComputeFogColor.class, event -> { + if (fogColorEnabled) { + event.setRed(fogColorR); + event.setGreen(fogColorG); + event.setBlue(fogColorB); + } + }); + // Fog distance override + NeoForge.EVENT_BUS.addListener(ViewportEvent.RenderFog.class, event -> { + if (fogDistanceEnabled) { + if (fogStartDist >= 0) { + event.setNearPlaneDistance(fogStartDist); + } + if (fogEndDist >= 0) { + event.setFarPlaneDistance(fogEndDist); + } + event.setCanceled(true); + } + }); + fogRegistered = true; + LOGGER.debug("Fog rendering listener registered"); + }); + } + // ── Draw text entry ── private record DrawTextEntry(int x, int y, String text, int color) {} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java index ec1907c..57766c2 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java @@ -1,13 +1,13 @@ package com.box3lab.box3js.client; import com.box3lab.box3js.script.Box3StorageTypes; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import com.box3lab.box3js.script.Box3StorageSupport; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; +import org.slf4j.Logger; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -20,8 +20,7 @@ */ public class Box3JSClientStorage { - private static final Gson GSON = new Gson(); - private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + private static final Logger LOGGER = LogUtils.getLogger(); private final Path baseDir; private final String projectName; @@ -30,7 +29,11 @@ public class Box3JSClientStorage { public Box3JSClientStorage(java.io.File gameDir, String projectName) { this.baseDir = gameDir.toPath().resolve("box3").resolve("client-storage"); this.projectName = projectName; - try { Files.createDirectories(baseDir); } catch (IOException ignored) {} + try { + Files.createDirectories(baseDir); + } catch (IOException e) { + LOGGER.warn("Failed to create client storage directory: {}", baseDir, e); + } } public String getKey() { return ""; } @@ -53,41 +56,14 @@ public class GameDataStorage { GameDataStorage(String name) { this.name = name; - String[] parts = name.split("/"); - Path dir = baseDir; - for (int i = 0; i < parts.length - 1; i++) { - String seg = sanitize(parts[i]); - if (!seg.isEmpty()) dir = dir.resolve(seg); - } - String file = sanitize(parts[parts.length - 1]); - if (file.isEmpty()) file = "default"; - this.path = dir.resolve(file + ".json"); - this.data = cache.computeIfAbsent(path, p -> { - if (Files.exists(p)) { - try { - String json = Files.readString(p); - Map map = GSON.fromJson(json, MAP_TYPE); - return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) - : Collections.synchronizedMap(new LinkedHashMap<>()); - } catch (IOException e) { - return Collections.synchronizedMap(new LinkedHashMap<>()); - } - } - return Collections.synchronizedMap(new LinkedHashMap<>()); - }); - } - - private String sanitize(String s) { - return s.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + this.path = Box3StorageSupport.resolveStoragePath(baseDir, name); + this.data = cache.computeIfAbsent(path, p -> Box3StorageSupport.readData(p, "client")); } public String getKey() { return name; } private void persist() { - try { - Files.createDirectories(path.getParent()); - Files.writeString(path, GSON.toJson(data)); - } catch (IOException ignored) {} + Box3StorageSupport.writeData(path, data, "client"); } // ── Basic API ── @@ -100,7 +76,7 @@ public void set(String key, Object value) { if (existing != null) { existing.value = value; existing.updateTime = now; - existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + existing.version = Box3StorageTypes.newVersion(now); } else { data.put(key, new Box3StorageTypes.ValueEntry(value, now)); } @@ -135,7 +111,7 @@ public void update(String key, Function handler) { Context.exit(); } entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); persist(); } } @@ -165,7 +141,7 @@ public double increment(String key, double value) { entry.value = delta; } entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); } else { entry = new Box3StorageTypes.ValueEntry(delta, now); data.put(key, entry); @@ -255,7 +231,7 @@ private double extractSortValue(Object value, String target) { public void destroy() { synchronized (data) { cache.remove(path); - try { Files.deleteIfExists(path); } catch (IOException ignored) {} + Box3StorageSupport.deleteData(path, "client"); } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java deleted file mode 100644 index 22bfd61..0000000 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/client/screen/Box3JSScriptContainerScreen.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.box3lab.box3js.client.screen; - -import com.box3lab.box3js.script.Box3JSScriptContainerMenu; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.entity.player.Inventory; - -public class Box3JSScriptContainerScreen extends AbstractContainerScreen { - - private static final ResourceLocation CONTAINER_BACKGROUND = - ResourceLocation.fromNamespaceAndPath("minecraft", "textures/gui/container/generic_54.png"); - - private final int rows; - - public Box3JSScriptContainerScreen(Box3JSScriptContainerMenu menu, Inventory playerInventory, - Component title) { - super(menu, playerInventory, title); - this.rows = menu.getRows(); - this.imageHeight = 114 + this.rows * 18; - this.inventoryLabelY = this.imageHeight - 94; - } - - @Override - public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - super.render(guiGraphics, mouseX, mouseY, partialTick); - this.renderTooltip(guiGraphics, mouseX, mouseY); - } - - @Override - protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) { - int x = (this.width - this.imageWidth) / 2; - int y = (this.height - this.imageHeight) / 2; - - // Draw chest background — only render the top part (dynamic rows) - int containerRows = this.rows; - int texHeight = 222; // Full 6-row texture height - int renderHeight = 18 + containerRows * 18; // Top border + rows + gap above inventory - - // Top of the container (including top border) - guiGraphics.blit(CONTAINER_BACKGROUND, x, y, 0, 0, this.imageWidth, 17); - // Middle rows - for (int row = 0; row < containerRows; row++) { - guiGraphics.blit(CONTAINER_BACKGROUND, x, y + 17 + row * 18, 0, 17, this.imageWidth, 18); - } - // Gap + player inventory area - int bottomStart = 17 + containerRows * 18; - int bottomTexY = 215 - 96; // Transition area above player inventory in texture - // Actually render the bottom inventory section from the 6-row texture - int invSectionStart = 17 + containerRows * 18; - // Copy the player inventory from the fixed texture - for (int row = 0; row < 4; row++) { - int srcY = 17 + 5 * 18 + row * 18; // Start from row 5 in the 6-row texture (skip rows) - // Actually use the bottom inventory strip from the texture - guiGraphics.blit(CONTAINER_BACKGROUND, x, y + invSectionStart + row * 18, - 0, 125 + row * 18, this.imageWidth, 18); - } - // Render the remaining lines below - guiGraphics.blit(CONTAINER_BACKGROUND, x, y + invSectionStart + 4 * 18, - 0, 215 - 7, this.imageWidth, 7); - } -} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java new file mode 100644 index 0000000..07535d1 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSConsole.java @@ -0,0 +1,52 @@ +package com.box3lab.box3js.script; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import java.util.function.Supplier; + +/** + * Console backend exposed to server-side scripts. + */ +public class Box3JSConsole { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final Supplier projectName; + + public Box3JSConsole(Supplier projectName) { + this.projectName = projectName; + } + + private String format(Object... args) { + StringBuilder sb = new StringBuilder(); + String project = projectName.get(); + if (project != null && !project.isEmpty()) { + sb.append('[').append(project).append("] "); + } + for (Object arg : args) { + sb.append(arg).append(' '); + } + return sb.toString().trim(); + } + + public void log(Object... args) { + LOGGER.info("[Box3JS] {}", format(args)); + } + + public void debug(Object... args) { + LOGGER.debug("[Box3JS] {}", format(args)); + } + + public void warn(Object... args) { + LOGGER.warn("[Box3JS] {}", format(args)); + } + + public void error(Object... args) { + LOGGER.error("[Box3JS] {}", format(args)); + } + + public void clear() { + // Server logs cannot be cleared reliably; keep console.clear() as a safe no-op. + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java index f749af6..f142597 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSDatabase.java @@ -62,7 +62,8 @@ public Box3JSDatabase(Path configDir, Box3ScriptEngine engine) { this.engine = engine; try { Files.createDirectories(dataDir); - } catch (java.io.IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to create database directory: {}", dataDir, e); } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java index 6fe690c..5aba6f3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEntity.java @@ -2,6 +2,7 @@ import net.minecraft.ChatFormatting; import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; @@ -16,13 +17,17 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Function; +import org.slf4j.Logger; import java.util.Map; import java.util.function.Consumer; public class Box3JSEntity { + private static final Logger LOGGER = LogUtils.getLogger(); + private final Entity entity; private final MinecraftServer server; private final Box3ScriptEngine engine; @@ -50,8 +55,8 @@ public String getId() { public boolean isPlayer() { return entity instanceof ServerPlayer; } public String getEntityType() { - var key = entity.getType().builtInRegistryHolder().key(); - return key != null ? key.location().toString() : "unknown"; + var key = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()); + return key != null ? key.toString() : "unknown"; } public Box3JSPlayer getPlayer() { @@ -387,7 +392,11 @@ public void setPersistent(boolean v) { public void setText(String text) { if (entity instanceof net.minecraft.world.entity.Display.TextDisplay td) { - try { _tdSetText.invoke(td, Component.literal(text)); } catch (Exception ignored) {} + try { + _tdSetText.invoke(td, Component.literal(text)); + } catch (Exception e) { + LOGGER.warn("Failed to set TextDisplay text for entity {}", entity.getStringUUID(), e); + } } } @@ -401,7 +410,9 @@ public void setTextColor(GameRGBColor color) { int b = (int) (Math.max(0, Math.min(1, color.b)) * 255); int rgb = (r << 16) | (g << 8) | b; _tdSetText.invoke(td, Component.literal(text).withColor(rgb)); - } catch (Exception ignored) {} + } catch (Exception e) { + LOGGER.warn("Failed to set TextDisplay text color for entity {}", entity.getStringUUID(), e); + } } } @@ -411,7 +422,11 @@ public void setTextBackgroundColor(GameRGBAColor color) { int g = (int) (Math.max(0, Math.min(1, color.g)) * 255); int b = (int) (Math.max(0, Math.min(1, color.b)) * 255); int a = (int) (Math.max(0, Math.min(1, color.a)) * 255); - try { _tdSetBgColor.invoke(td, (a << 24) | (r << 16) | (g << 8) | b); } catch (Exception ignored) {} + try { + _tdSetBgColor.invoke(td, (a << 24) | (r << 16) | (g << 8) | b); + } catch (Exception e) { + LOGGER.warn("Failed to set TextDisplay background color for entity {}", entity.getStringUUID(), e); + } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java index a2aba6e..31d4aea 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSGuiServerHandler.java @@ -1,15 +1,17 @@ package com.box3lab.box3js.script; -import com.box3lab.box3js.Box3JS; import com.box3lab.box3js.Box3JSNetwork; +import com.mojang.logging.LogUtils; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.MenuProvider; import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.MenuType; import net.neoforged.neoforge.network.PacketDistributor; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; /** * Handles client→server GUI packets on the server thread. @@ -17,8 +19,21 @@ */ public final class Box3JSGuiServerHandler { + private static final Logger LOGGER = LogUtils.getLogger(); + private Box3JSGuiServerHandler() {} + /** Vanilla generic chest menu types mapped by row count (1-6). */ + private static final MenuType[] CHEST_TYPES = { + MenuType.GENERIC_9x1, MenuType.GENERIC_9x2, MenuType.GENERIC_9x3, + MenuType.GENERIC_9x4, MenuType.GENERIC_9x5, MenuType.GENERIC_9x6 + }; + + private static MenuType chestType(int rows) { + int idx = Math.clamp(rows, 1, 6) - 1; + return CHEST_TYPES[idx]; + } + private static final Map activeGuis = new ConcurrentHashMap<>(); private static class ActiveGui { @@ -43,7 +58,7 @@ public static void handleOpen(ServerPlayer player, String title, int rows, Strin MenuProvider provider = new SimpleMenuProvider((containerId, inv, p) -> { Box3JSScriptContainerMenu menu = new Box3JSScriptContainerMenu( - Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, inv, rows, controller); + chestType(rows), containerId, inv, rows, controller); controller.setMenu(menu); // Populate initial slots from JSON string like {"0":"minecraft:diamond","1":"minecraft:stone"} @@ -68,7 +83,9 @@ public static void handleOpen(ServerPlayer player, String title, int rows, Strin if (item != null && slot >= 0 && slot < rows * 9) { menu.getContainer().setItem(slot, new net.minecraft.world.item.ItemStack(item, 1)); } - } catch (NumberFormatException | IndexOutOfBoundsException ignored) {} + } catch (NumberFormatException | IndexOutOfBoundsException e) { + LOGGER.debug("Ignoring invalid GUI slot entry '{}'", pair, e); + } } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java index 0686430..a43de4b 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java @@ -17,15 +17,20 @@ import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.GameType; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.ScriptableObject; +import org.slf4j.Logger; import java.util.Map; import java.util.function.Consumer; +@SuppressWarnings("deprecation") public class Box3JSPlayer { + private static final Logger LOGGER = LogUtils.getLogger(); + private final ServerPlayer player; private final MinecraftServer server; private final Box3ScriptEngine engine; @@ -361,8 +366,8 @@ public Object dialog(NativeObject config) { // ---- Chat (player-level) ---- - public void onChat(Function handler) { - engine.setPlayerChatHandler(player.getUUID(), handler); + public GameEventHandlerToken onChat(Function handler) { + return new GameEventHandlerToken(engine.setPlayerChatHandler(player.getUUID(), handler)); } // ---- Link ---- @@ -380,14 +385,16 @@ public void link(String href) { public void setPlayerListName(String name) { try { - java.lang.reflect.Field f = net.minecraft.world.entity.player.Player.class.getDeclaredField("displayName"); + java.lang.reflect.Field f = net.minecraft.world.entity.player.Player.class.getDeclaredField("displayname"); f.setAccessible(true); f.set(player, Component.literal(name)); server.getPlayerList().broadcastAll( new net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket( java.util.EnumSet.of(net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), java.util.List.of(player))); - } catch (Exception ignored) {} + } catch (Exception e) { + LOGGER.warn("Failed to set player list name for {}", player.getGameProfile().getName(), e); + } } // ---- Look at (MC extension) ---- diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java index b938108..c69ada3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java @@ -71,7 +71,7 @@ public void broadcastClientEvent(Object event) { // ── onServerEvent(handler) ── - public Object onServerEvent(Function handler) { + public GameEventHandlerToken onServerEvent(Function handler) { String project = engine.getCurrentProject(); Function stored = engine.bus.addServerEventHandler(project, handler); return new GameEventHandlerToken(() -> diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java index 9f274f0..fc66ef2 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSResponse.java @@ -60,9 +60,7 @@ public Box3JSResponse(HttpResponse r, String responseType, long maxBodyS if (responseType != null && ok && body != null && body.length > 0) { switch (responseType) { - case "json" -> { - try { this.parsedBody = json(); } catch (Exception ignored) {} - } + case "json" -> this.parsedBody = json(); case "text" -> this.parsedBody = text(); case "arrayBuffer" -> this.parsedBody = arrayBuffer(); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java index a58c7a9..d472ba9 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSScriptContainerMenu.java @@ -1,14 +1,10 @@ package com.box3lab.box3js.script; -import com.box3lab.box3js.Box3JS; -import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.SimpleContainer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.*; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.flag.FeatureFlags; -import net.neoforged.neoforge.network.IContainerFactory; public class Box3JSScriptContainerMenu extends AbstractContainerMenu { @@ -106,26 +102,6 @@ public ItemStack quickMoveStack(Player player, int index) { return copy; } - // ---- Factory for MenuType registration ---- - - public static class Factory implements MenuType.MenuSupplier, - IContainerFactory { - @Override - public Box3JSScriptContainerMenu create(int containerId, Inventory playerInventory) { - // Default 3 rows — used as fallback if IContainerFactory path fails - return new Box3JSScriptContainerMenu( - Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, playerInventory, 3, null); - } - - @Override - public Box3JSScriptContainerMenu create(int containerId, Inventory playerInventory, - RegistryFriendlyByteBuf extraData) { - int rows = extraData.readVarInt(); - return new Box3JSScriptContainerMenu( - Box3JS.SCRIPT_CONTAINER_MENU.get(), containerId, playerInventory, rows, null); - } - } - // ---- Player inventory helper ---- private void addPlayerInventory(Inventory playerInventory, int x, int y) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java index b255cd4..ffe8963 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSStorage.java @@ -1,11 +1,10 @@ package com.box3lab.box3js.script; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; import org.mozilla.javascript.Function; +import org.slf4j.Logger; import java.io.IOException; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -13,8 +12,7 @@ public class Box3JSStorage { - private static final Gson GSON = new Gson(); - private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + private static final Logger LOGGER = LogUtils.getLogger(); private final Path baseDir; private final Box3ScriptEngine engine; @@ -23,7 +21,11 @@ public class Box3JSStorage { public Box3JSStorage(Path configDir, Box3ScriptEngine engine) { this.baseDir = configDir.resolve("box3").resolve("storage"); this.engine = engine; - try { Files.createDirectories(baseDir); } catch (IOException ignored) {} + try { + Files.createDirectories(baseDir); + } catch (IOException e) { + LOGGER.warn("Failed to create storage directory: {}", baseDir, e); + } } // ---- GameStorage ---- @@ -55,32 +57,8 @@ public class GameDataStorage { GameDataStorage(String name) { this.name = name; - String[] parts = name.split("/"); - Path dir = baseDir; - for (int i = 0; i < parts.length - 1; i++) { - String seg = sanitize(parts[i]); - if (!seg.isEmpty()) dir = dir.resolve(seg); - } - String file = sanitize(parts[parts.length - 1]); - if (file.isEmpty()) file = "default"; - this.path = dir.resolve(file + ".json"); - this.data = cache.computeIfAbsent(path, p -> { - if (Files.exists(p)) { - try { - String json = Files.readString(p); - Map map = GSON.fromJson(json, MAP_TYPE); - return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) - : Collections.synchronizedMap(new LinkedHashMap<>()); - } catch (IOException e) { - return Collections.synchronizedMap(new LinkedHashMap<>()); - } - } - return Collections.synchronizedMap(new LinkedHashMap<>()); - }); - } - - private String sanitize(String s) { - return s.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + this.path = Box3StorageSupport.resolveStoragePath(baseDir, name); + this.data = cache.computeIfAbsent(path, p -> Box3StorageSupport.readData(p, "server")); } public String getKey() { return name; } @@ -88,10 +66,7 @@ private String sanitize(String s) { // ---- Persist ---- private void persist() { - try { - Files.createDirectories(path.getParent()); - Files.writeString(path, GSON.toJson(data)); - } catch (IOException ignored) {} + Box3StorageSupport.writeData(path, data, "server"); } // ---- Public API ---- @@ -104,7 +79,7 @@ public void set(String key, Object value) { if (existing != null) { existing.value = value; existing.updateTime = now; - existing.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + existing.version = Box3StorageTypes.newVersion(now); } else { data.put(key, new Box3StorageTypes.ValueEntry(value, now)); } @@ -134,7 +109,7 @@ public void update(String key, Function handler) { long now = System.currentTimeMillis(); entry.value = engine.callFunction(handler, entry.value); entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); persist(); } } @@ -164,7 +139,7 @@ public double increment(String key, double value) { entry.value = delta; } entry.updateTime = now; - entry.version = Long.toHexString(now) + "-" + Integer.toHexString(new Random().nextInt()); + entry.version = Box3StorageTypes.newVersion(now); } else { entry = new Box3StorageTypes.ValueEntry(delta, now); data.put(key, entry); @@ -254,7 +229,7 @@ private double extractSortValue(Object value, String target) { public void destroy() { synchronized (data) { cache.remove(path); - try { Files.deleteIfExists(path); } catch (IOException ignored) {} + Box3StorageSupport.deleteData(path, "server"); } } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java index 25c2f6f..0de8a8e 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSWorld.java @@ -358,13 +358,6 @@ public void say(String message) { server.getPlayerList().broadcastSystemMessage(net.minecraft.network.chat.Component.literal(message), false); } - // ---- Timers ---- - - public int setTimeout(Function handler, int ticks) { return engine.scheduleTimeout(handler, ticks); } - public int setInterval(Function handler, int ticks) { return engine.scheduleInterval(handler, ticks); } - public void clearTimeout(int id) { engine.clearTimer(id); } - public void clearInterval(int id) { engine.clearTimer(id); } - // ---- Command ---- public void runCommand(String cmd) { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java new file mode 100644 index 0000000..9636e42 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3Rhino.java @@ -0,0 +1,18 @@ +package com.box3lab.box3js.script; + +import org.mozilla.javascript.Context; + +/** + * Shared Rhino helpers for server and client script engines. + */ +public final class Box3Rhino { + + private Box3Rhino() {} + + @SuppressWarnings("deprecation") + public static Context enterInterpretedContext() { + Context cx = Context.enter(); + cx.setOptimizationLevel(-1); + return cx; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java index 0632bec..e862da7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptConfig.java @@ -9,11 +9,14 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; import net.minecraft.server.MinecraftServer; +import org.slf4j.Logger; public class Box3ScriptConfig { + private static final Logger LOGGER = LogUtils.getLogger(); private static final Gson GSON = new Gson(); private static final Type MAP_TYPE = new TypeToken>() { }.getType(); @@ -41,7 +44,8 @@ public void load(MinecraftServer server) { Map loaded = GSON.fromJson(json, MAP_TYPE); if (loaded != null) projects = new LinkedHashMap<>(loaded); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to load script config: {}", configFile, e); } } } @@ -53,7 +57,8 @@ private void save() { try { Files.createDirectories(configFile.getParent()); Files.writeString(configFile, GSON.toJson(projects)); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to save script config: {}", configFile, e); } } @@ -89,7 +94,8 @@ public void discover(MinecraftServer server) { projects.putIfAbsent(name, false); } }); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to discover script projects in: {}", scriptDir, e); } save(); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java index 32184ea..73697c8 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java @@ -1,6 +1,7 @@ package com.box3lab.box3js.script; import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.state.BlockState; @@ -15,7 +16,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import com.mojang.logging.LogUtils; @@ -91,16 +91,16 @@ public ScriptableObject getScope() { } /** Exposed for remoteChannel payload handler. */ - public MinecraftServer getServer() { + MinecraftServer getServer() { return server; } /** Exposed for remoteChannel. */ - public long getCurrentTick() { + long getCurrentTick() { return currentTick; } - public boolean isInitialized() { + boolean isInitialized() { return initialized; } @@ -121,7 +121,7 @@ public void handleClientEvent(ServerPlayer sender, String projectName, String ev }); } - /** Execute app.js for enabled projects under config/box3/script/ */ + /** Execute server.js for enabled projects under config/box3/script/ */ public void autoLoad(MinecraftServer server) { init(server); Box3ScriptConfig config = Box3ScriptConfig.get(); @@ -152,15 +152,15 @@ public void autoLoad(MinecraftServer server) { } } }); - } catch (IOException ignored) { + } catch (IOException e) { + LOGGER.warn("Failed to scan script directory for auto-load: {}", scriptDir, e); } } public Object eval(String code) { if (!initialized) throw new IllegalStateException("ScriptEngine not initialized"); - Context cx = Context.enter(); - cx.setOptimizationLevel(-1); // interpreter mode avoids regex classloader issues + Context cx = Box3Rhino.enterInterpretedContext(); try { return cx.evaluateString(scope, code, "script", 1, null); } finally { @@ -323,9 +323,15 @@ public Runnable addMessageCallback(String project, MessageCallback cb) { return () -> bus.removeMessage(project, wrapped); } - public void setPlayerChatHandler(UUID uuid, Function handler) { + public Runnable setPlayerChatHandler(UUID uuid, Function handler) { String project = currentProject; bus.chatHandlersFor(project).put(uuid, handler); + return () -> { + Map handlers = bus.chatHandlersFor(project); + if (handlers.get(uuid) == handler) { + handlers.remove(uuid); + } + }; } private Runnable wrapContext(String project, Runnable cb) { @@ -350,12 +356,12 @@ public void runInContext(String project, Runnable action) { } } - public void setCurrentProject(String name) { + void setCurrentProject(String name) { currentProject = name; worldBinding.setProjectName(name); } - public String getCurrentProject() { + String getCurrentProject() { return currentProject; } @@ -645,8 +651,8 @@ private void tickFluid(String project, List enter, List clearTimer(id)); + } + }); + ScriptableObject.putProperty(scope, "setInterval", new BaseFunction() { + @Override + public Object call(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + Function handler = (Function) args[0]; + int ticks = ((Number) args[1]).intValue(); + int id = scheduleInterval(handler, ticks); + return new GameEventHandlerToken(() -> clearTimer(id)); + } + }); + ScriptableObject.putProperty(scope, "_jConsole", Context.javaToJS(new Box3JSConsole(() -> currentProject), scope)); cx.evaluateString(scope, Box3ScriptUtils.CONSOLE_INIT_JS, "console-init", 1, null); ScriptableObject.putProperty(scope, "require", new BaseFunction() { @@ -1027,49 +1053,10 @@ protected String getCharacterEncoding(java.net.URLConnection c) { } } - public Box3JSVoxels getVoxelsBinding() { + Box3JSVoxels getVoxelsBinding() { return voxelsBinding; } - public class Box3JSConsole { - private void print(String level, Object... args) { - StringBuilder sb = new StringBuilder(); - String proj = currentProject; - if (proj != null) - sb.append('[').append(proj).append("] "); - for (Object a : args) - sb.append(a).append(' '); - System.out.println("[Box3JS]" + level + " " + sb.toString().trim()); - } - - public void log(Object... args) { - print("", args); - } - - public void debug(Object... args) { - print("[DEBUG]", args); - } - - public void warn(Object... args) { - print("[WARN]", args); - } - - public void error(Object... args) { - StringBuilder sb = new StringBuilder(); - String proj = currentProject; - if (proj != null) - sb.append('[').append(proj).append("] "); - for (Object a : args) - sb.append(a).append(' '); - System.err.println("[Box3JS][ERROR] " + sb.toString().trim()); - } - - public void clear() { - System.out.print("\033[H\033[2J"); - System.out.flush(); - } - } - static class TimerEntry { final int id; final Function handler; diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java index 67b1457..01e62ca 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptSandbox.java @@ -25,6 +25,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; +@SuppressWarnings("deprecation") class Box3ScriptSandbox { private static final Logger LOGGER = LogUtils.getLogger(); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java index e023377..3b166cc 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java @@ -32,6 +32,12 @@ public class Box3ScriptTemplate { "types/client/ui.d.ts", "types/client/chat.d.ts", "types/client/gui.d.ts", + "registries/blocks.json", + "registries/items.json", + "registries/sounds.json", + "registries/creativeTabs.json", + "assets/lang/en_us.json", + "assets/lang/zh_cn.json", }; public static void copyTo(Path projectDir, String projectName) throws IOException { @@ -46,7 +52,7 @@ public static void copyTo(Path projectDir, String projectName) throws IOExceptio throw new IOException("Template file not found: " + resourcePath); Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); } - if (relPath.equals("package.json") || relPath.equals("src/server/app.ts")) { + if (relPath.endsWith(".json") || relPath.endsWith(".ts") || relPath.endsWith(".mjs")) { String content = Files.readString(dest); Files.writeString(dest, content.replace("PROJECT_NAME", projectName)); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java index 71b97fd..1d4249f 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3ScriptWatcher.java @@ -93,7 +93,8 @@ private void closeWatchService() { if (watchService != null) { try { watchService.close(); - } catch (IOException ignored) { + } catch (IOException e) { + Box3JS.LOGGER.warn("Failed to close script watch service", e); } watchService = null; } @@ -165,7 +166,8 @@ private void retryRegister(String project) { } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - } catch (IOException ignored) { + } catch (IOException e) { + Box3JS.LOGGER.warn("Failed to re-register watch for project '{}'", project, e); } } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java new file mode 100644 index 0000000..a09d9c5 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageSupport.java @@ -0,0 +1,81 @@ +package com.box3lab.box3js.script; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Shared JSON storage helpers for server and client storage implementations. + */ +public final class Box3StorageSupport { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Gson GSON = new Gson(); + private static final Type MAP_TYPE = new TypeToken>() {}.getType(); + + private Box3StorageSupport() {} + + public static String sanitize(String value) { + return value.replaceAll("[^a-zA-Z0-9_.\\-]", "_"); + } + + public static Path resolveStoragePath(Path baseDir, String name) { + String[] parts = name.split("/"); + Path dir = baseDir; + for (int i = 0; i < parts.length - 1; i++) { + String segment = sanitize(parts[i]); + if (!segment.isEmpty()) { + dir = dir.resolve(segment); + } + } + String file = sanitize(parts[parts.length - 1]); + if (file.isEmpty()) { + file = "default"; + } + return dir.resolve(file + ".json"); + } + + public static Map emptyDataMap() { + return Collections.synchronizedMap(new LinkedHashMap<>()); + } + + public static Map readData(Path path, String label) { + if (!Files.exists(path)) { + return emptyDataMap(); + } + try { + String json = Files.readString(path); + Map map = GSON.fromJson(json, MAP_TYPE); + return map != null ? Collections.synchronizedMap(new LinkedHashMap<>(map)) : emptyDataMap(); + } catch (IOException e) { + LOGGER.warn("Failed to read {} storage file: {}", label, path, e); + return emptyDataMap(); + } + } + + public static void writeData(Path path, Map data, String label) { + try { + Files.createDirectories(path.getParent()); + Files.writeString(path, GSON.toJson(data)); + } catch (IOException e) { + LOGGER.warn("Failed to persist {} storage file: {}", label, path, e); + } + } + + public static void deleteData(Path path, String label) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + LOGGER.warn("Failed to delete {} storage file: {}", label, path, e); + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java index 056e8b2..400e078 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3StorageTypes.java @@ -1,6 +1,7 @@ package com.box3lab.box3js.script; import java.util.*; +import java.util.concurrent.ThreadLocalRandom; /** * Shared storage data types used by both server-side and client-side storage. @@ -21,10 +22,14 @@ public ValueEntry(Object value, long createTime) { this.value = value; this.createTime = createTime; this.updateTime = createTime; - this.version = Long.toHexString(createTime) + "-" + Integer.toHexString(new Random().nextInt()); + this.version = newVersion(createTime); } } + public static String newVersion(long timestamp) { + return Long.toHexString(timestamp) + "-" + Integer.toHexString(ThreadLocalRandom.current().nextInt()); + } + // ── ReturnValue ── public static class ReturnValue { diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs index 5e42a34..3c211fe 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/build.mjs @@ -362,6 +362,137 @@ function rhinoArrayMethodsPlugin({ types: t }) { }; } +/** + * Regex literal → __regexSplit / __regexMatch / __regexReplace / __regexTest + * + * Rhino inside Minecraft cannot load NativeRegExp (classloader isolation). + * All regex operations are delegated to pure-JS helpers injected from Java. + * + * str.split(/pattern/flags) → __regexSplit(str, "pattern", "flags") + * str.match(/pattern/flags) → __regexMatch(str, "pattern", "flags") + * str.replace(/pattern/flags,r) → __regexReplace(str, "pattern", "flags", r) + * /pattern/flags.test(str) → __regexTest("pattern", "flags", str) + */ +function rhinoRegexPlugin({ types: t }) { + function patternString(node) { + return t.stringLiteral(node.pattern); + } + function flagsString(node) { + return t.stringLiteral(node.flags || ""); + } + + return { + visitor: { + CallExpression(path) { + const { callee, arguments: args } = path.node; + + // str.split(regex) → __regexSplit(str, pattern, flags) + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property, { name: "split" }) && + args.length >= 1 && + t.isRegExpLiteral(args[0]) + ) { + const regex = args[0]; + path.replaceWith( + t.callExpression(t.identifier("__regexSplit"), [ + callee.object, + patternString(regex), + flagsString(regex), + ]), + ); + return; + } + + // str.match(regex) → __regexMatch(str, pattern, flags) + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property, { name: "match" }) && + args.length >= 1 && + t.isRegExpLiteral(args[0]) + ) { + const regex = args[0]; + path.replaceWith( + t.callExpression(t.identifier("__regexMatch"), [ + callee.object, + patternString(regex), + flagsString(regex), + ]), + ); + return; + } + + // str.replace(regex, replacement) → __regexReplace(str, pattern, flags, replacement) + if ( + t.isMemberExpression(callee) && + t.isIdentifier(callee.property, { name: "replace" }) && + args.length >= 1 && + t.isRegExpLiteral(args[0]) + ) { + const regex = args[0]; + path.replaceWith( + t.callExpression(t.identifier("__regexReplace"), [ + callee.object, + patternString(regex), + flagsString(regex), + args[1] || t.nullLiteral(), + ]), + ); + return; + } + + // regex.test(str) → __regexTest(pattern, flags, str) + if ( + t.isMemberExpression(callee) && + t.isRegExpLiteral(callee.object) && + t.isIdentifier(callee.property, { name: "test" }) && + args.length >= 1 + ) { + const regex = callee.object; + path.replaceWith( + t.callExpression(t.identifier("__regexTest"), [ + patternString(regex), + flagsString(regex), + args[0], + ]), + ); + return; + } + + // regex.exec(str) → __regexExec(pattern, flags, str) + if ( + t.isMemberExpression(callee) && + t.isRegExpLiteral(callee.object) && + t.isIdentifier(callee.property, { name: "exec" }) && + args.length >= 1 + ) { + const regex = callee.object; + path.replaceWith( + t.callExpression(t.identifier("__regexExec"), [ + patternString(regex), + flagsString(regex), + args[0], + ]), + ); + } + }, + + // Standalone regex literals not in a call expression context + RegExpLiteral(path) { + if (t.isCallExpression(path.parent) && path.parent.arguments.includes(path.node)) { + return; + } + if (t.isMemberExpression(path.parent) && path.parent.object === path.node) { + return; + } + throw path.buildCodeFrameError( + "Regex literals must be used with .split(), .match(), .replace(), .test(), or .exec()", + ); + }, + }, + }; +} + // ═══════════════════════════════════════════════════════════════ // esbuild plugin // ═══════════════════════════════════════════════════════════════ @@ -390,6 +521,7 @@ const babelRhinoPlugin = { "@babel/preset-typescript", ], plugins: [ + rhinoRegexPlugin, rhinoArrayMethodsPlugin, rhinoForOfPlugin, rhinoTemplatePlugin, diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs index 064f9f5..9019cf3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/eslint.config.mjs @@ -95,9 +95,7 @@ export default [ languageOptions: { parser: tseslint.parser, parserOptions: { - project: true, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + project: ['./tsconfig.server.json', './tsconfig.client.json'], tsconfigRootDir: import.meta.dirname, }, }, diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts index c884b42..39e46e3 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/src/server/app.ts @@ -6,12 +6,10 @@ world.onPlayerJoin((entity: GamePlayerEntity) => { p.directMessage("§6Welcome to §ePROJECT_NAME§6!"); p.directMessage("§7Type §e!hello §7to say hi"); - if (entity.hasBox3JSClient()) { - remoteChannel.sendClientEvent(entity, { - type: "welcome", - message: `Welcome, ${p.name}`, - }); - } + remoteChannel.sendClientEvent(entity, { + type: "welcome", + message: `Welcome, ${p.name}`, + }); }); world.onChat((entity: GamePlayerEntity, message: string, _tick: number) => { diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts index fa048a5..200de8d 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/client.d.ts @@ -35,8 +35,12 @@ interface RemoteChannel { /** @zh 通过 `client` 访问:生命周期回调 @en Accessed via `client`: lifecycle callbacks */ interface GameClient { - /** @zh 注册客户端每 tick 回调(每秒 20 次)。 @en Registers a callback invoked every client tick (20/sec). */ - onTick(callback: () => void): void; + /** + * @zh 注册客户端每 tick 回调(每秒 20 次)。 + * @en Registers a callback invoked every client tick (20/sec). + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to unsubscribe + */ + onTick(callback: () => void): GameEventHandlerToken; /** * @zh 获取当前帧率 (FPS)。 @@ -68,15 +72,14 @@ interface GameClient { * @returns @zh `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` 或 null @en `{ type: "block"|"entity", position, entity?, blockPos?, direction? }` or null */ getLookingAt(): { - type: string; + type: "entity"; position: { x: number; y: number; z: number }; - entity?: { - name: string; - uuid: string; - type: string; - }; - blockPos?: { x: number; y: number; z: number }; - direction?: string; + entity: { name: string; uuid: string; type: string }; + } | { + type: "block"; + position: { x: number; y: number; z: number }; + blockPos: { x: number; y: number; z: number }; + direction: string; } | null; /** @@ -88,9 +91,47 @@ interface GameClient { ip: string; name: string; isLocal: boolean; - playerCount: number; - maxPlayers: number; + playerCount?: number; + maxPlayers?: number; }; + + // ── Fog control ── + + /** + * @zh 获取当前自定义雾颜色。未设置时返回 null。 + * @en Gets the current custom fog colour. Returns null if not set. + * @returns @zh GameRGBColor 或 null @en GameRGBColor or null + */ + getFogColor(): GameRGBColor | null; + + /** + * @zh 设置雾颜色(RGB 0-255)。 + * @en Sets the fog colour (RGB 0-255). + * @param r - @zh 红色 (0-255) @en Red (0-255) + * @param g - @zh 绿色 (0-255) @en Green (0-255) + * @param b - @zh 蓝色 (0-255) @en Blue (0-255) + */ + setFogColor(r: number, g: number, b: number): void; + + /** + * @zh 设置雾起始距离(方块)。低于此距离完全透明。 + * @en Sets the distance (in blocks) where fog begins. Fully transparent below this distance. + * @param distance - @zh 雾起始距离(方块) @en Fog start distance (blocks) + */ + setFogStartDistance(distance: number): void; + + /** + * @zh 设置雾结束距离(方块),对应 Box3 的 maxFog。超过此距离完全被雾遮挡。 + * @en Sets the distance (in blocks) where fog is fully opaque, equivalent to Box3's maxFog. + * @param distance - @zh 雾结束距离(方块) @en Fog end distance (blocks) + */ + setFogEndDistance(distance: number): void; + + /** + * @zh 重置雾效果为 Minecraft 默认值。 + * @en Resets fog to Minecraft's default behaviour. + */ + resetFog(): void; } // ── §7 @zh 全局声明(客户端) @en Global Declarations (client) ── diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts index 213117b..105733d 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/client/input.d.ts @@ -8,8 +8,9 @@ */ type KeyName = | "space" | "enter" | "escape" | "tab" | "backspace" | "delete" - | "left_shift" | "right_shift" | "left_ctrl" | "right_ctrl" - | "left_alt" | "right_alt" + | "shift" | "left_shift" | "right_shift" + | "ctrl" | "left_ctrl" | "right_ctrl" + | "alt" | "left_alt" | "right_alt" | "up" | "down" | "left" | "right" | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts index 56590a7..0ae1bf6 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/entity.d.ts @@ -135,6 +135,8 @@ interface GameEntity { * @en Custom name tag text (empty string = none). */ nameTag: string; + + /** @zh 设置名称标签文本。 @en Sets the custom name tag text. */ setNameTag(name: string): void; // ── @zh 物理 @en Physics ── @@ -308,6 +310,7 @@ interface GameEntity { */ destroy(): void; + /** @zh 注册实体销毁回调。 @en Registers a callback invoked when this entity is destroyed. */ setOnDestroy(handler: (entity: GameEntity) => void): void; // ── @zh 玩家代理 @en Player proxy ── @@ -323,4 +326,4 @@ interface GameEntity { * @zh 玩家实体 — `GameEntity` 的子类型,保证 `player` 属性非 null。 * @en A player entity — subtype of `GameEntity` with a guaranteed non‑null `player`. */ -type GamePlayerEntity = GameEntity & { player: GamePlayer; hasBox3JSClient(): boolean }; +type GamePlayerEntity = GameEntity & { player: GamePlayer }; diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts index 0de3231..a3f8176 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/player.d.ts @@ -430,13 +430,4 @@ interface GamePlayer { */ revokeAdvancement(advancementId: string): void; - // ── @zh 客户端 Mod 检测 @en Client Mod Detection ── - - /** - * @zh 检查该玩家的客户端是否安装了 Box3JS mod。 - * @en Returns true if this player's client has the Box3JS mod installed. - * @remarks 用于在调用 `remoteChannel.sendClientEvent()` 前检测,避免向未安装的客户端发送。 - * Use before calling `remoteChannel.sendClientEvent()` to avoid sending to unsupported clients. - */ - hasBox3JSClientMod(): boolean; } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts index 50ef9a7..5ce92c0 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/server.d.ts @@ -266,30 +266,39 @@ type GameMapColor = * @en Query interface for registered content — only available in JARs built via `/box3script compile`. */ interface GameRegistries { + /** @zh 获取已注册方块信息。 @en Gets registered block information. */ getBlock(id: string): { block: any; itemId: string; } | null; + /** @zh 检查方块是否已注册。 @en Checks whether a block is registered. */ hasBlock(id: string): boolean; + /** @zh 列出所有已注册方块 ID。 @en Lists all registered block IDs. */ listBlocks(): string[]; + /** @zh 获取已注册物品信息。 @en Gets registered item information. */ getItem(id: string): { item: any; itemId: string; } | null; + /** @zh 检查物品是否已注册。 @en Checks whether an item is registered. */ hasItem(id: string): boolean; + /** @zh 列出所有已注册物品 ID。 @en Lists all registered item IDs. */ listItems(): string[]; + /** @zh 获取已注册音效信息。 @en Gets registered sound information. */ getSound(id: string): { soundId: string; } | null; + /** @zh 检查音效是否已注册。 @en Checks whether a sound is registered. */ hasSound(id: string): boolean; + /** @zh 列出所有已注册音效 ID。 @en Lists all registered sound IDs. */ listSounds(): string[]; } diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts index 674c11b..fa8a6d7 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/server/world.d.ts @@ -657,32 +657,6 @@ interface GameWorld { */ setBorderWarning(blocks: number): void; - // ── @zh 定时器 @en Timers ── - - /** - * @zh 设置一次性延时回调。 - * @en Schedules a one‑shot delayed callback. - * @param handler - 回调函数 - * @param ticks - 延迟 tick 数 - * @returns @zh 定时器 ID(可用于 clearTimeout) @en Timer ID (can be used with clearTimeout) - */ - setTimeout(handler: () => void, ticks: number): number; - - /** - * @zh 设置循环定时回调。 - * @en Schedules a recurring interval callback. - * @param handler - 回调函数 - * @param ticks - 间隔 tick 数 - * @returns @zh 定时器 ID(可用于 clearInterval) @en Timer ID (can be used with clearInterval) - */ - setInterval(handler: () => void, ticks: number): number; - - /** @zh 取消 setTimeout @en Clears a timeout by ID. */ - clearTimeout(id: number): void; - - /** @zh 取消 setInterval @en Clears an interval by ID. */ - clearInterval(id: number): void; - // ── @zh 项目间消息 @en Cross‑project Messaging ── /** diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts index a8d79e3..5ccda11 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/shared.d.ts @@ -161,6 +161,9 @@ declare class GameVector3 { static fromPolar(mag: number, phi: number, theta: number): GameVector3; /** @zh 返回 "(x, y, z)" 格式的字符串表示。 @en Returns a string in "(x, y, z)" format. */ + /** @zh 返回 "(lo, hi)" 格式的字符串表示。 @en Returns a string representation in "(lo, hi)" format. */ + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ toString(): string; } @@ -230,6 +233,7 @@ declare class GameBounds3 { /** @zh 从 GameVector3 数组创建最小包围盒。 @en Creates bounds from an array of GameVector3. */ static fromPoints(points: GameVector3[]): GameBounds3 | null; + /** @zh 返回颜色字符串表示。 @en Returns a string representation of the color. */ toString(): string; } @@ -301,6 +305,7 @@ declare class GameRGBColor { /** @zh 生成一个随机 RGB 颜色(每个通道 0–1)。 @en Generates a random RGB color (each channel 0–1). */ static random(): GameRGBColor; + /** @zh 返回颜色字符串表示。 @en Returns a string representation of the color. */ toString(): string; } @@ -373,6 +378,7 @@ declare class GameRGBAColor { */ blendEq(rgb: GameRGBColor): GameRGBColor; + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ toString(): string; } @@ -491,6 +497,7 @@ declare class GameQuaternion { /** @zh 近似相等检查(容差 1e‑6)。 @en Approximate equality check within 1e‑6 tolerance. */ equals(v: GameQuaternion): boolean; + /** @zh 返回四元数字符串表示。 @en Returns a string representation of the quaternion. */ toString(): string; } @@ -1008,3 +1015,36 @@ declare const db: GameDatabase; /** @zh HTTP 请求 API @en HTTP request API */ declare const http: GameHttpAPI; + +// ── §8 @zh 定时器(全局函数) @en Timers (global functions) ── + +/** + * @zh 设置一次性延时回调。Rhino 引擎不提供浏览器内置的 setTimeout,由 Box3JS 提供。 + * @en Schedules a one‑shot delayed callback. Rhino does not provide the browser built‑in setTimeout; Box3JS supplies it. + * @param handler - @zh 回调函数 @en callback function + * @param ticks - @zh 延迟 tick 数(20 ticks = 1 秒) @en delay in ticks (20 ticks = 1 second) + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to cancel + * + * @example + * const token = setTimeout(() => { + * world.say("3 seconds passed"); + * }, 60); + * // token.cancel(); // cancel before it fires + */ +declare function setTimeout(handler: () => void, ticks: number): GameEventHandlerToken; + +/** + * @zh 设置循环定时回调。Rhino 引擎不提供浏览器内置的 setInterval,由 Box3JS 提供。 + * @en Schedules a recurring interval callback. Rhino does not provide the browser built‑in setInterval; Box3JS supplies it. + * @param handler - @zh 回调函数 @en callback function + * @param ticks - @zh 间隔 tick 数(20 ticks = 1 秒) @en interval in ticks (20 ticks = 1 second) + * @returns @zh GameEventHandlerToken — 调用 .cancel() 取消 @en GameEventHandlerToken — call .cancel() to cancel + * + * @example + * const token = setInterval(() => { + * world.say("Every 5 seconds"); + * }, 100); + * // token.cancel(); // stop the interval + */ +declare function setInterval(handler: () => void, ticks: number): GameEventHandlerToken; + diff --git a/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json b/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json new file mode 100644 index 0000000..cbbd474 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/tools/box3js-api-manifest.json @@ -0,0 +1,277 @@ +{ + "globals": [ + { "side": "server", "name": "world", "type": "GameWorld", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "voxels", "type": "GameVoxels", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "storage", "type": "GameStorage", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "remoteChannel", "type": "RemoteChannel", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "db", "type": "GameDatabase", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "http", "type": "GameHttpAPI", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "registries", "type": "GameRegistries | undefined", "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", "runtime": "src/main/java/com/box3lab/box3js/standalone/Box3StandaloneBootstrap.java" }, + { "side": "server", "name": "setTimeout", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "server", "name": "setInterval", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java" }, + { "side": "client", "name": "setTimeout", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "setInterval", "type": "GlobalFunction", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "audio", "type": "GameAudio", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "client", "type": "GameClient", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "input", "type": "GameInput", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "ui", "type": "GameUI", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "chat", "type": "GameChat", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "gui", "type": "GameGUI", "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "storage", "type": "GameStorage", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "remoteChannel", "type": "RemoteChannel", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "db", "type": "GameDatabase", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" }, + { "side": "client", "name": "http", "type": "GameHttpAPI", "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", "runtime": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java" } + ], + "apis": [ + { + "name": "world", + "prefix": "world", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSWorld.java", + "dts": "src/main/resources/assets/box3js/template/types/server/world.d.ts", + "iface": "GameWorld", + "accessorProperties": ["serverId", "rainDensity", "thunderDensity", "time", "timeScale", "difficulty", "spawnPoint", "ambientSound", "playerJoinSound", "playerLeaveSound", "placeVoxelSound", "breakVoxelSound", "borderSize"], + "keepMethodNames": ["setTime"], + "ignoreJava": ["setProjectName", "removeProject", "resetAll", "getProjectName", "getCurrentTick"], + "docs": ["docs/api/world.md", "docs/api/world_en.md"] + }, + { + "name": "GameVector3", + "java": "src/main/java/com/box3lab/box3js/script/GameVector3.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameVector3" + }, + { + "name": "GameBounds3", + "java": "src/main/java/com/box3lab/box3js/script/GameBounds3.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameBounds3" + }, + { + "name": "GameRGBColor", + "java": "src/main/java/com/box3lab/box3js/script/GameRGBColor.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameRGBColor" + }, + { + "name": "GameRGBAColor", + "java": "src/main/java/com/box3lab/box3js/script/GameRGBAColor.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameRGBAColor" + }, + { + "name": "GameQuaternion", + "java": "src/main/java/com/box3lab/box3js/script/GameQuaternion.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameQuaternion" + }, + { + "name": "GameEventHandlerToken", + "java": "src/main/java/com/box3lab/box3js/script/GameEventHandlerToken.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameEventHandlerToken" + }, + { + "name": "console", + "expectedMembers": ["log", "debug", "warn", "error", "clear", "assert"], + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameConsole" + }, + { + "name": "entity", + "prefix": "entity", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSEntity.java", + "dts": "src/main/resources/assets/box3js/template/types/server/entity.d.ts", + "iface": "GameEntity", + "ignoreJava": ["getEntity"], + "accessorProperties": ["id", "entityType", "player", "position", "velocity", "bounds", "meshInvisible", "glowing", "nameTag", "onGround", "eyePosition", "destroyed", "hp", "maxHp", "collides", "fixed", "gravity", "friction", "mass", "restitution", "invulnerable"], + "keepMethodNames": ["isPlayer", "setNameTag"], + "docs": ["docs/api/entity.md", "docs/api/entity_en.md"] + }, + { + "name": "player", + "prefix": "player", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java", + "dts": "src/main/resources/assets/box3js/template/types/server/player.d.ts", + "iface": "GamePlayer", + "ignoreJava": ["getPlayer"], + "accessorProperties": ["position", "velocity", "bounds", "onGround", "name", "userId", "opLevel", "invisible", "scale", "walkSpeed", "runSpeed", "jumpPower", "moveState", "walkState", "enableJump", "crouchSpeed", "swimSpeed", "canFly", "flying", "collision", "spectator", "flySpeed", "gameMode", "dimension", "disableFly", "cameraMode", "cameraEntity", "cameraPitch", "cameraYaw", "facingDirection", "cameraTarget", "dead", "spawnPoint", "hp", "maxHp", "xp", "food", "saturation"], + "docs": ["docs/api/player.md", "docs/api/player_en.md"] + }, + { + "name": "voxels", + "prefix": "voxels", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSVoxels.java", + "dts": "src/main/resources/assets/box3js/template/types/server/voxels.d.ts", + "iface": "GameVoxels", + "ignoreJava": ["getId"], + "docs": ["docs/api/voxels.md", "docs/api/voxels_en.md"] + }, + { + "name": "server storage", + "prefix": "storage", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSStorage.java", + "dts": ["src/main/resources/assets/box3js/template/types/shared.d.ts", "src/main/resources/assets/box3js/template/types/server/server.d.ts"], + "iface": "GameStorage", + "accessorProperties": ["key"], + "docs": ["docs/api/storage.md", "docs/api/storage_en.md"] + }, + { + "name": "server data storage", + "prefix": "store", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSStorage.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDataStorage", + "nestedClass": "GameDataStorage", + "accessorProperties": ["key"], + "docs": ["docs/api/storage.md", "docs/api/storage_en.md"] + }, + { + "name": "client storage", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameStorage", + "accessorProperties": ["key"] + }, + { + "name": "client data storage", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSClientStorage.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDataStorage", + "nestedClass": "GameDataStorage", + "accessorProperties": ["key"] + }, + { + "name": "server remoteChannel", + "prefix": "remoteChannel", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java", + "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", + "iface": "RemoteChannel", + "docs": ["docs/api/server.md", "docs/api/server_en.md"] + }, + { + "name": "http", + "prefix": "http", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSHttp.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpAPI", + "docs": ["docs/api/http.md", "docs/api/http_en.md"] + }, + { + "name": "client http", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSClientHttp.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpAPI" + }, + { + "name": "database", + "prefix": "db", + "java": "src/main/java/com/box3lab/box3js/script/Box3DatabaseBase.java", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDatabase", + "docs": ["docs/api/database.md", "docs/api/database_en.md"] + }, + { + "name": "query result", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSQueryResult.java", + "className": "Box3JSQueryResult", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameQueryResult", + "ignoreJava": ["toString"], + "accessorProperties": ["rows", "firstRow", "columnNames", "columnCount", "rowCount", "affectedRows"] + }, + { + "name": "http response", + "java": "src/main/java/com/box3lab/box3js/script/Box3JSResponse.java", + "className": "Box3JSResponse", + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpFetchResponse", + "ignoreJava": ["timeout", "error"], + "accessorProperties": ["status", "statusText", "ok", "errorMessage", "headers", "data", "truncated"] + }, + { + "name": "registries", + "prefix": "registries", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/standalone/Box3StandaloneBootstrap.java", "variable": "registriesObj" }, + "dts": "src/main/resources/assets/box3js/template/types/server/server.d.ts", + "iface": "GameRegistries", + "docs": ["docs/api/registries.md", "docs/api/registries_en.md"] + }, + { + "name": "client", + "prefix": "client", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "clientObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", + "iface": "GameClient", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "audio", + "prefix": "audio", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "audioObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/audio.d.ts", + "iface": "GameAudio", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "input", + "prefix": "input", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "inputObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/input.d.ts", + "iface": "GameInput", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "ui", + "prefix": "ui", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "uiObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/ui.d.ts", + "iface": "GameUI", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "chat", + "prefix": "chat", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "chatObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/chat.d.ts", + "iface": "GameChat", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "client remoteChannel", + "prefix": "remoteChannel", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "rcObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/client.d.ts", + "iface": "RemoteChannel", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "client database", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "dbObj" }, + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameDatabase" + }, + { + "name": "client http global", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "httpObj" }, + "dts": "src/main/resources/assets/box3js/template/types/shared.d.ts", + "iface": "GameHttpAPI" + }, + { + "name": "gui", + "prefix": "gui", + "javaObject": { "path": "src/main/java/com/box3lab/box3js/client/Box3JSClientEngine.java", "variable": "guiObj" }, + "dts": "src/main/resources/assets/box3js/template/types/client/gui.d.ts", + "iface": "GameGUI", + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + }, + { + "name": "gui controller", + "prefix": "ctrl", + "java": "src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java", + "dts": "src/main/resources/assets/box3js/template/types/client/gui.d.ts", + "iface": "GuiController", + "ignoreJava": ["fireSlotClick", "fireClose"], + "docs": ["docs/api/client.md", "docs/api/client_en.md"] + } + ] +} diff --git a/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs b/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs new file mode 100644 index 0000000..eb69dbe --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/tools/verify-box3js-project.mjs @@ -0,0 +1,552 @@ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; + +const root = new URL("..", import.meta.url).pathname.replace(/\/$/, ""); +const failures = []; + +function read(path) { + return readFileSync(join(root, path), "utf8"); +} + +function readJson(path) { + return JSON.parse(read(path)); +} + +function fail(message) { + failures.push(message); +} + +const apiManifest = readJson("tools/box3js-api-manifest.json"); + +function listFiles(dir) { + const base = join(root, dir); + const out = []; + function walk(abs) { + for (const name of readdirSync(abs)) { + const file = join(abs, name); + if (statSync(file).isDirectory()) { + walk(file); + } else { + out.push(relative(base, file).replaceAll("\\", "/")); + } + } + } + walk(base); + return out.sort(); +} + +function extractTemplateFiles() { + const source = read("src/main/java/com/box3lab/box3js/script/Box3ScriptTemplate.java"); + const match = source.match(/private static final String\[\] FILES = \{([\s\S]*?)\};/); + if (!match) { + fail("Box3ScriptTemplate FILES list was not found"); + return []; + } + return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]).sort(); +} + +function normalizeTemplatePath(path) { + return path === "gitignore.template" ? "gitignore.template" : path; +} + +function verifyTemplateFiles() { + const declared = extractTemplateFiles().map(normalizeTemplatePath); + const actual = listFiles("src/main/resources/assets/box3js/template") + .filter((path) => !path.startsWith("dist/")) + .filter((path) => !path.endsWith(".map")); + + for (const path of declared) { + if (!actual.includes(path)) { + fail(`Template file declared in Box3ScriptTemplate but missing from resources: ${path}`); + } + } + + for (const path of actual) { + if (!declared.includes(path)) { + fail(`Template resource is not copied by Box3ScriptTemplate: ${path}`); + } + } +} + +function verifyRuntimeTypeSplit() { + const serverIndex = read("src/main/resources/assets/box3js/template/types/server/index.d.ts"); + const clientIndex = read("src/main/resources/assets/box3js/template/types/client/index.d.ts"); + const serverConfig = read("src/main/resources/assets/box3js/template/tsconfig.server.json"); + const clientConfig = read("src/main/resources/assets/box3js/template/tsconfig.client.json"); + + if (serverIndex.includes("../client") || serverIndex.includes("./client")) { + fail("Server DTS index references client types"); + } + if (clientIndex.includes("../server") || clientIndex.includes("./server")) { + fail("Client DTS index references server types"); + } + if (serverConfig.includes("src/client") || serverConfig.includes("types/client")) { + fail("tsconfig.server.json includes client sources or types"); + } + if (clientConfig.includes("src/server") || clientConfig.includes("types/server")) { + fail("tsconfig.client.json includes server sources or types"); + } +} + +function verifyEventTokens() { + const javaFiles = [ + "src/main/java/com/box3lab/box3js/script/Box3JSWorld.java", + "src/main/java/com/box3lab/box3js/script/Box3JSPlayer.java", + "src/main/java/com/box3lab/box3js/script/Box3JSRemoteChannel.java", + "src/main/java/com/box3lab/box3js/client/Box3JSGuiProxy.java", + ]; + + for (const file of javaFiles) { + const source = read(file); + for (const match of source.matchAll(/public\s+([A-Za-z0-9_<>, ?]+)\s+(on[A-Z]\w*)\s*\(/g)) { + const returnType = match[1].trim(); + const method = match[2]; + if (returnType !== "GameEventHandlerToken") { + fail(`${file}: ${method} returns ${returnType}, expected GameEventHandlerToken`); + } + } + } + + for (const file of listFiles("src/main/resources/assets/box3js/template/types")) { + if (!file.endsWith(".d.ts")) continue; + const path = `src/main/resources/assets/box3js/template/types/${file}`; + const source = read(path); + for (const match of source.matchAll(/^\s*(on[A-Z]\w*)\s*\([\s\S]*?\):\s*([^;\n]+);/gm)) { + const method = match[1]; + const returnType = match[2].trim(); + if (returnType !== "GameEventHandlerToken") { + fail(`${path}: ${method} returns ${returnType}, expected GameEventHandlerToken`); + } + } + } +} + +function findMatchingBrace(source, openIndex) { + let depth = 0; + for (let i = openIndex; i < source.length; i++) { + const ch = source[i]; + if (ch === "{") { + depth++; + } else if (ch === "}") { + depth--; + if (depth === 0) { + return i; + } + } + } + return -1; +} + +function stripBlockAndLineComments(source) { + return source + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/\/\/.*$/gm, ""); +} + +function stripCommentsPreserveLength(source) { + return source.replace(/\/\*[\s\S]*?\*\//g, (comment) => " ".repeat(comment.length)) + .replace(/\/\/.*$/gm, (comment) => " ".repeat(comment.length)); +} + +function extractDtsInterfaceMembers(paths, interfaceName) { + const files = Array.isArray(paths) ? paths : [paths]; + const members = new Set(); + for (const path of files) { + const source = stripBlockAndLineComments(read(path)); + const interfaceRe = new RegExp(`(?:interface|declare\\s+class|class)\\s+${interfaceName}\\b[^\\{]*\\{`, "g"); + let match; + while ((match = interfaceRe.exec(source)) !== null) { + const open = source.indexOf("{", match.index); + const close = findMatchingBrace(source, open); + if (close === -1) { + fail(`${path}: interface ${interfaceName} has no closing brace`); + continue; + } + const body = source.slice(open + 1, close); + let parenDepth = 0; + let braceDepth = 0; + let bracketDepth = 0; + for (const line of body.split(/\r?\n/)) { + if (parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) { + const member = line.trim().match(/^(?:static\s+)?(?:readonly\s+)?([A-Za-z_$]\w*)\??\s*(?:<[^;{(]*>)?\s*[:(]/); + if (member) { + if (member[1] !== "constructor") { + members.add(member[1]); + } + } + } + for (const ch of line) { + if (ch === "(") parenDepth++; + if (ch === ")") parenDepth = Math.max(0, parenDepth - 1); + if (ch === "{") braceDepth++; + if (ch === "}") braceDepth = Math.max(0, braceDepth - 1); + if (ch === "[") bracketDepth++; + if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1); + } + } + } + } + return members; +} + +function updateDepths(line, state) { + let inString = null; + let escaped = false; + for (const ch of line) { + if (inString) { + if (escaped) { + escaped = false; + } else if (ch === "\\") { + escaped = true; + } else if (ch === inString) { + inString = null; + } + continue; + } + if (ch === "\"" || ch === "'" || ch === "`") { + inString = ch; + } else if (ch === "(") { + state.parenDepth++; + } else if (ch === ")") { + state.parenDepth = Math.max(0, state.parenDepth - 1); + } else if (ch === "{") { + state.braceDepth++; + } else if (ch === "}") { + state.braceDepth = Math.max(0, state.braceDepth - 1); + } else if (ch === "[") { + state.bracketDepth++; + } else if (ch === "]") { + state.bracketDepth = Math.max(0, state.bracketDepth - 1); + } + } +} + +function extractDtsInterfaceMemberDetails(paths, interfaceName) { + const files = Array.isArray(paths) ? paths : [paths]; + const members = new Map(); + for (const path of files) { + const source = read(path); + const searchable = stripCommentsPreserveLength(source); + const interfaceRe = new RegExp(`(?:interface|declare\\s+class|class)\\s+${interfaceName}\\b[^\\{]*\\{`, "g"); + let match; + while ((match = interfaceRe.exec(searchable)) !== null) { + const open = searchable.indexOf("{", match.index); + const close = findMatchingBrace(searchable, open); + if (close === -1) { + fail(`${path}: interface ${interfaceName} has no closing brace`); + continue; + } + const body = source.slice(open + 1, close); + const state = { parenDepth: 0, braceDepth: 0, bracketDepth: 0 }; + let pendingComment = ""; + let commentBuffer = ""; + let inDocComment = false; + + for (const line of body.split(/\r?\n/)) { + const trimmed = line.trim(); + if (trimmed.startsWith("/**")) { + inDocComment = true; + commentBuffer = `${line}\n`; + if (trimmed.includes("*/")) { + inDocComment = false; + pendingComment = commentBuffer; + commentBuffer = ""; + } + continue; + } + if (inDocComment) { + commentBuffer += `${line}\n`; + if (trimmed.includes("*/")) { + inDocComment = false; + pendingComment = commentBuffer; + commentBuffer = ""; + } + continue; + } + + if (state.parenDepth === 0 && state.braceDepth === 0 && state.bracketDepth === 0) { + const lineWithoutComment = line.replace(/\/\/.*$/, ""); + const member = lineWithoutComment.trim().match(/^(?:static\s+)?(?:readonly\s+)?([A-Za-z_$]\w*)\??\s*(?:<[^;{(]*>)?\s*[:(]/); + if (member) { + const name = member[1]; + if (name === "constructor") { + pendingComment = ""; + continue; + } + const documented = pendingComment.includes("@zh") && pendingComment.includes("@en"); + const current = members.get(name) ?? { documented: false, locations: [] }; + current.documented ||= documented; + current.locations.push(path); + members.set(name, current); + pendingComment = ""; + } else if (trimmed !== "" && !trimmed.startsWith("//")) { + pendingComment = ""; + } + } + + updateDepths(stripBlockAndLineComments(line), state); + } + } + } + return members; +} + +function extractClassBody(source, className) { + const classRe = new RegExp(`\\bclass\\s+${className}\\b[^\\{]*\\{`); + const match = source.match(classRe); + if (!match) { + return null; + } + const open = source.indexOf("{", match.index); + const close = findMatchingBrace(source, open); + return close === -1 ? null : source.slice(open + 1, close); +} + +function javaPropertyName(method) { + if (/^(get|set)[A-Z]/.test(method)) { + return method.slice(3, 4).toLowerCase() + method.slice(4); + } + if (/^is[A-Z]/.test(method)) { + return method.slice(2, 3).toLowerCase() + method.slice(3); + } + return method; +} + +function extractJavaApiMembers(path, options = {}) { + const source = read(path); + const ignore = new Set(options.ignore ?? []); + const accessorProperties = new Set(options.accessorProperties ?? []); + const keepMethodNames = new Set(options.keepMethodNames ?? []); + const members = new Set(); + const defaultClassName = path.split("/").pop().replace(/\.java$/, ""); + const javaBody = options.nestedClass + ? extractClassBody(source, options.nestedClass) + : extractClassBody(source, options.className ?? defaultClassName); + + if (!javaBody) { + fail(`${path}: Java API body was not found`); + return members; + } + + const fieldRe = /^\s*public\s+(?:static\s+)?(?:final\s+)?[A-Za-z0-9_.<>\[\]?]+\s+([A-Za-z_$]\w*(?:\s*,\s*[A-Za-z_$]\w*)*)\s*(?:=|;)/gm; + let field; + while ((field = fieldRe.exec(javaBody)) !== null) { + const before = javaBody.slice(0, field.index); + const depth = (before.match(/\{/g) ?? []).length - (before.match(/\}/g) ?? []).length; + if (depth === 0) { + for (const name of field[1].split(",").map((part) => part.trim())) { + if (!ignore.has(name)) { + members.add(name); + } + } + } + } + + const methodRe = /^\s*public\s+(?:static\s+)?(?!class\b|interface\b)([A-Za-z0-9_.<>\[\], ?]+)\s+([A-Za-z_$]\w*)\s*\(/gm; + let match; + while ((match = methodRe.exec(javaBody)) !== null) { + const before = javaBody.slice(0, match.index); + const depth = (before.match(/\{/g) ?? []).length - (before.match(/\}/g) ?? []).length; + if (depth !== 0) { + continue; + } + const method = match[2]; + if (ignore.has(method)) { + continue; + } + const property = javaPropertyName(method); + if (accessorProperties.has(property)) { + members.add(property); + if (keepMethodNames.has(method)) { + members.add(method); + } + } else { + members.add(method); + } + } + return members; +} + +function extractJavaScriptableMembers(path, objectVariable) { + const source = read(path); + const members = new Set(); + const escapedVariable = objectVariable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const propertyRe = new RegExp( + `ScriptableObject\\.putProperty\\(\\s*${escapedVariable}\\s*,\\s*"([^"]+)"`, + "g", + ); + let match; + while ((match = propertyRe.exec(source)) !== null) { + members.add(match[1]); + } + return members; +} + +function hasDtsConst(paths, name, type) { + const files = Array.isArray(paths) ? paths : [paths]; + // GlobalFunction entries use `declare function` instead of `declare const` + if (type === "GlobalFunction") { + const funcRe = new RegExp(`declare\\s+function\\s+${name}\\s*\\(`); + return files.some((path) => funcRe.test(read(path))); + } + const escapedType = type.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s*"); + const declarationRe = new RegExp(`declare\\s+const\\s+${name}\\s*:\\s*${escapedType}\\s*;`); + return files.some((path) => declarationRe.test(read(path))); +} + +function hasRuntimeScopeBinding(path, name) { + const source = read(path); + const bindingRe = new RegExp(`ScriptableObject\\.putProperty\\(\\s*scope\\s*,\\s*"${name}"`); + return bindingRe.test(source); +} + +function verifyGlobalDeclarations() { + for (const global of apiManifest.globals) { + if (!hasDtsConst(global.dts, global.name, global.type)) { + fail(`${global.side}: DTS missing global declaration 'declare const ${global.name}: ${global.type};'`); + } + if (global.runtimeContains && !read(global.runtime).includes(global.runtimeContains)) { + fail(`${global.side}: runtime does not initialize global '${global.name}' via ${global.runtimeContains}`); + } else if (!global.runtimeContains && !hasRuntimeScopeBinding(global.runtime, global.name)) { + fail(`${global.side}: runtime does not bind global '${global.name}'`); + } + } +} + +function extractDtsPublicTypeNames() { + const names = new Set(); + for (const file of listFiles("src/main/resources/assets/box3js/template/types")) { + if (!file.endsWith(".d.ts")) continue; + const source = read(`src/main/resources/assets/box3js/template/types/${file}`); + for (const match of source.matchAll(/^(?:declare\s+class|interface)\s+([A-Za-z_$]\w*)/gm)) { + names.add(match[1]); + } + } + return names; +} + +function extractDtsGlobalNames() { + const names = new Set(); + for (const file of listFiles("src/main/resources/assets/box3js/template/types")) { + if (!file.endsWith(".d.ts")) continue; + const source = read(`src/main/resources/assets/box3js/template/types/${file}`); + for (const match of source.matchAll(/^declare\s+const\s+([A-Za-z_$]\w*)\s*:/gm)) { + names.add(match[1]); + } + } + return names; +} + +function verifyManifestCoverage() { + const coveredTypes = new Set(apiManifest.apis.map((api) => api.iface)); + const ignoredTypes = new Set(apiManifest.ignoredDtsTypes ?? []); + for (const typeName of extractDtsPublicTypeNames()) { + if (!coveredTypes.has(typeName) && !ignoredTypes.has(typeName)) { + fail(`manifest: public DTS type '${typeName}' is not covered by tools/box3js-api-manifest.json`); + } + } + + const coveredGlobals = new Set(apiManifest.globals.map((global) => global.name)); + const ignoredGlobals = new Set(apiManifest.ignoredGlobals ?? []); + for (const globalName of extractDtsGlobalNames()) { + if (!coveredGlobals.has(globalName) && !ignoredGlobals.has(globalName)) { + fail(`manifest: global DTS const '${globalName}' is not covered by tools/box3js-api-manifest.json`); + } + } +} + +function verifyJavaDtsApiParity() { + for (const mapping of apiManifest.apis) { + const javaMembers = mapping.expectedMembers + ? new Set(mapping.expectedMembers) + : mapping.javaObject + ? extractJavaScriptableMembers(mapping.javaObject.path, mapping.javaObject.variable) + : extractJavaApiMembers(mapping.java, { + ignore: mapping.ignoreJava, + nestedClass: mapping.nestedClass, + className: mapping.className, + accessorProperties: mapping.accessorProperties, + keepMethodNames: mapping.keepMethodNames, + }); + const dtsMembers = extractDtsInterfaceMembers(mapping.dts, mapping.iface); + + for (const member of javaMembers) { + if (!dtsMembers.has(member)) { + fail(`${mapping.name}: Java exposes '${member}' but ${mapping.iface} DTS does not declare it`); + } + } + + for (const member of dtsMembers) { + if (!javaMembers.has(member)) { + fail(`${mapping.name}: ${mapping.iface} DTS declares '${member}' but Java does not expose it`); + } + } + } +} + +function docsContainApiMember(docs, prefix, member) { + const token = `${prefix}.${member}`; + const headingRe = new RegExp(`^#{2,4}\\s+\`?${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "m"); + return docs.some((path) => { + const source = read(path); + return source.includes(token) || headingRe.test(source); + }); +} + +function verifyDtsDocumentation() { + for (const mapping of apiManifest.apis) { + const details = extractDtsInterfaceMemberDetails(mapping.dts, mapping.iface); + for (const [member, info] of details.entries()) { + if (!info.documented) { + fail(`${mapping.name}: ${mapping.iface}.${member} DTS member must include @zh and @en documentation`); + } + } + } +} + +function verifyDocsApiSync() { + for (const mapping of apiManifest.apis) { + if (!mapping.docs || !mapping.prefix) { + continue; + } + const members = extractDtsInterfaceMembers(mapping.dts, mapping.iface); + for (const member of members) { + if (!docsContainApiMember(mapping.docs, mapping.prefix, member)) { + fail(`${mapping.name}: docs/api missing ${mapping.prefix}.${member} from ${mapping.docs.join(", ")}`); + } + } + } +} + +function verifyEntrypoints() { + const build = read("src/main/resources/assets/box3js/template/build.mjs"); + const engine = read("src/main/java/com/box3lab/box3js/script/Box3ScriptEngine.java"); + if (!build.includes("src/server/app.ts") || !build.includes("dist/server.js")) { + fail("build.mjs must build src/server/app.ts to dist/server.js"); + } + if (!build.includes("src/client/app.ts") || !build.includes("dist/client.js")) { + fail("build.mjs must build src/client/app.ts to dist/client.js"); + } + if (engine.includes("app.js")) { + fail("Box3ScriptEngine still references legacy app.js"); + } +} + +verifyTemplateFiles(); +verifyRuntimeTypeSplit(); +verifyEventTokens(); +verifyGlobalDeclarations(); +verifyJavaDtsApiParity(); +verifyDtsDocumentation(); +verifyDocsApiSync(); +verifyEntrypoints(); + +if (failures.length > 0) { + console.error("Box3JS project verification failed:"); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); +} + +console.log("Box3JS project verification passed."); diff --git a/CLAUDE.md b/CLAUDE.md index 0c1e16a..da72907 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a server-side TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, and world interactions. +Box3Blocks is a Minecraft mod that imports 372 decorative blocks from the Box3 platform into Minecraft, supporting terrain file import/export and model items. It also includes **Box3JS**, a dual-side (server + client) TypeScript/JavaScript scripting engine (Rhino) for creating custom gameplay, mini-games, GUIs, and world interactions. The repository is a **multi-project monorepo** with 7 independent subprojects targeting different mod loaders and Minecraft versions. There is no root build system — each subproject has its own Gradle wrapper and `build.gradle`. @@ -31,9 +31,12 @@ cd NeoForge-1.21.1 && ./gradlew build # Clean build artifacts cd NeoForge-1.21.1 && ./gradlew clean -# Build Box3JS script (in run/config/box3/script//) -cd run/config/box3/script/colorzone -npm install && npm run build # esbuild → Babel → Rhino target +# Build Box3JS scripts +cd run/config/box3/script/colorzone && npm run build +cd run/config/box3/script/mygame && npm run build + +# Run project verification (Java ↔ DTS ↔ docs consistency) +cd NeoForge-1.21.1 && node tools/verify-box3js-project.mjs ``` **Important:** Forge-1.20.1 requires Java 17. All other subprojects use Java 21+. NeoForge-26.1 uses Java 25. @@ -58,70 +61,143 @@ All subprojects (including NeoForge-1.21.1) share the block mod's runtime genera ## Box3JS Scripting Engine (NeoForge-1.21.1 only) -Box3JS uses Mozilla Rhino to run server-side JavaScript/TypeScript. Scripts live in `run/config/box3/script//`. Each project has its own isolated scope, callbacks, and tracked state. +Box3JS uses Mozilla Rhino to run **dual-side** JavaScript/TypeScript — server scripts (`src/server/`) run on the server thread, client scripts (`src/client/`) run on each player's client. Scripts live in `run/config/box3/script//`. Each project has its own isolated scope, callbacks, and tracked state. + +### Package Structure + +| Package | Role | +|---------|------| +| `script/` | Server-side API bindings, engine, event bus, sandbox | +| `client/` | Client-side engine, API bindings (input, ui, gui, audio, chat, storage, db, http) | +| `registries/` | Recipe manager | +| `standalone/` | JS→JAR compiler, standalone bootstrap, registry code-gen | -### Java Package: `com.box3lab.box3js` +### Server-side Java (`script/`) + +| File | Role | +|------|------| +| `Box3ScriptEngine.java` | Singleton Rhino engine: load/reload/stop scripts, fire events, manage scopes | +| `Box3ScriptCommand.java` | `/box3script` command handler | +| `Box3ScriptConfig.java` | Config: enabled projects, sandbox state, file watcher | +| `Box3ScriptSandbox.java` | Tracks block/entity/player/world mutations for rollback | +| `Box3ScriptTemplate.java` | Template for `/box3script create` | +| `Box3ScriptWatcher.java` | File watching + auto-reload on `.js` change | +| `Box3JSEventBus.java` | Per-project callback storage with isolation | +| `Box3JSCallbacks.java` | Callback interface definitions | +| `Box3JSWorld.java` | `world.*` API: events, entity queries, scoreboard, BossBar, teams, border, particles, fireworks, recipes, structures | +| `Box3JSEntity.java` | `entity.*` API: position, velocity, HP, tags, AI, equipment, effects | +| `Box3JSPlayer.java` | `player.*` API: inventory, flight, game mode, teleport, XP, food, advancements, tab list | +| `Box3JSVoxels.java` | `voxels.*` API: get/set voxel, fill region, spawner control | +| `Box3JSRemoteChannel.java` | `remoteChannel.*` cross-side event communication | +| `Box3JSStorage.java` | Per-project JSON file persistence + `GameDataStorage` inner class | +| `Box3JSHttp.java` | Server-side HTTP API (`http.*`) | +| `Box3DatabaseBase.java` | Shared SQLite database base class (used by server + client db) | +| `Box3JSQueryResult.java` | SQL query result wrapper exposed to JS | +| `Box3JSResponse.java` | HTTP response wrapper (`GameHttpFetchResponse`) | +| `Box3JSConsole.java` | `console.*` (log/warn/error/debug/clear/assert) | +| `Box3JSGuiServerHandler.java` | Handles C→S GUI packets (slot clicks, close) on server thread | +| `Box3JSGuiController.java` | Side-agnostic GUI controller (callbacks via Consumer/Runnable) | +| `Box3JSScriptContainerMenu.java` | `AbstractContainerMenu` subclass using vanilla `MenuType.GENERIC_9xN` | +| `Box3ScriptUtils.java` | Shared helpers: sound, raycast, lookAt, stringify | +| `Box3JSScoreboard.java` / `Box3JSBossbar.java` / `Box3JSTeam.java` | Scoreboard / BossBar / Team CRUD | +| `GameVector3.java` / `GameBounds3.java` / `GameRGBColor.java` / `GameRGBAColor.java` / `GameQuaternion.java` | Math types exposed to JS | +| `GameEventHandlerToken.java` | Returned by all `onXxx()` — has `cancel()` and `active()` | + +### Client-side Java (`client/`) | File | Role | |------|------| -| `Box3JS.java` | `@Mod` entry point, subscribes to NeoForge events, fires callbacks into JS | -| `script/Box3ScriptEngine.java` | Singleton Rhino engine: load/reload/stop scripts, fire events, manage scopes | -| `script/Box3ScriptCommand.java` | `/box3script` command handler | -| `script/Box3ScriptConfig.java` | Config: enabled projects, sandbox state, file watcher | -| `script/Box3ScriptSandbox.java` | Tracks block/entity/player/world mutations for rollback | -| `script/Box3ScriptTemplate.java` | Template for `/box3script create` | -| `script/Box3ScriptWatcher.java` | File watching + auto-reload on `.js` change | -| `script/Box3JSWorld.java` | `world.*` API: events, entity queries, scoreboard, BossBar, teams, border, particles, fireworks, recipes, structures, custom items | -| `script/Box3JSEntity.java` | `entity.*` API: position, velocity, HP, tags, AI, equipment, effects | -| `script/Box3JSPlayer.java` | `player.*` API: inventory, flight, game mode, teleport, XP, food, advancements, tab list | -| `script/Box3JSVoxels.java` | `voxels.*` API: get/set voxel, fill region, spawner control | -| `script/Box3JSQuery.java` | `world.querySelectorAll()` / `entitiesInRadius()` etc. | -| `script/Box3JSEventBus.java` | Per-project callback storage with isolation | -| `script/Box3JSCallbacks.java` | Callback interface definitions | -| `script/Box3JSScoreboard.java` | Scoreboard CRUD | -| `script/Box3JSBossbar.java` | BossBar CRUD | -| `script/Box3JSTeam.java` | Team CRUD | -| `script/Box3JSStorage.java` | Per-project JSON file persistence | -| `script/Box3ScriptUtils.java` | Shared helpers: sound playing, raycast, entity lookAt | -| `script/GameVector3.java` | 3D vector exposed to JS (`new GameVector3(x, y, z)`) | -| `script/GameBounds3.java` | AABB bounds | -| `script/GameRGBColor.java` / `GameRGBAColor.java` | Color types | -| `script/GameQuaternion.java` | Quaternion math | -| `script/GameEventHandlerToken.java` | Returned by `world.onXxx()` — has `cancel()` and `active()` | -| `registries/Box3JSCustomItems.java` | Custom items via Minecraft data components on `minecraft:paper` carrier | -| `registries/Box3JSRecipeManager.java` | Recipe blacklist via `RecipeManager.replaceRecipes()` | - -### DTS Type Constraints - -`world.currentTick` and `world.projectName` are **methods** in `globals.d.ts`, not properties: -```ts -world.currentTick() // ✅ returns number -world.projectName() // ✅ returns string +| `Box3JSClientEngine.java` | Client-side Rhino engine; wires `client`, `audio`, `input`, `ui`, `chat`, `gui`, `remoteChannel`, `storage`, `db`, `http` globals | +| `Box3JSGuiProxy.java` | Returned by `gui.openGUI()` — stores callbacks, sends C→S packets | +| `Box3JSClientStorage.java` | Client-side JSON storage + `GameDataStorage` | +| `Box3JSClientDatabase.java` | Client-side SQLite with graceful-fallback reminder | +| `Box3JSClientHttp.java` | Client-side HTTP API | +| `screen/Box3JSScriptContainerScreen.java` | **DELETED** — vanilla `ChestScreen` renders the container now | + +### Network Layer + +`Box3JSNetwork.java` defines all custom payloads (C↔S): +- **GUI**: `GUIServerboundPayload` (open/click/close), `GUIClientboundPayload` (slot update/close) +- **RemoteChannel**: `RemoteChannelPayload` (server→client event), `RemoteChannelServerboundPayload` (client→server event) + +Payloads are registered with `optional()` — vanilla clients silently ignore them. + +`Box3JS.java` (`@Mod` class) registers all payload handlers, subscribes to NeoForge events, and fires callbacks into JS. + +### key constraints + +- **No custom `MenuType`**: GUI uses vanilla `MenuType.GENERIC_9x1` through `GENERIC_9x6`. This means vanilla clients never see unknown registry keys and can connect safely. +- `world.currentTick` and `world.projectName` are **methods**, not properties: `world.currentTick()`, `world.projectName()` +- All `onXxx()` event registration methods return `GameEventHandlerToken` (has `.cancel()` and `.active()`) + +### DTS Structure + +Template types live in `src/main/resources/assets/box3js/template/types/`: + ``` +types/ + shared.d.ts — math types, console, storage, db, http, remoteChannel + server/ + index.d.ts — references: shared, world, voxels, entity, player + server.d.ts — world, voxels, registries, server-specific remoteChannel/storage + world.d.ts — GameWorld interface + entity.d.ts — GameEntity + GamePlayerEntity type + player.d.ts — GamePlayer interface + voxels.d.ts — GameVoxels interface + client/ + index.d.ts — references: shared, client, audio, input, ui, chat, gui + client.d.ts — GameClient, RemoteChannel (client-side) + audio.d.ts — GameAudio + input.d.ts — GameInput + ui.d.ts — GameUI + chat.d.ts — GameChat + gui.d.ts — GameGUI + GuiController +``` + +**Template sync rule**: When changing template DTS, also sync to `colorzone/types/` and `mygame/types/`. ### Script Build Pipeline -`build.mjs` in each script project does: `esbuild bundle` → `Babel` (target Rhino 1.9.1) → regex sanitize for Rhino. Entry is always `src/app.ts`, output is `dist/app.js`. Supports `--watch` for hot reload. +`build.mjs` in each project: `esbuild bundle` → `Babel` (target Rhino 1.9.1) → regex sanitize. Two entry points: -### Custom Items System +``` +src/server/app.ts → dist/server.js (server-side, runs on server thread) +src/client/app.ts → dist/client.js (client-side, runs on each player's client) +``` + +Supports `--watch` for hot reload. ESLint uses split tsconfig: `tsconfig.server.json` + `tsconfig.client.json` (no root tsconfig). + +### ESLint Config -Uses `minecraft:paper` as carrier with `DataComponents` (CUSTOM_NAME, LORE, CUSTOM_MODEL_DATA, MAX_STACK_SIZE, ENCHANTMENT_GLINT_OVERRIDE, RARITY, FOOD). Client-side textures via resource pack `paper.json` with `custom_model_data` overrides. **No DeferredRegister** — no registry sync needed. +Projects with split tsconfig (no root `tsconfig.json`) must explicitly list them: + +```js +// eslint.config.mjs +export default [ + { + languageOptions: { + parserOptions: { + project: ['./tsconfig.server.json', './tsconfig.client.json'], + }, + }, + }, +]; +``` -Config: `resourcepacks/box3js-items/items.json` + textures + model JSONs. Loaded via `world.loadCustomItems("box3js-items")`. +### Registries System (Standalone/JAR mode) -Consumable/Cooldown/Enchantable/JukeboxPlayable components are NOT available in NeoForge 21.1.220 (need MC 1.21.2+). +`Box3JSRegistryGen.java` reads JSON config files (`registries/blocks.json`, `items.json`, `creativeTabs.json`, `sounds.json`) and generates Java registration code injected into the compiled `@Mod` class. `Box3ScriptCompiler.java` bundles JS source into a JAR; `Box3StandaloneBootstrap.java` launches it. ### Recipe Manager `Box3JSRecipeManager` uses `RecipeManager.replaceRecipes()` (public API, no reflection): -- `removeRecipe(id)` — filters via replaceRecipes -- `clearRecipes()` — restores full original list -- `listRecipes(filter)` — searches by keyword +- `removeRecipe(id)` / `clearRecipes()` / `listRecipes(filter)` ### Documentation -- `docs/api/` — Full API reference for world, entity, player, voxels, storage, math, commands (Chinese + English) -- `docs/tutorial/` — 5-part tutorial series (01-basics → 05-examples) with complete PvP arena and parkour game examples +- `docs/api/` — Full API reference: world, entity, player, voxels, storage, database, http, server, client, math, commands (CN + EN) +- `docs/guide/` — Getting started, architecture, JS vs Java comparison, FAQ, cookbook +- `docs/tutorial/` — 6-part tutorial (01-basics → 06-client-scripting) ## Version Differences @@ -132,5 +208,7 @@ Consumable/Cooldown/Enchantable/JukeboxPlayable components are NOT available in ## Tools -- **`tools/generate_blocks_fabric.py`** / **`tools/generate_blocks_forge.py`** — generates block registration code +- **`tools/verify-box3js-project.mjs`** — 7-check project integrity: template files, type split, event tokens, globals, Java↔DTS parity, DTS docs, docs↔API sync +- **`tools/box3js-api-manifest.json`** — Source of truth: 17 globals + 32 API groups with Java/DTS/docs paths, accessor property rules, ignore lists +- **`tools/generate_blocks_fabric.py`** / **`tools/generate_blocks_forge.py`** — Block registration code generators - **`tools/box3-texture-cut/`** — TypeScript tool for cutting sprite sheets into textures