diff --git a/Box3JS-NeoForge-1.21.1/README.md b/Box3JS-NeoForge-1.21.1/README.md index 22afc7a..de5300d 100644 --- a/Box3JS-NeoForge-1.21.1/README.md +++ b/Box3JS-NeoForge-1.21.1/README.md @@ -54,6 +54,16 @@ npm run build # 输出 dist/app.js [API 总览 →](docs/api/README.md) ([English](docs/api/README_en.md)) +## 教程 + +从零基础到完整小游戏,手把手教你用 Box3JS 写脚本: + +1. [从零开始](docs/tutorial/01-basics.md) — 创建项目、控制台、聊天命令、定时器 +2. [玩家与物品](docs/tutorial/02-player-items.md) — 传送、飞行、背包、自定义物品 +3. [事件与实体](docs/tutorial/03-events-entities.md) — 事件回调、实体生成/AI、计分板、队伍 +4. [高级游戏系统](docs/tutorial/04-advanced-systems.md) — BossBar、粒子、烟花、世界边界、PvP 竞技场 +5. [实用示例集](docs/tutorial/05-examples.md) — 传送系统、防破坏、波次刷怪、赛跑、捉迷藏等 + ## 命令 [命令详细参考 →](docs/api/commands.md) ([English](docs/api/commands_en.md)) diff --git a/Box3JS-NeoForge-1.21.1/README_en.md b/Box3JS-NeoForge-1.21.1/README_en.md index d765696..d11fc98 100644 --- a/Box3JS-NeoForge-1.21.1/README_en.md +++ b/Box3JS-NeoForge-1.21.1/README_en.md @@ -54,6 +54,16 @@ Back in game and enable it: [API Overview →](docs/api/README.md) ([English](docs/api/README_en.md)) +## Tutorials + +Step-by-step guides from basics to full mini-games: + +1. [Getting Started](docs/tutorial/01-basics.md) — Project setup, console, chat commands, timers +2. [Players & Items](docs/tutorial/02-player-items.md) — Teleport, flight, inventory, custom items +3. [Events & Entities](docs/tutorial/03-events-entities.md) — Event callbacks, entity spawn/AI, scoreboards, teams +4. [Advanced Systems](docs/tutorial/04-advanced-systems.md) — BossBar, particles, fireworks, world border, PvP arena +5. [Example Collection](docs/tutorial/05-examples.md) — Teleport, anti-grief, wave mobs, race checkpoints, hide-and-seek etc. + ## Commands [Full Command Reference →](docs/api/commands.md) ([English](docs/api/commands_en.md)) diff --git a/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md new file mode 100644 index 0000000..260d5e9 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/BOX3_API_COMPARISON.md @@ -0,0 +1,1146 @@ +# Box3 API vs Box3JS 实现对比 + +本文档详细对比官方 Box3 平台 API 与 Box3JS 模组(NeoForge 1.21.1)的实现差异。仅涉及**服务端 API**,因为客户端 API(ClientUI、ClientAudio、ClientMedia 等)在 Box3JS 中完全不可用——Minecraft 模组运行在服务端,没有 Box3 平台的客户端渲染环境。 + +> **图例**: ✅ 已实现 | ⚠️ 部分实现 | ❌ 未实现 | ⬆ MC 独有扩展 + +--- + +## 目录 + +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` (读写属性) | ✅ | 一致。映射到服务端 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,超出了服务端脚本引擎的范围。 + +### 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` | + +### 1.4 难度 + +| 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)` | ✅ | 一致 | + +### 1.6 游戏规则 + +Box3 **没有**游戏规则 API。Box3JS 完全为 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)` | — | ❌ | | + +**原因**: 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()` | — | ❌ | 实体配额查询 | + +### 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") | + +**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` + +### 1.10 区域系统 (GameZone) + +Box3 的区域系统完全未实现: + +| Box3 API | 状态 | +|----------|------| +| `world.addZone(config)` | ❌ | +| `world.removeZone(trigger)` | ❌ | +| `world.zones()` | ❌ | +| `zone.onEnter / zone.onLeave` | ❌ | +| `zone.entities` | ❌ | +| `zone.remove()` | ❌ | + +Zone 可以设置局部天气/光照/力场参数,这在 MC 服务端无法实现(需要客户端渲染支持)。 + +### 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)` | — | ❌ | | + +### 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 扩展。跨脚本消息 | + +**关键差异总结**: +1. 所有事件注册**已返回 GameEventHandlerToken**,支持 `.cancel()` 和 `.active()` +2. Box3 事件回调接收**事件对象**,Box3JS 回调接收**展开的参数列表**(JS 展开参数风格) +3. `onTick` 已对齐传入 `{tick, prevTick, elapsedTimeMS, skip}`;`onPlayerJoin/Leave/Respawn` 已添加 tick 参数 +4. Box3 的 `onVoxelContact` 每个碰撞帧触发(持续),Box3JS 仅在进入/离开时触发 + +### 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 扩展。展开参数版本 | + +### 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 的 setTimeout/setInterval 在官方文档中标注为 ⬆ MC 扩展,但 Box3JS 在 `world` 全局对象上直接提供,用法一致。 + +### 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 重载 | + +### 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 重载 | + +### 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 可引燃方块 | + +### 1.18 动画 + +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +|----------|-------------|------|---------| +| `world.animate(keyframes, playback?)` | — | ❌ | 世界级关键帧动画 | +| `world.getAnimations()` | — | ❌ | | +| `world.getEntityAnimations()` | — | ❌ | | +| `world.getPlayerAnimations()` | — | ❌ | | + +### 1.19 传送 + +| 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)` | + +### 1.21 控制台命令 + +| 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)` | 列出所有条目 | + +### 1.23 Boss 血条 (全部 MC 扩展) + +Box3 无 Bossbar 系统。 + +| Box3JS API | 说明 | +|------------|------| +| `world.showBossbar(name, text, progress, color)` | 显示或更新血条 | +| `world.removeBossbar(name)` | 移除血条 | + +### 1.24 队伍 (全部 MC 扩展) + +Box3 无队伍系统。 + +| Box3JS API | 说明 | +|------------|------| +| `world.createTeam(name, color)` | 创建队伍 | +| `world.removeTeam(name)` | 删除队伍 | +| `world.joinTeam(entity, teamName)` | 加入队伍 | +| `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)` | 警告距离 | + +--- + +## 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") | + +### 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()` | ✅ | 一致。返回所有标签的字符串数组 | + +### 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 扩展。是否着地 | + +### 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 扩展。设置/清除着火 | + +### 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` | — | ❌ | | + +**注意**: MC 物理由原版引擎控制,物理属性(friction/mass/restitution)存储为自定义属性供脚本读取,不会改变原版物理行为。collides/fixed/gravity 有对应 MC 副作用(noGravity)。 + +### 2.6 粒子 + +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` | ❌ | + +### 2.7 音效 + +| 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)` | — | ❌ | | + +### 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() 调用后实体立即消失 | + +### 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` 替代 | + +**注意**: 所有 `world.onXxx()` 事件回调注册后返回 `GameEventHandlerToken`,支持 `.cancel()` / `.active()`,与 Box3 一致。实体级事件 (`entity.onDestroy`) 暂不返回 token。 + +### 2.11 动画 + +| 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)` | — | ❌ | 局部缩放 | + +### 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)` | 使实体看向某位置 | + +--- + +## 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` (属性) | — | ❌ | | + +### 3.2 社交 + +| 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?)` | — | ❌ | | + +### 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` (只读属性) | ✅ | 一致 | + +### 3.5 画面滤镜 + +| 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 扩展。带时间的标题 | + +### 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` (属性) | ✅ | 一致 | + +### 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)` | ✅ | 一致。额外支持自定义踢出原因 | + +### 3.9 输入事件 + +| Box3 API | Box3JS 实现 | 状态 | 差异说明 | +|----------|-------------|------|---------| +| `player.onPress(handler)` | — | ❌ | 可用 `world.onButtonPressed` 替代 | +| `player.onRelease(handler)` | — | ❌ | | +| `player.onKeyDown(handler)` | — | ❌ | 键盘事件,MC 服务端无法获取 | +| `player.onKeyUp(handler)` | — | ❌ | | + +### 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 展开参数 | + +### 3.11 链接/跳转 + +| 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()` | — | ❌ | + +### 3.13 玩家动画 + +| 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 列表显示名称 | + +--- + +## 4. GameVoxels (voxels) + +Box3JS 的 Voxels 实现是所有 API 中**最完整**的。 + +### 4.1 属性 + +| 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→名称 | + +### 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 重载 | + +**旋转编码方案一致**: `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 重载 | + +### 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)` | 设置刷怪笼类型 | + +--- + +## 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)` | ✅ | 一致 | + +### 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 扩展。返回存储空间名称 | + +### 5.3 关键差异 + +1. **同步 vs 异步**: Box3 的 DataStorage 是异步 API(基于网络),Box3JS 是同步的本地文件存储。在 JS 中调用方式不同:Box3 需要 `await ds.get("key")`,Box3JS 直接 `ds.get("key")`。 +2. **返回值**: Box3 的 `get` 返回 `ReturnValue {key, value, updateTime, createTime, version}`,Box3JS 的 `get` 直接返回 value。 +3. **存储位置**: Box3 使用云端数据库,Box3JS 使用本地 JSON 文件(`config/box3/storage/`)。 +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)` (静态) | ✅ | ✅ | | + +**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 | + +**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()` (静态) | ✅ | ✅ | | + +**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()` | ✅ | ✅ | | + +**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)` (静态) | ✅ | ✅ | | + +**GameQuaternion 是完全实现的**。 + +--- + +## 7. 其他服务端 API + +### 7.1 GameAnimation + +**状态**: ❌ 完全未实现 + +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/...) | ❌ | + +### 7.2 GameMotionController + +**状态**: ❌ 完全未实现 + +Box3 的 Voxa 模型动画系统,用于控制自定义模型的骨骼动画。 + +| Box3 API | 状态 | +|----------|------| +| `entity.motion` | ❌ | +| `motion.loadByName(configs)` | ❌ | +| `motion.pause()` / `motion.resume()` | ❌ | +| `motion.setDefaultMotionByName(name)` | ❌ | +| `GameMotionHandler` (play/cancel/onFinish) | ❌ | + +### 7.3 Sound + +**状态**: ❌ 未实现独立 Sound 对象 + +Box3 的 `sound()` 方法返回 Sound 对象,支持 `setCurrentTime`/`resume`/`pause`/`stop`。Box3JS 的 `playSound` 是 fire-and-forget 模式,无法控制播放中的声音。 + +### 7.4 RemoteChannel (跨端通信) + +**状态**: ❌ 完全未实现 + +Box3 的 `remoteChannel` 用于服务端↔客户端双向通信。MC 模组运行在服务端,无客户端代码,无法实现。 + +| Box3 API | 状态 | +|----------|------| +| `remoteChannel.sendClientEvent(entities, event)` (服务端) | ❌ | +| `remoteChannel.broadcastClientEvent(event)` (服务端) | ❌ | +| `remoteChannel.onServerEvent(handler)` (服务端) | ❌ | +| `remoteChannel.sendServerEvent(event)` (客户端) | ❌ | +| `remoteChannel.onClientEvent(handler)` (客户端) | ❌ | + +**替代方案**: Box3JS 提供了 `world.sendMessage(target, data)` 和 `world.onMessage(fn)` 用于**脚本间**通信(同一服务端不同脚本项目),但这与 Box3 的跨端通信不同。 + +### 7.5 GameRTC (实时语音) + +**状态**: ❌ 完全未实现 + +MC 无内置语音通信,无法实现。 + +### 7.6 GameHttpAPI (http) + +**状态**: ❌ 未实现 + +Box3 的 `http.fetch(url, options?)` 用于服务端发起 HTTP 请求。Box3JS 暂未实现。 + +### 7.7 GameAnalytics (analytics) + +**状态**: ❌ 未实现 + +Box3 的神策数据分析埋点。 + +### 7.8 GameAssetListEntry (resources) + +**状态**: ❌ 未实现 + +Box3 的 `resources.ls(type?)` 浏览资源文件。MC 无对应资源管理 API。 + +### 7.9 GameEventHandlerToken + +**状态**: ❌ 未实现 + +Box3 的事件注册方法返回 `GameEventHandlerToken`,可调用 `.cancel()` / `.resume()` / `.active()`。Box3JS 没有此机制,回调注册后无法单独取消。 + +--- + +## 8. 客户端 API(不适用) + +以下 Box3 API 全部运行在**客户端**(玩家浏览器),Box3JS 作为纯服务端模组**完全无法实现**: + +| 类别 | 全局对象 | 说明 | +|------|---------|------| +| ClientUI | `ui`, `input`, `screenWidth`, `screenHeight` | 2D UI 系统(盒子、文本、图片、输入框、滚动框等) | +| ClientAudio | `Audio` | 客户端音频播放 | +| ClientMedia | `media` | 录音/播放 | +| ClientNavigator | `navigator` | 设备信息、语言 | +| ClientScreen | `screen` | 屏幕尺寸事件 | +| ClientHttp | `http` (客户端) | 客户端 HTTP 请求 | +| ClientWorld | `world` (客户端) | 3D 渲染开关 | + +--- + +## 9. Box3JS 独有 MC 扩展 + +这些 API 是 Box3JS 利用 Minecraft 原生能力提供的,在 Box3 平台**不存在**: + +### 9.1 世界管理 +- `world.thunderDensity` — 雷暴强度 +- `world.clearWeather()` — 清除天气 +- `world.getGameRule/setGameRule` — 游戏规则 +- `world.runCommand(cmd)` — 控制台命令 +- `world.onVoxelDestroy` — 方块破坏事件 +- `world.onBlockPlace` — 方块放置事件 +- `world.onBlockActivate` — 方块右键事件 +- `world.entitiesInArea/entitiesInRadius` — 空间实体查询 +- `world.getBiome` — 生物群系查询 +- `world.spawnParticleCircle` — 圆形粒子 + +### 9.2 实体管理 +- `entity.nameTag` — 名牌 +- `entity.glowing` — 发光 +- `entity.invulnerable` — 无敌 +- `entity.onGround` — 着地状态 +- `entity.eyePosition` — 眼睛位置 +- `entity.navigateTo` — 寻路 +- `entity.setTarget/clearTarget/getTarget` — 战斗目标 +- `entity.setAI` — AI 开关 +- `entity.heal` — 治疗 +- `entity.addEffect` — 药水效果 +- `entity.setEquipment/setDropChance` — 装备管理 +- `entity.setPersistent` — 持久化 +- `entity.getAttribute/setAttribute` — 属性修改 +- `entity.lookAt` — 视线方向 + +### 9.3 玩家管理 +- `player.opLevel` — OP 权限 +- `player.gameMode` — 游戏模式 +- `player.dimension` — 维度切换 +- `player.flying` — 飞行状态 +- `player.collision` — 碰撞开关 +- `player.disableFly` — 禁用飞行 +- `player.actionBar` — ActionBar 消息 +- `player.title` — 标题/副标题 +- `player.giveItem/giveEnchantedItem/giveNamedItem` — 物品给予 +- `player.getHeldItem` — 手持物品 +- `player.clearInventory` — 清空背包 +- `player.addEffect/clearEffects` — 药水效果 +- `player.xp/addExperienceLevels` — 经验管理 +- `player.food/saturation` — 饥饿管理 +- `player.runCommand` — 以玩家身份执行命令 +- `player.lookAt` — 视线方向 +- `player.setPlayerListName` — TAB 列表名称 + +### 9.4 系统 +- `world.addScoreboard/removeScoreboard/setScore/getScore/showScoreboard/hideScoreboard/listScores` — 记分板 +- `world.showBossbar/removeBossbar` — Boss 血条 +- `world.createTeam/removeTeam/joinTeam/leaveTeam/getTeamOf` — 队伍管理 +- `world.borderSize/setBorderCenter/shrinkBorder/setBorderDamage/setBorderWarning` — 世界边界 +- `world.sendMessage/onMessage` — 跨脚本通信 +- `voxels.fillVoxel/countVoxel/getVoxelName/setSpawner` — 方块批量操作 + +### 9.5 额外事件 +- `world.onEntityDamage` — 实体受伤(Pre 阶段) +- `world.onMessage` — 跨脚本消息 + +--- + +## 10. 总结 + +### 10.1 实现统计 + +| 类别 | Box3 API 总数 | 已实现 | 部分实现 | 未实现 | MC 独有扩展 | +|------|--------------|--------|---------|--------|-------------| +| GameWorld | ~80 | ~30 | ~6 | ~44 | 27 | +| GameEntity | ~65 | ~21 | ~1 | ~43 | 17 | +| GamePlayerEntity | ~72 | ~27 | ~4 | ~41 | 17 | +| GameVoxels | 14 | 14 | 0 | 0 | 4 | +| GameDataStorage | 8 | 8 | 7 (同步化) | 0 | 2 | +| Math 类型 | ~100 | ~100 | 0 | 0 | 0 | +| 其他服务端 | ~30 | 0 | 0 | ~30 | 0 | +| **总计** | **~369** | **~200** | **~18** | **~158** | **~67** | + +> **2026-05 更新**: 本阶段实现约 54 个新 Box3 API(属性对齐 + math 补全 + 物理属性 + token + 回调签名 + World API 补全),Math 类型现已完全对齐。 + +### 10.2 核心差异模式 + +1. **事件回调签名**: Box3 传事件对象 → Box3JS 传展开参数;已为 onTick/onPlayerJoin/Leave/Respawn 添加 tick 参数 +2. **异步存储**: Box3 的 Promise 存储 → Box3JS 的同步本地文件存储 +3. **视觉/渲染 API**: Box3 有独立渲染引擎 → Box3JS 依赖 MC 原版渲染,无法控制雾/光照/雪花/粒子系统参数 +4. **物理 API**: 基本物理属性(collides/fixed/gravity/friction/mass/restitution)已对齐,高级物理(接触力/OBB/碰撞过滤)不支持 +5. **客户端 API**: Box3 有完整 UI/音频/媒体客户端 API → Box3JS 纯服务端,全部不可用 +6. **动画系统**: Box3 有关键帧动画 → Box3JS 无 +7. **事件令牌**: 已实现 GameEventHandlerToken,所有 world.onXxx() 返回 token,支持 cancel()/active() + +### 10.3 Box3JS 的独特优势 + +Box3JS 虽然缺失大量 Box3 视觉/渲染/客户端 API,但提供了 Box3 平台**完全不具备**的 MC 原生能力: + +- **完整的原版方块系统** — 数百种方块类型、红石、容器、刷怪笼 +- **生物 AI/寻路/战斗** — 全套 MC 生物行为控制 +- **原版物品/装备/附魔** — 完整的物品系统 +- **药水效果/属性修改** — 精细的属性控制 +- **计分板/Bossbar/队伍** — MC 原生的 UI 系统 +- **世界边界** — 缩圈玩法 +- **维度切换** — 下界/末地传送 +- **游戏模式/OP 权限** — 权限管理 +- **跨脚本通信** — 模块化脚本协作 + +### 10.4 迁移建议 + +从 Box3 平台迁移脚本到 Box3JS 时: + +1. **视觉相关代码需重写** — 雾/光照/天气效果无法直接迁移,需使用 MC 原版机制代替 +2. **回调参数需调整** — 将事件对象访问改为参数列表访问 +3. **异步存储改为同步** — 移除 `await`,直接调用存储方法 +4. **实体外观/物理不可用** — mesh/粒子/物理属性需删除或用 MC 代替 +5. **客户端代码全废弃** — UI/音频/媒体代码无对应 +6. **可充分利用 MC 扩展** — 记分板/Bossbar/生物 AI/物品系统/药水效果等是 Box3 没有的强大功能 diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity.md b/Box3JS-NeoForge-1.21.1/docs/api/entity.md index d8c1eef..8ad15a5 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity.md @@ -1,22 +1,22 @@ # entity — 实体 API -`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。通过 `world.spawnEntity()`、`world.querySelector()`、`world.entitiesInRadius()` 或事件回调参数获取。 +`entity` 代表 Minecraft 世界中的任意实体(怪物、动物、掉落物、玩家)。通过 `world.spawnEntity()`、`world.createEntity()`、`world.querySelector()`、`world.searchBox()`、`world.entitiesInRadius()` 或事件回调参数获取。 -通过 `entity.player` 可获取该实体对应的 `player` 对象(仅当是玩家时有效)。 +通过 `entity.player` 可获取该实体对应的 `player` 对象(仅当是玩家时非 null)。 -## 基本属性 +## 基本身份 ### entity.id -✅ Box3 API | 只读。实体的 UUID 字符串。 +✅ Box3 API | 只读。实体的 UUID 字符串(如 `"550e8400-e29b-41d4-a716-446655440000"`)。 ### entity.isPlayer() -✅ Box3 API | 返回 `true` 表示该实体是玩家。 +✅ Box3 API | 返回 `true` 表示该实体是玩家。当返回 `true` 后,`entity.player` 必定非 null。 ### entity.entityType -✅ Box3 API | 只读。返回实体的命名空间 ID 字符串。 +✅ Box3 API | 只读。返回实体的命名空间 ID 字符串(如 `"minecraft:zombie"`)。 ```js var all = world.querySelectorAll("*"); @@ -30,41 +30,45 @@ for (var i = 0; i < all.length; i++) { ### entity.position -✅ Box3 API | 只读 `GameVector3`。注意:这是一个 LiveVec3,调用 `.set(x,y,z)` 可直接传送实体。 +✅ Box3 API | 只读 `GameVector3`。这是一个 **LiveVec3**:读取时自动同步实体当前坐标,调用 `.set(x,y,z)` 可**直接传送实体**。 ```js var pos = entity.position; console.log(pos.x, pos.y, pos.z); -// 传送 +// 传送实体 entity.position.set(0, 100, 0); ``` ### entity.velocity -✅ Box3 API | 只读 `GameVector3`。LiveVec3,`.set(x,y,z)` 可直接修改速度。 +✅ Box3 API | 只读 `GameVector3`。**LiveVec3**:读取时自动同步当前速度,`.set(x,y,z)` 直接设置速度向量。 ```js -entity.velocity.set(0, 1, 0); // 向上的速度 +entity.velocity.set(0, 1, 0); // 向上弹射 +entity.velocity.set(2, 0, 2); // 水平方向速度 ``` ### entity.bounds -✅ Box3 API | 只读 `GameVector3`。实体的包围盒半尺寸(half-size)。 +✅ Box3 API | 只读 `GameVector3`。实体的包围盒**半尺寸** (half-extents): +- `x` = 宽度 / 2 +- `y` = 高度 / 2 +- `z` = 宽度 / 2 ### entity.onGround -⬆ MC 扩展 | 只读。实体是否在地面上。 +⬆ MC 扩展 | 只读。实体是否站在方块上。 ```js if (entity.onGround) { - // 在地面 + // 在地面上 } ``` ### entity.eyePosition -⬆ MC 扩展 | 只读 `GameVector3`。实体视线高度位置。 +⬆ MC 扩展 | 只读 `GameVector3`。实体视线高度位置(射线检测起点)。 ```js var eye = entity.eyePosition; @@ -74,11 +78,11 @@ var eye = entity.eyePosition; ### entity.hp -✅ Box3 API | 获取/设置当前生命值(仅 LivingEntity 有效)。 +✅ Box3 API | 获取/设置当前生命值。非 LivingEntity 返回/存储自定义属性。 ### entity.maxHp -✅ Box3 API | 获取/设置最大生命值(仅 LivingEntity 有效)。 +✅ Box3 API | 获取/设置最大生命值。设置后若当前生命超过新上限会自动截断。 ```js var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); @@ -88,57 +92,115 @@ zombie.hp = 100; ### entity.hurt(amount) -✅ Box3 API | 对实体造成 `amount` 点伤害(触发伤害事件)。 +✅ Box3 API | 对实体造成 `amount` 点伤害(通用伤害类型,触发伤害事件)。 ### entity.heal(amount) ✅ Box3 API | 治疗实体 `amount` 点生命值(不超过 maxHp)。 ```js -zombie.hurt(10); // 造成 10 点伤害 -zombie.heal(5); // 治疗 5 点 +zombie.hurt(10); // 造成 10 点伤害 +zombie.heal(5); // 治疗 5 点 ``` ### entity.invulnerable -⬆ MC 扩展 | 获取/设置实体是否无敌。 +⬆ MC 扩展 | 获取/设置实体是否无敌(不受伤害)。 ```js -entity.invulnerable = true; // 不受伤害 +entity.invulnerable = true; console.log(entity.invulnerable); ``` +### entity.destroyed + +✅ Box3 API | 只读。实体是否已被移除/销毁。 + +## 物理属性 + +✅ Box3 API | 以下属性控制实体的物理行为。 + +### entity.collides + +获取/设置实体是否参与碰撞。默认 `true`。设为 `false` 时对 LivingEntity 禁用物理 (setNoPhysics)。 + +```js +entity.collides = false; // 无碰撞幽灵 +``` + +### entity.fixed + +获取/设置实体是否固定。默认 `false`。设为 `true` 时禁用重力并每 tick 清零移动速度。 + +```js +entity.fixed = true; // 固定装饰物,不受重力不掉落 +``` + +### entity.gravity + +获取/设置实体是否受重力影响。默认 `true`。设为 `false` 时禁用重力 (setNoGravity)。 + +```js +entity.gravity = false; // 无重力漂浮 +``` + +### entity.friction + +获取/设置摩擦系数(自定义属性,默认 `0.0`)。脚本可读取此值自行处理摩擦逻辑。 + +### entity.mass + +获取/设置质量(自定义属性,默认 `1.0`)。脚本可读取此值自行处理物理计算。 + +### entity.restitution + +获取/设置弹性系数 / 反弹力(自定义属性,默认 `0.0`)。脚本可读取此值自行处理碰撞反弹。 + +```js +// 创建弹跳球 +var ball = world.createEntity({ + type: "minecraft:slime", + position: new GameVector3(0, 100, 0), + gravity: true, + collides: true, + restitution: 0.8, + mass: 0.5 +}); +``` + ## 外观 ### entity.meshInvisible -✅ Box3 API | 控制实体是否不可见。 +✅ Box3 API | 控制实体是否不可见(隐身)。 ```js -entity.meshInvisible = true; // 隐身 +entity.meshInvisible = true; // 隐身 +console.log(entity.meshInvisible); ``` ### entity.glowing -⬆ MC 扩展 | 获取/设置发光效果(类似光灵箭效果)。 +⬆ MC 扩展 | 获取/设置发光效果(类似光灵箭轮廓高亮)。 ```js -entity.glowing = true; // 实体发光 +entity.glowing = true; console.log(entity.glowing); ``` ### entity.nameTag -⬆ MC 扩展 | 获取/设置实体的自定义名称(头上显示的名字)。 +⬆ MC 扩展 | 获取/设置实体的自定义名称(头上显示的名字,支持颜色代码)。空字符串 = 无名称。 ```js entity.nameTag = "§cBoss 怪物"; -console.log(entity.nameTag); +console.log(entity.nameTag); // 属性方式读取 +entity.setNameTag("§e守卫"); // 方法方式设置 ``` ## 标签系统 -全部 ✅ Box3 API。标签是附加在实体上的字符串标记,用于分类和查询。 +全部 ✅ Box3 API。标签是附加在实体上的字符串标记(实质是 Minecraft 的 scoreboard tags),用于分类和查询。 ### entity.addTag(tag) @@ -152,6 +214,10 @@ console.log(entity.nameTag); 移除标签。 +### entity.tags() + +返回所有标签的字符串数组。 + ```js entity.addTag("boss"); entity.addTag("red_team"); @@ -160,6 +226,12 @@ if (entity.hasTag("boss")) { entity.maxHp = 200; } +// 获取全部标签 +var allTags = entity.tags(); +for (var i = 0; i < allTags.length; i++) { + console.log(allTags[i]); +} + // 通过标签查询 var bosses = world.querySelectorAll(".boss"); ``` @@ -175,8 +247,8 @@ var bosses = world.querySelectorAll(".boss"); ⬆ MC 扩展 | 扑灭实体火焰。 ```js -entity.setFire(100); // 点燃 5 秒 -entity.clearFire(); // 立即扑灭 +entity.setFire(100); // 点燃 5 秒 +entity.clearFire(); // 立即扑灭 ``` ## AI 与导航 @@ -186,37 +258,39 @@ entity.clearFire(); // 立即扑灭 ⬆ MC 扩展 | 启用/禁用实体 AI(仅 Mob 有效)。禁用后实体不会移动或攻击。 ```js -entity.setAI(false); // 冻结实体 +entity.setAI(false); // 冻结实体 ``` -### entity.setTarget(entity) +### entity.setTarget(target) -⬆ MC 扩展 | 设置怪物攻击目标(仅 Mob 有效)。 +⬆ MC 扩展 | 设置怪物的攻击目标(仅 Mob 有效)。怪物会自动寻路并攻击该目标。 ### entity.getTarget() -⬆ MC 扩展 | 获取当前攻击目标,返回 `Box3JSEntity` 或 null。 +⬆ MC 扩展 | 获取当前攻击目标,返回 `GameEntity` 或 `null`。 ### entity.clearTarget() -⬆ MC 扩展 | 清除攻击目标。 +⬆ MC 扩展 | 清除攻击目标,停止追击。 ```js var boss = world.spawnEntity("minecraft:skeleton", new GameVector3(0, 100, 0)); var target = world.querySelectorAll("*")[0]; boss.setTarget(target); +// ... +boss.clearTarget(); ``` ### entity.navigateTo(x, y, z, speed) -⬆ MC 扩展 | 让实体寻路到目标坐标(仅 PathfinderMob 有效)。 +⬆ MC 扩展 | 让实体寻路到目标坐标(仅 PathfinderMob 有效)。返回 `true` 表示路径计算成功。 ### entity.navigateTo(pos, speed) ⬆ GameVector3 重载。 ```js -entity.navigateTo(10, 100, 10, 1.0); // 以 1.0 速度走过去 +entity.navigateTo(10, 100, 10, 1.0); entity.navigateTo(target.position, 1.0); ``` @@ -239,16 +313,16 @@ entity.lookAt(target.position); ### entity.addEffect(effectId, duration, amplifier) -添加药水效果。`duration` 单位为 tick,`amplifier` 从 0 开始。 +添加药水效果。`duration` 单位为 tick (20 tick = 1秒),`amplifier` 从 0 开始(0 = 一级效果)。 ### entity.addEffect(effectId, duration, amplifier, hideParticles) -添加效果并可选隐藏粒子。 +添加效果并可选择隐藏粒子。 ```js -entity.addEffect("minecraft:speed", 600, 2); // 速度 III,30 秒 -entity.addEffect("minecraft:strength", 99999, 1, true); // 永久力量 II,不显示粒子 -entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 +entity.addEffect("minecraft:speed", 600, 2); // 速度 III,30 秒 +entity.addEffect("minecraft:strength", 99999, 1, true); // 永久力量 II,无粒子 +entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 // 常用效果: // minecraft:speed, minecraft:slowness, minecraft:strength @@ -263,9 +337,16 @@ entity.addEffect("minecraft:glowing", 200, 0); // 发光 10 秒 ### entity.setEquipment(slot, itemId) -给生物穿戴装备。 +给生物穿戴装备。**slot 值:** -**slot 值:** `"mainhand"`、`"offhand"`、`"head"`(头盔)、`"chest"`(胸甲)、`"legs"`(护腿)、`"feet"`(靴子) +| slot | 说明 | +|------|------| +| `"mainhand"` | 主手 | +| `"offhand"` | 副手 | +| `"head"`, `"helmet"`, `"helm"` | 头盔 | +| `"chest"`, `"chestplate"` | 胸甲 | +| `"legs"`, `"leggings"` | 护腿 | +| `"feet"`, `"boots"` | 靴子 | ```js entity.setEquipment("mainhand", "minecraft:diamond_sword"); @@ -276,11 +357,11 @@ entity.setEquipment("feet", "minecraft:leather_boots"); ### entity.setDropChance(slot, chance) -设置装备槽物品的掉落概率,0.0–1.0。`slot` 设为 `"all"` 可一次性设置所有槽位。 +设置装备槽物品的掉落概率,范围 0.0–1.0。`slot` 设为 `"all"` 可一次性设置所有槽位(包括主副手和四个护甲槽)。 ```js -entity.setDropChance("mainhand", 0.5); // 50% 概率掉落主手物品 -entity.setDropChance("all", 0); // 不掉落任何装备 +entity.setDropChance("mainhand", 0.5); // 50% 概率掉落主手物品 +entity.setDropChance("all", 0); // 不掉落任何装备 ``` ## 属性 @@ -289,11 +370,11 @@ entity.setDropChance("all", 0); // 不掉落任何装备 ### entity.getAttribute(attributeId) -获取实体属性当前值。 +获取实体属性当前值。非 LivingEntity 返回 0。 ### entity.setAttribute(attributeId, value) -设置实体属性基值。 +设置实体属性基值。仅 LivingEntity 有效。 ```js var attack = entity.getAttribute("minecraft:generic.attack_damage"); @@ -304,37 +385,36 @@ entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); entity.setAttribute("minecraft:generic.armor", 10); ``` -> 注意:`maxHp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性,仅当需要访问未封装的属性时才用 `setAttribute`。 +> 注意:`maxHp` / `hp` / `walkSpeed` / `jumpPower` 等 Box3 便捷属性内部也使用这些 attribute,推荐优先使用便捷属性。仅当需要访问未封装的属性时才用 `setAttribute`。 ## 生命周期 ### entity.destroy() -✅ Box3 API | 销毁实体。如果设置了 `onDestroy` 回调,会触发它。 - -### entity.remove() - -⬆ MC 扩展 | 直接移除实体,**不触发** `onDestroy` 回调。 +✅ Box3 API | 销毁实体。如果通过 `setOnDestroy()` 设置了回调,会触发它。 ### entity.setOnDestroy(handler) ✅ Box3 API | 设置销毁回调。`handler` 接收一个参数 `(entity)`。 -### entity.destroyed - -✅ Box3 API | 只读。实体是否已被移除。 +```js +entity.setOnDestroy(function(e) { + console.log("实体 " + e.id + " 被销毁"); +}); +``` ### entity.setPersistent(v) -⬆ MC 扩展 | 设为 `true` 时生物不会因远离玩家而自然消失(仅 Mob 有效)。 +⬆ MC 扩展 | 设为 `true` 时生物不会因远离玩家而自然消失(仅 Mob 有效)。仅写方法,无 getter。 ```js var boss = world.spawnEntity( "minecraft:wither_skeleton", new GameVector3(0, 100, 0), ); -boss.setPersistent(true); // 不会消失 -boss.setOnDestroy((e) => { +boss.setPersistent(true); // 不会自然消失 +boss.setNameTag("§c§l凋零守卫"); +boss.setOnDestroy(function(e) { world.say("Boss 被击败了!"); }); ``` @@ -343,6 +423,8 @@ boss.setOnDestroy((e) => { ✅ Box3 API | 可以直接在 entity 上存储任意 JS 数据,存活期等于实体生命周期。 +自定义属性存储在 entity 的 UUID 下,通过 `ConcurrentHashMap` 持久化直到实体被移除。 + ```js entity.myCustomField = "hello"; entity.spawnTick = world.currentTick(); diff --git a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md b/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md index 007e814..6bbe495 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/entity_en.md @@ -1,22 +1,22 @@ # entity — Entity API -`entity` represents any entity in the Minecraft world (mobs, animals, items, players). Obtain via `world.spawnEntity()`, `world.querySelector()`, `world.entitiesInRadius()`, or event callback parameters. +`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. -Use `entity.player` to get the corresponding `player` object (only valid if the entity is a player). +Use `entity.player` to get the corresponding `player` object (non-null only when the entity is a player). -## Basic Properties +## Basic Identity ### entity.id -✅ Box3 API | Read-only. The entity's UUID string. +✅ Box3 API | Readonly. The entity's UUID string (e.g. `"550e8400-e29b-41d4-a716-446655440000"`). ### entity.isPlayer() -✅ Box3 API | Returns `true` if this entity is a player. +✅ Box3 API | Returns `true` if the entity is a player. When true, `entity.player` is guaranteed non-null. ### entity.entityType -✅ Box3 API | Read-only. Returns the entity's namespaced ID string. +✅ Box3 API | Readonly. Returns the entity's namespace ID string (e.g. `"minecraft:zombie"`). ```js var all = world.querySelectorAll("*"); @@ -30,7 +30,7 @@ for (var i = 0; i < all.length; i++) { ### entity.position -✅ Box3 API | Read-only `GameVector3`. Note: this is a LiveVec3 — calling `.set(x,y,z)` directly teleports the entity. +✅ Box3 API | Readonly `GameVector3`. This is a **LiveVec3**: reading syncs to the entity's current coordinates; calling `.set(x,y,z)` **directly teleports** the entity. ```js var pos = entity.position; @@ -42,29 +42,33 @@ entity.position.set(0, 100, 0); ### entity.velocity -✅ Box3 API | Read-only `GameVector3`. LiveVec3 — `.set(x,y,z)` directly modifies velocity. +✅ Box3 API | Readonly `GameVector3`. **LiveVec3**: reading syncs to current velocity; `.set(x,y,z)` directly sets the velocity vector. ```js -entity.velocity.set(0, 1, 0); // upward velocity +entity.velocity.set(0, 1, 0); // Launch upward +entity.velocity.set(2, 0, 2); // Horizontal velocity ``` ### entity.bounds -✅ Box3 API | Read-only `GameVector3`. The entity's bounding box half-size. +✅ Box3 API | Readonly `GameVector3`. The entity's bounding box **half-extents**: +- `x` = width / 2 +- `y` = height / 2 +- `z` = width / 2 ### entity.onGround -⬆ MC Extension | Read-only. Whether the entity is on the ground. +⬆ MC extension | Readonly. Whether the entity is standing on a block. ```js if (entity.onGround) { - // on ground + // On the ground } ``` ### entity.eyePosition -⬆ MC Extension | Read-only `GameVector3`. The entity's eye-level position. +⬆ MC extension | Readonly `GameVector3`. Eye position (raycast origin). ```js var eye = entity.eyePosition; @@ -74,11 +78,11 @@ var eye = entity.eyePosition; ### entity.hp -✅ Box3 API | Get/set current health (LivingEntity only). +✅ Box3 API | Gets/sets current health. For non-LivingEntity, returns/stores a custom property. ### entity.maxHp -✅ Box3 API | Get/set maximum health (LivingEntity only). +✅ Box3 API | Gets/sets maximum health. If current health exceeds the new maximum, it is clamped. ```js var zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); @@ -88,26 +92,82 @@ zombie.hp = 100; ### entity.hurt(amount) -✅ Box3 API | Deal `amount` damage to the entity (triggers damage event). +✅ Box3 API | Deals `amount` generic damage to the entity (triggers damage events). ### entity.heal(amount) -✅ Box3 API | Heal the entity by `amount` (capped at maxHp). +✅ Box3 API | Heals the entity by `amount` (capped at maxHp). ```js -zombie.hurt(10); // deal 10 damage -zombie.heal(5); // heal 5 +zombie.hurt(10); // Deal 10 damage +zombie.heal(5); // Heal 5 ``` ### entity.invulnerable -⬆ MC Extension | Get/set whether the entity is invulnerable. +⬆ MC extension | Gets/sets whether the entity is invulnerable (immune to damage). ```js -entity.invulnerable = true; // immune to damage +entity.invulnerable = true; console.log(entity.invulnerable); ``` +### entity.destroyed + +✅ Box3 API | Readonly. Whether the entity has been removed/destroyed. + +## Physics + +✅ Box3 API | The following properties control entity physics behavior. + +### entity.collides + +Gets/sets whether the entity participates in collisions. Default `true`. When `false`, disables physics for LivingEntity (setNoPhysics). + +```js +entity.collides = false; // No-collision ghost +``` + +### entity.fixed + +Gets/sets whether the entity is fixed in place. Default `false`. When `true`, disables gravity and zeros velocity each tick. + +```js +entity.fixed = true; // Stationary decoration, no gravity +``` + +### entity.gravity + +Gets/sets whether the entity is affected by gravity. Default `true`. When `false`, disables gravity (setNoGravity). + +```js +entity.gravity = false; // Floats without gravity +``` + +### entity.friction + +Gets/sets friction coefficient (custom property, default `0.0`). Scripts can read this to implement custom friction logic. + +### entity.mass + +Gets/sets mass (custom property, default `1.0`). Scripts can read this for custom physics calculations. + +### entity.restitution + +Gets/sets restitution / bounciness (custom property, default `0.0`). Scripts can read this for custom collision response. + +```js +// Create a bouncy ball +var ball = world.createEntity({ + type: "minecraft:slime", + position: new GameVector3(0, 100, 0), + gravity: true, + collides: true, + restitution: 0.8, + mass: 0.5 +}); +``` + ## Appearance ### entity.meshInvisible @@ -115,42 +175,48 @@ console.log(entity.invulnerable); ✅ Box3 API | Controls entity invisibility. ```js -entity.meshInvisible = true; // invisible +entity.meshInvisible = true; // Invisible +console.log(entity.meshInvisible); ``` ### entity.glowing -⬆ MC Extension | Get/set glowing effect (like spectral arrow effect). +⬆ MC extension | Gets/sets the glow outline effect (similar to spectral arrow). ```js -entity.glowing = true; // entity glows +entity.glowing = true; console.log(entity.glowing); ``` ### entity.nameTag -⬆ MC Extension | Get/set the entity's custom name (displayed above head). +⬆ MC extension | Gets/sets the entity's custom display name (supports color codes). Empty string = no name. ```js -entity.nameTag = "§cBoss Monster"; -console.log(entity.nameTag); +entity.nameTag = "§cBoss Mob"; +console.log(entity.nameTag); // Property access +entity.setNameTag("§eGuard"); // Method access ``` ## Tag System -All ✅ Box3 API. Tags are string markers attached to entities for classification and querying. +All ✅ Box3 API. Tags are string markers attached to entities (backed by Minecraft scoreboard tags), used for classification and queries. ### entity.addTag(tag) -Add a tag. +Adds a tag. ### entity.hasTag(tag) -Check if the entity has the given tag. +Checks whether the entity has the specified tag. ### entity.removeTag(tag) -Remove a tag. +Removes a tag. + +### entity.tags() + +Returns all tags as a string array. ```js entity.addTag("boss"); @@ -160,6 +226,12 @@ if (entity.hasTag("boss")) { entity.maxHp = 200; } +// Get all tags +var allTags = entity.tags(); +for (var i = 0; i < allTags.length; i++) { + console.log(allTags[i]); +} + // Query by tag var bosses = world.querySelectorAll(".boss"); ``` @@ -168,61 +240,63 @@ var bosses = world.querySelectorAll(".boss"); ### entity.setFire(ticks) -⬆ MC Extension | Set the entity on fire for the given number of ticks. 20 ticks = 1 second. +⬆ MC extension | Sets the entity on fire for the given number of ticks. 20 ticks = 1 second. ### entity.clearFire() -⬆ MC Extension | Extinguish the entity's fire. +⬆ MC extension | Extinguishes any fire on the entity. ```js -entity.setFire(100); // ignite for 5 seconds -entity.clearFire(); // extinguish immediately +entity.setFire(100); // Ignite for 5 seconds +entity.clearFire(); // Extinguish immediately ``` ## AI & Navigation ### entity.setAI(enabled) -⬆ MC Extension | Enable/disable entity AI (Mob only). When disabled, the entity won't move or attack. +⬆ MC extension | Enables/disables the entity's AI (Mob only). When disabled, the entity won't move or attack. ```js -entity.setAI(false); // freeze entity +entity.setAI(false); // Freeze entity ``` -### entity.setTarget(entity) +### entity.setTarget(target) -⬆ MC Extension | Set the mob's attack target (Mob only). +⬆ MC extension | Sets the mob's attack target (Mob only). The mob will pathfind to and attack it. ### entity.getTarget() -⬆ MC Extension | Get the current attack target, returns `Box3JSEntity` or null. +⬆ MC extension | Returns the current attack target, or `null`. ### entity.clearTarget() -⬆ MC Extension | Clear the attack target. +⬆ MC extension | Clears the attack target, stopping pursuit. ```js var boss = world.spawnEntity("minecraft:skeleton", new GameVector3(0, 100, 0)); var target = world.querySelectorAll("*")[0]; boss.setTarget(target); +// ... +boss.clearTarget(); ``` ### entity.navigateTo(x, y, z, speed) -⬆ MC Extension | Make the entity pathfind to the target coordinates (PathfinderMob only). +⬆ MC extension | Orders a pathfinder mob to navigate to the given coordinates. Returns `true` if path calculation succeeded. ### entity.navigateTo(pos, speed) ⬆ GameVector3 overload. ```js -entity.navigateTo(10, 100, 10, 1.0); // walk to target at speed 1.0 +entity.navigateTo(10, 100, 10, 1.0); entity.navigateTo(target.position, 1.0); ``` ### entity.lookAt(x, y, z) -⬆ MC Extension | Make the entity face the target coordinates. +⬆ MC extension | Makes the entity look at the given coordinates. ### entity.lookAt(pos) @@ -233,22 +307,22 @@ entity.lookAt(0, 100, -10); entity.lookAt(target.position); ``` -## Potion Effects +## Status Effects -All ⬆ MC Extension. +All ⬆ MC extension. ### entity.addEffect(effectId, duration, amplifier) -Add a potion effect. `duration` in ticks, `amplifier` starts at 0. +Applies a status effect. `duration` in ticks (20 ticks = 1 second), `amplifier` starts at 0 (0 = level I). ### entity.addEffect(effectId, duration, amplifier, hideParticles) -Add effect with optional particle hiding. +Applies an effect, optionally hiding particles. ```js -entity.addEffect("minecraft:speed", 600, 2); // Speed III, 30 seconds -entity.addEffect("minecraft:strength", 99999, 1, true); // Permanent Strength II, no particles -entity.addEffect("minecraft:glowing", 200, 0); // Glowing 10 seconds +entity.addEffect("minecraft:speed", 600, 2); // Speed III, 30 seconds +entity.addEffect("minecraft:strength", 99999, 1, true); // Permanent Strength II, no particles +entity.addEffect("minecraft:glowing", 200, 0); // Glowing 10 seconds // Common effects: // minecraft:speed, minecraft:slowness, minecraft:strength @@ -259,13 +333,20 @@ entity.addEffect("minecraft:glowing", 200, 0); // Glowing 10 seconds ## Equipment -All ⬆ MC Extension. +All ⬆ MC extension. ### entity.setEquipment(slot, itemId) -Equip a mob with gear. +Equips an item onto a mob. **Slot values:** -**Slot values:** `"mainhand"`, `"offhand"`, `"head"` (helmet), `"chest"` (chestplate), `"legs"` (leggings), `"feet"` (boots) +| slot | Description | +|------|-------------| +| `"mainhand"` | Main hand | +| `"offhand"` | Off hand | +| `"head"`, `"helmet"`, `"helm"` | Helmet | +| `"chest"`, `"chestplate"` | Chestplate | +| `"legs"`, `"leggings"` | Leggings | +| `"feet"`, `"boots"` | Boots | ```js entity.setEquipment("mainhand", "minecraft:diamond_sword"); @@ -276,24 +357,24 @@ entity.setEquipment("feet", "minecraft:leather_boots"); ### entity.setDropChance(slot, chance) -Set the drop probability for equipment in a slot, 0.0–1.0. Use `slot = "all"` to set all slots at once. +Sets the drop chance for an equipment slot, range 0.0–1.0. Use `"all"` for `slot` to set all slots at once (both hands + four armor slots). ```js -entity.setDropChance("mainhand", 0.5); // 50% chance to drop mainhand item -entity.setDropChance("all", 0); // drop nothing +entity.setDropChance("mainhand", 0.5); // 50% chance to drop main hand item +entity.setDropChance("all", 0); // Drop nothing ``` ## Attributes -All ⬆ MC Extension. +All ⬆ MC extension. ### entity.getAttribute(attributeId) -Get the current value of an entity attribute. +Returns the current attribute value. Returns 0 for non-LivingEntity. ### entity.setAttribute(attributeId, value) -Set the base value of an entity attribute. +Sets the base attribute value. Only works on LivingEntity. ```js var attack = entity.getAttribute("minecraft:generic.attack_damage"); @@ -304,44 +385,45 @@ entity.setAttribute("minecraft:generic.knockback_resistance", 1.0); entity.setAttribute("minecraft:generic.armor", 10); ``` -> Note: Box3 convenience properties like `maxHp` / `walkSpeed` / `jumpPower` use these attributes internally. Prefer convenience properties; use `setAttribute` only for attributes without dedicated wrappers. +> 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. ## Lifecycle ### entity.destroy() -✅ Box3 API | Destroy the entity. Fires the `onDestroy` callback if set. - -### entity.remove() - -⬆ MC Extension | Directly remove the entity, **without** firing the `onDestroy` callback. +✅ Box3 API | Destroys the entity. If a callback was registered via `setOnDestroy()`, it will be invoked. ### entity.setOnDestroy(handler) -✅ Box3 API | Set a destroy callback. `handler` receives one argument `(entity)`. - -### entity.destroyed +✅ Box3 API | Registers a callback called when the entity is destroyed. `handler` receives one argument `(entity)`. -✅ Box3 API | Read-only. Whether the entity has been removed. +```js +entity.setOnDestroy(function(e) { + console.log("Entity " + e.id + " destroyed"); +}); +``` ### entity.setPersistent(v) -⬆ MC Extension | When `true`, the mob won't despawn when far from players (Mob only). +⬆ MC extension | When `true`, prevents the mob from despawning naturally (Mob only). Write-only method, no getter. ```js var boss = world.spawnEntity( "minecraft:wither_skeleton", new GameVector3(0, 100, 0), ); -boss.setPersistent(true); // won't despawn -boss.setOnDestroy((e) => { +boss.setPersistent(true); // Won't despawn +boss.setNameTag("§c§lWither Guard"); +boss.setOnDestroy(function(e) { world.say("Boss defeated!"); }); ``` ## Custom Properties -✅ Box3 API | You can store arbitrary JS data directly on an entity. The data lives as long as the entity. +✅ Box3 API | You can store arbitrary JS data directly on the entity. The data lives as long as the entity exists. + +Custom properties are stored under the entity's UUID in a `ConcurrentHashMap` and persist until the entity is removed. ```js entity.myCustomField = "hello"; diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math.md b/Box3JS-NeoForge-1.21.1/docs/api/math.md index 82c65e1..05ac38c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math.md @@ -4,179 +4,376 @@ ## GameVector3 -三维向量,用于位置、方向、速度等。 +三维向量,用于位置、方向、速度等。所有分量使用 `double` 精度。 ### 构造 ```js -var v = new GameVector3(0, 100, 0); // x, y, z +var v = new GameVector3(); // 零向量 (0, 0, 0) +var v = new GameVector3(x, y, z); // 指定坐标 ``` ### 属性 -```js -v.x = 10; // 读写 -v.y = 20; -v.z = 30; -``` - -### 方法 - -| 方法 | 返回值 | 说明 | -| ---------------- | ------------- | ------------------ | -| `v.set(x, y, z)` | `GameVector3` | 设置分量,返回自身 | -| `v.add(w)` | `GameVector3` | 加法,返回新向量 | -| `v.sub(w)` | `GameVector3` | 减法 | -| `v.scale(s)` | `GameVector3` | 标量乘 | -| `v.dot(w)` | `number` | 点积 | -| `v.mag()` | `number` | 向量长度 | -| `v.sqrMag()` | `number` | 长度平方(更快) | -| `v.normalize()` | `GameVector3` | 归一化,返回新向量 | -| `v.distance(w)` | `number` | 两点距离 | -| `v.lerp(w, t)` | `GameVector3` | 线性插值,t 0–1 | -| `v.equals(w)` | `boolean` | 分量相等比较 | - -### 静态方法 - -```js -var v = GameVector3.fromPolar(mag, phi, theta); // 球坐标 → 向量 -``` +| 属性 | 类型 | 说明 | +|------|------|------| +| `v.x` | `number` | X 分量 (东西方向),可读写 | +| `v.y` | `number` | Y 分量 (上下方向),可读写 | +| `v.z` | `number` | 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.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.dot(w)` | `number` | 点积 (内积):`v · w` | +| `v.mag()` | `number` | 向量长度 (模) | +| `v.sqrMag()` | `number` | 长度平方,比 `mag()` 更快 | +| `v.distance(w)` | `number` | 与 `w` 的欧几里得距离 | +| `v.angle(w)` | `number` | 与 `w` 的夹角 (弧度, 0–π) | + +#### 比较 + +| 方法 | 返回值 | 说明 | +|------|--------|------| +| `v.equals(w)` | `boolean` | 近似相等,容差 1e-6 | +| `v.exactEquals(w)` | `boolean` | 精确相等,分量完全一致 | ```js var pos = new GameVector3(0, 100, 0); var target = new GameVector3(10, 100, 10); -// 计算两点距离 +// 计算距离 var dist = pos.distance(target); // ~14.14 // 方向向量 var dir = target.sub(pos).normalize(); -// 传送 +// 夹角 +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 + +// 传送实体 (LiveVec3) entity.position.set(0, 100, 0); ``` +### 静态方法 + +```js +// 球坐标 → 向量 +var v = GameVector3.fromPolar(mag, phi, theta); +// mag: 半径 +// phi: 方位角 (弧度, 绕 Y 轴水平旋转) +// theta: 仰角 (弧度, 从水平面起算) +``` + +### toString + +```js +var v = new GameVector3(1, 2, 3); +v.toString(); // "GameVector3(1.0, 2.0, 3.0)" +``` + +--- + ## GameBounds3 -轴对齐包围盒(AABB)。 +轴对齐包围盒 (AABB),由两个对角顶点 `lo` (最小角) 和 `hi` (最大角) 定义。 ### 构造 ```js var bounds = new GameBounds3( - new GameVector3(-1, 0, -1), // 下界 (lo) - new GameVector3(1, 2, 1), // 上界 (hi) + new GameVector3(-1, 0, -1), // lo (最小角) + new GameVector3(1, 2, 1), // hi (最大角) ); ``` -### 方法 +### 属性 -| 方法 | 返回值 | 说明 | -| -------------------------- | --------- | -------------------- | -| `bounds.intersects(other)` | `boolean` | 与另一包围盒是否相交 | -| `bounds.contains(point)` | `boolean` | 点是否在包围盒内 | +| 属性 | 类型 | 说明 | +|------|------|------| +| `bounds.lo` | `GameVector3` | 最小角 (三个分量均为最小值),可读写 | +| `bounds.hi` | `GameVector3` | 最大角 (三个分量均为最大值),可读写 | -## GameRGBColor +### 实例方法 -RGB 颜色,分量范围 0.0–1.0。 +| 方法 | 返回值 | 说明 | +|------|--------|------| +| `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` | -### 构造 +### 静态方法 ```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); +// 从 GameVector3 数组创建最小包围盒 +var points = [new GameVector3(0,0,0), new GameVector3(5,10,3)]; +var box = GameBounds3.fromPoints(points); // 返回 GameBounds3 或 null ``` -### 属性 +### toString + +```js +bounds.toString(); // "GameBounds3(GameVector3(-1.0, 0.0, -1.0), GameVector3(1.0, 2.0, 1.0))" +``` ```js -color.r = 0.5; // 读写 -color.g = 0.8; -color.b = 0.2; +// 查询区域内实体 +var entities = world.searchBox(bounds); + +// 检测点是否在区域内 +if (bounds.contains(player.position)) { + // 玩家在区域内 +} ``` -### 方法 +--- -| 方法 | 返回值 | 说明 | -| -------------- | -------------- | -------- | -| `c.lerp(d, t)` | `GameRGBColor` | 线性插值 | +## GameRGBColor -### 静态方法 +RGB 颜色,三个通道范围 0.0–1.0。 + +### 构造 ```js -var randomColor = GameRGBColor.random(); // 随机颜色 +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); ``` -## GameRGBAColor +### 属性 -带 Alpha 通道的颜色,分量范围 0.0–1.0。 +| 属性 | 类型 | 说明 | +|------|------|------| +| `color.r` | `number` | 红色通道 (0–1),可读写 | +| `color.g` | `number` | 绿色通道 (0–1),可读写 | +| `color.b` | `number` | 蓝色通道 (0–1),可读写 | + +### 实例方法 + +#### 原地修改 (返回 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.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.equals(o)` | `boolean` | 近似相等,容差 1e-6 | +| `c.toRGBA()` | `string` | 转为 CSS 格式:`"rgba(r,g,b,1.0)"` | -### 构造 +### 静态方法 ```js -var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +var randomColor = GameRGBColor.random(); // 每个通道 0–1 随机值 ``` -### 方法 +### toString ```js -var a = new GameRGBAColor(1, 0, 0, 1); -var b = new GameRGBAColor(0, 1, 0, 0.5); +new GameRGBColor(1, 0.5, 0).toString(); // "GameRGBColor(1.0, 0.5, 0.0)" +``` -var c = a.add(b); // 分量加法 -var d = a.sub(b); // 分量减法 -var e = a.mul(b); // 分量乘法 -var f = a.div(b); // 分量除法 +--- -a.addEq(b); // 原地加法 (a += b) -a.subEq(b); // 原地减法 -a.mulEq(b); // 原地乘法 -a.divEq(b); // 原地除法 +## GameRGBAColor -a.blendEq(b); // 混合 +带 Alpha 通道的颜色,四个分量范围 0.0–1.0。 -a.set(0.5, 0.5, 0.5, 1); // 设置分量 -var result = new GameRGBAColor(0, 0, 0, 0); -result.copy(a); // 浅拷贝 a -var clone = a.clone(); // 深拷贝 +### 构造 -var lerped = a.lerp(b, 0.5); // 插值 -var eq = a.equals(b); // 比较 +```js +var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +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.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.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.equals(o)` | `boolean` | 近似相等,容差 1e-6 | +| `c.blendEq(rgb)` | `GameRGBColor` | Alpha 混合到 RGB 背景上,返回最终 RGB | + +### toString + +```js +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 颜色 +``` + +--- + ## GameQuaternion -四元数,用于 3D 旋转。 +四元数,用于 3D 旋转。单位四元数 (模长=1) 表示纯旋转。 ### 构造 ```js -var q = new GameQuaternion(0, 0, 0, 1); // 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.set(w, x, y, z)` | 设置分量 | -| `q.copy(other)` | 浅拷贝 | -| `q.clone()` | 深拷贝 | -| `q.add(p)` / `q.sub(p)` / `q.mul(p)` / `q.div(p)` | 算术 | -| `q.inv()` | 逆四元数 | -| `q.dot(p)` | 点积 | -| `q.mag()` / `q.sqrMag()` | 模长 | -| `q.normalize()` | 归一化 | -| `q.slerp(p, t)` | 球面线性插值 | -| `q.angle(p)` | 与另一四元数的夹角 (弧度) | -| `q.getAxisAngle()` | 获取旋转轴和角度 | -| `q.rotateX(a)` / `q.rotateY(a)` / `q.rotateZ(a)` | 绕轴旋转 | -| `q.equals(p)` | 比较 | +### 实例方法 + +#### 原地修改 (返回 this) + +| 方法 | 返回值 | 说明 | +|------|--------|------| +| `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.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.rotateX(rad)` | `GameQuaternion` | 绕 X 轴旋转 | +| `q.rotateY(rad)` | `GameQuaternion` | 绕 Y 轴旋转 | +| `q.rotateZ(rad)` | `GameQuaternion` | 绕 Z 轴旋转 | + +#### 轴角分解 + +```js +var result = q.getAxisAngle(); +// result.angle — 旋转角度 (弧度) +// result.axis — 旋转轴 (单位 GameVector3) +``` ### 静态方法 ```js -var q1 = GameQuaternion.fromAxisAngle(axis, angle); +// 从轴-角表示创建 +var q1 = GameQuaternion.fromAxisAngle(axis, rad); +// axis: GameVector3 (自动归一化) +// rad: 旋转角度 (弧度) + +// 从欧拉角创建 (YZX 旋转顺序: Y → Z → X) var q2 = GameQuaternion.fromEuler(x, y, z); +// x, y, z: 绕各轴旋转的弧度 + +// 从向量 a 旋转到向量 b 的最短弧 var q3 = GameQuaternion.rotationBetween(fromVec, toVec); ``` + +### toString + +```js +q.toString(); // "GameQuaternion(0.707, 0.0, 0.707, 0.0)" +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md index a0cc04e..f3c1431 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/math_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/math_en.md @@ -1,182 +1,384 @@ # Math Types -All ✅ Box3 API. The following data types are available globally in JS. +All ✅ Box3 API. The following data types are globally available in JS. ## GameVector3 -3D vector for positions, directions, velocities, etc. +A 3D vector with double-precision components. Used for position, direction, velocity, etc. -### Construction +### Constructor ```js -var v = new GameVector3(0, 100, 0); // x, y, z +var v = new GameVector3(); // Zero vector (0, 0, 0) +var v = new GameVector3(x, y, z); // Specified coordinates ``` ### Properties -```js -v.x = 10; // read/write -v.y = 20; -v.z = 30; -``` - -### Methods - -| Method | Returns | Description | -| ---------------- | ------------- | ----------------------------- | -| `v.set(x, y, z)` | `GameVector3` | Set components, returns self | -| `v.add(w)` | `GameVector3` | Addition, returns new vector | -| `v.sub(w)` | `GameVector3` | Subtraction | -| `v.scale(s)` | `GameVector3` | Scalar multiplication | -| `v.dot(w)` | `number` | Dot product | -| `v.mag()` | `number` | Vector magnitude | -| `v.sqrMag()` | `number` | Squared magnitude (faster) | -| `v.normalize()` | `GameVector3` | Normalize, returns new vector | -| `v.distance(w)` | `number` | Distance between two points | -| `v.lerp(w, t)` | `GameVector3` | Linear interpolation, t 0–1 | -| `v.equals(w)` | `boolean` | Component-wise equality | - -### Static Methods - -```js -var v = GameVector3.fromPolar(mag, phi, theta); // spherical → vector -``` +| 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 | + +#### 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 | + +#### 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–π) | + +#### Comparison + +| Method | Returns | Description | +|--------|---------|-------------| +| `v.equals(w)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `v.exactEquals(w)` | `boolean` | Exact equality — components strictly equal | ```js var pos = new GameVector3(0, 100, 0); var target = new GameVector3(10, 100, 10); -// Distance between two points +// Distance var dist = pos.distance(target); // ~14.14 // Direction vector var dir = target.sub(pos).normalize(); -// Teleport +// 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). +Axis-aligned bounding box (AABB), defined by two opposing corners: `lo` (minimum corner) and `hi` (maximum corner). -### Construction +### Constructor ```js var bounds = new GameBounds3( - new GameVector3(-1, 0, -1), // lower bound (lo) - new GameVector3(1, 2, 1), // upper bound (hi) + new GameVector3(-1, 0, -1), // lo (min corner) + new GameVector3(1, 2, 1), // hi (max corner) ); ``` -### Methods +### Properties -| Method | Returns | Description | -| -------------------------- | --------- | -------------------------------------------- | -| `bounds.intersects(other)` | `boolean` | Whether it intersects another bounding box | -| `bounds.contains(point)` | `boolean` | Whether the point is inside the bounding box | +| Property | Type | Description | +|----------|------|-------------| +| `bounds.lo` | `GameVector3` | Minimum corner, read/write | +| `bounds.hi` | `GameVector3` | Maximum corner, read/write | -## GameRGBColor +### Instance Methods -RGB color, components range 0.0–1.0. +| 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` | -### Construction +### Static Methods ```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); +// 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 ``` -### Properties +### toString + +```js +bounds.toString(); // "GameBounds3(GameVector3(-1.0, 0.0, -1.0), GameVector3(1.0, 2.0, 1.0))" +``` ```js -color.r = 0.5; // read/write -color.g = 0.8; -color.b = 0.2; +// Query entities within bounds +var entities = world.searchBox(bounds); + +// Check if point is inside +if (bounds.contains(player.position)) { + // Player is inside the area +} ``` -### Methods +--- -| Method | Returns | Description | -| -------------- | -------------- | -------------------- | -| `c.lerp(d, t)` | `GameRGBColor` | Linear interpolation | +## GameRGBColor -### Static Methods +An RGB color with three channels ranging from 0.0 to 1.0. + +### Constructor ```js -var randomColor = GameRGBColor.random(); // random color +var red = new GameRGBColor(1, 0, 0); +var blue = new GameRGBColor(0, 0, 1); +var gray = new GameRGBColor(0.5, 0.5, 0.5); ``` -## GameRGBAColor +### Properties -Color with alpha channel, components range 0.0–1.0. +| 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 | + +#### 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.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `c.toRGBA()` | `string` | CSS format string: `"rgba(r,g,b,1.0)"` | -### Construction +### Static Methods ```js -var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +var randomColor = GameRGBColor.random(); // Each channel 0–1 random ``` -### Methods +### toString ```js -var a = new GameRGBAColor(1, 0, 0, 1); -var b = new GameRGBAColor(0, 1, 0, 0.5); +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. -var c = a.add(b); // component-wise addition -var d = a.sub(b); // component-wise subtraction -var e = a.mul(b); // component-wise multiplication -var f = a.div(b); // component-wise division +### Constructor + +```js +var semiRed = new GameRGBAColor(1, 0, 0, 0.5); +var opaque = new GameRGBAColor(0, 1, 0, 1.0); +``` -a.addEq(b); // in-place addition (a += b) -a.subEq(b); // in-place subtraction -a.mulEq(b); // in-place multiplication -a.divEq(b); // in-place division +### Properties -a.blendEq(b); // blend +| 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 | + +#### 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.equals(o)` | `boolean` | Approximate equality, tolerance 1e-6 | +| `c.blendEq(rgb)` | `GameRGBColor` | Alpha-blend onto an RGB background, returns displayed RGB | + +### toString -a.set(0.5, 0.5, 0.5, 1); // set components -var result = new GameRGBAColor(0, 0, 0, 0); -result.copy(a); // shallow copy from a -var clone = a.clone(); // deep copy +```js +new GameRGBAColor(1, 0, 0, 0.5).toString(); // "GameRGBAColor(1.0, 0.0, 0.0, 0.5)" +``` -var lerped = a.lerp(b, 0.5); // interpolation -var eq = a.equals(b); // comparison +```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 -Quaternion for 3D rotations. +A quaternion used for 3D rotation. Unit quaternions (magnitude=1) represent pure rotations. -### Construction +### Constructor ```js -var q = new GameQuaternion(0, 0, 0, 1); // w, x, y, z +var q = new GameQuaternion(); // Identity (1, 0, 0, 0) +var q = new GameQuaternion(w, x, y, z); // Specified components ``` -### Methods +### 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 | Description | -| ------------------------------------------------- | ------------------------------------- | -| `q.set(w, x, y, z)` | Set components | -| `q.copy(other)` | Shallow copy | -| `q.clone()` | Deep copy | -| `q.add(p)` / `q.sub(p)` / `q.mul(p)` / `q.div(p)` | Arithmetic | -| `q.inv()` | Inverse quaternion | -| `q.dot(p)` | Dot product | -| `q.mag()` / `q.sqrMag()` | Magnitude | -| `q.normalize()` | Normalize | -| `q.slerp(p, t)` | Spherical linear interpolation | -| `q.angle(p)` | Angle to another quaternion (radians) | -| `q.getAxisAngle()` | Get rotation axis and angle | -| `q.rotateX(a)` / `q.rotateY(a)` / `q.rotateZ(a)` | Rotate around axis | -| `q.equals(p)` | Comparison | +| 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 | + +#### Axis-Angle Decomposition + +```js +var result = q.getAxisAngle(); +// result.angle — rotation angle (radians) +// result.axis — rotation axis (unit GameVector3) +``` ### Static Methods ```js -var q1 = GameQuaternion.fromAxisAngle(axis, angle); +// 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); ``` + +### 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 6dd3741..5385365 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player.md @@ -1,10 +1,10 @@ # player — 玩家 API -`player` 对象通过 `entity.player` 获取,代表登录的玩家。它包含 `entity` 的全部能力,并额外提供玩家专属功能:背包、经验、飞行、消息、传送等。 +`player` 对象通过 `entity.player` 获取,代表登录的玩家。它拥有 `entity` 的全部属性和方法(如 `hp`、`position`、`tags()` 等),并额外提供玩家专属功能:背包、经验、飞行、消息、传送等。 ```js -world.onPlayerJoin((entity) => { - var p = entity.player; // p 即为 player 对象 +world.onPlayerJoin(function(entity, tick) { + var p = entity.player; // p 即为 player 对象 p.directMessage("欢迎回来, " + p.name + "!"); }); ``` @@ -17,19 +17,29 @@ world.onPlayerJoin((entity) => { ### player.userId -✅ Box3 API | 只读。玩家 UUID 字符串。 +✅ Box3 API | 只读。玩家 UUID 字符串(与 `entity.id` 相同)。 -### player.getOpLevel() +### player.opLevel -⬆ MC 扩展 | 获取/设置玩家管理员权限等级 (0-4)。0=普通玩家, 1=可绕过出生点保护, 2=可使用大部分命令, 3=可管理玩家, 4=最高权限。 +✅ Box3 API | 获取/设置玩家管理员权限等级 (0–4)。 + +| 等级 | 说明 | +|------|------| +| 0 | 普通玩家 | +| 1 | 可绕过出生点保护 | +| 2 | 可使用大部分命令 | +| 3 | 可管理玩家 | +| 4 | 最高权限 (等同于 `/op`) | ```js -if (player.getOpLevel() >= 2) { +if (player.opLevel >= 2) { // 需要权限等级 2 的操作 } -player.opLevel = 3; // 设置为 3 级权限 +player.opLevel = 3; // 属性方式设置为 3 级 ``` +另有 `player.getOpLevel()` 方法返回权限等级数字。 + ## 外观 ### player.invisible @@ -38,10 +48,11 @@ player.opLevel = 3; // 设置为 3 级权限 ### player.scale -✅ Box3 API | 只读。玩家缩放值。 +✅ Box3 API | 只读。玩家模型缩放比例(MC 原生 scale,非 Box3 scale)。 ```js -player.invisible = true; // 隐形 +player.invisible = true; // 隐形 +console.log("玩家缩放: " + player.scale); ``` ## 移动 @@ -50,29 +61,66 @@ player.invisible = true; // 隐形 ### player.walkSpeed -步行速度,对应 `MOVEMENT_SPEED` 属性。默认值约 0.1。 +步行速度,对应 `MOVEMENT_SPEED` 属性基值。默认值约 0.1。 ### player.runSpeed -奔跑速度。`walkSpeed × 1.3` 的关系自动保持。 +奔跑速度。get 返回 `walkSpeed × 1.3`,set 自动反算 `walkSpeed`,保持 1.3 倍比例关系。 ### player.jumpPower -跳跃力度,对应 `JUMP_STRENGTH` 属性。 +跳跃力度,对应 `JUMP_STRENGTH` 属性基值。默认值约 0.42。 + +### player.enableJump + +获取/设置是否允许跳跃。默认 `true`。设为 `false` 时保存当前跳跃力并将 `JUMP_STRENGTH` 设为 0;设回 `true` 时恢复。 + +```js +player.enableJump = false; // 禁止跳跃 +player.enableJump = true; // 恢复跳跃 +``` + +### player.crouchSpeed + +获取/设置潜行速度(自定义属性,默认 `0.0`)。MC 无独立潜行速度属性,脚本可读取此值自行实现。 + +### player.swimSpeed + +获取/设置游泳速度。底层映射到 `WATER_MOVEMENT_EFFICIENCY` 属性。 + +```js +player.swimSpeed = 0.5; // 游泳更快 +``` ### player.moveState -只读。当前移动状态:`"FLYING"`、`"SWIM"`、`"JUMP"`、`"FALL"`、`"GROUND"`。 +只读。当前移动状态字符串: + +| 值 | 说明 | +|------|------| +| `"FLYING"` | 正在飞行 | +| `"SWIM"` | 在水中 | +| `"JUMP"` | 向上跳跃 | +| `"FALL"` | 下落中 | +| `"GROUND"` | 在地面上 | ### player.walkState -只读。当前行走状态:`"CROUCH"`、`"RUN"`、`"WALK"`、`"NONE"`。 +只读。当前行走状态字符串: + +| 值 | 说明 | +|------|------| +| `"CROUCH"` | 潜行中 | +| `"RUN"` | 奔跑中 | +| `"WALK"` | 行走中 | +| `"NONE"` | 静止 | ```js -player.walkSpeed = 0.2; // 加速 -player.jumpPower = 0.6; // 跳更高 +player.walkSpeed = 0.2; // 加速 +player.jumpPower = 0.6; // 跳更高 +player.swimSpeed = 0.3; // 游泳速度 -world.onTick(() => { +world.onTick(function() { if (player.walkState === "RUN") { // 玩家在奔跑 } @@ -115,11 +163,11 @@ player.disableFly = true; ### player.collision -⬆ MC 扩展 | 获取/设置团队内碰撞。设为 `false` 可防止多人推搡。底层修改团队的 `CollisionRule`。 +⬆ MC 扩展 | 获取/设置团队内碰撞。设为 `false` 可防止多人推搡。底层修改玩家所在队伍的 `CollisionRule`(ALWAYS / NEVER)。 ```js -player.collision = false; // 禁用碰撞 -console.log(player.collision); // false +player.collision = false; // 禁用碰撞 +console.log(player.collision); ``` ## 生命值 @@ -134,14 +182,22 @@ console.log(player.collision); // false 获取/设置最大生命值。 +### player.dead + +只读。玩家是否已死亡或正在死亡中(`isDeadOrDying()`)。 + ```js // 设置职业血量 -player.maxHp = 40; // 战士 40 HP -player.hp = 40; // 满血 +player.maxHp = 40; // 战士 40 HP +player.hp = 40; // 满血 // 设置后若当前血量超过新最大值会自动截断 player.maxHp = 20; // player.hp 自动降到 20 封顶 + +if (player.dead) { + console.log("玩家已死亡"); +} ``` ## 游戏模式 @@ -151,10 +207,10 @@ player.maxHp = 20; ✅ Box3 API | 获取/设置游戏模式。get 返回名称字符串,set 接受字符串或数字。 ```js -player.gameMode = "creative"; // 创造模式 -player.gameMode = "survival"; // 生存模式 -player.gameMode = "adventure"; // 冒险模式 -player.gameMode = "spectator"; // 旁观模式 +player.gameMode = "creative"; // 创造模式 +player.gameMode = "survival"; // 生存模式 +player.gameMode = "adventure"; // 冒险模式 +player.gameMode = "spectator"; // 旁观模式 // 或数字: 0=生存, 1=创造, 2=冒险, 3=旁观 ``` @@ -164,19 +220,19 @@ player.gameMode = "spectator"; // 旁观模式 ### player.cameraMode -获取/设置相机模式:`"FPS"`(第一人称)或 `"FOLLOW"`(跟随实体)。 +获取/设置相机模式:`"FPS"`(第一人称)或 `"FOLLOW"`(跟随实体)。设为 `"FPS"` 时自动清除跟随目标。 ### player.cameraEntity -设置或获取跟随的实体对象(`Box3JSEntity`)。 +设置或获取跟随的实体对象(`GameEntity`)。设为实体时相机模式自动切换为 `"FOLLOW"`,设为 `null` 时切换回 `"FPS"`。 ### player.cameraPitch / player.cameraYaw -相机的俯仰角和偏航角。 +相机的俯仰角(pitch)和偏航角(yaw)。注意:MC 中 yaw 对应 Y 旋转角 (xRot),pitch 对应 X 旋转角 (yRot)。 ### player.facingDirection -只读 `GameVector3`。玩家视线方向单位向量。 +只读 `GameVector3`。玩家视线方向的单位向量。 ### player.cameraTarget @@ -194,7 +250,7 @@ player.gameMode = "spectator"; // 旁观模式 player.lookAt(10, 100, 10); player.lookAt(target.position); -// 获取视线方向 +// 获取视线信息 var dir = player.facingDirection; var target = player.cameraTarget; ``` @@ -205,9 +261,23 @@ var target = player.cameraTarget; ✅ Box3 API | 传送玩家到指定 `GameVector3` 坐标。 +### player.spawnPoint + +✅ Box3 API | 获取/设置玩家的重生点坐标 (`GameVector3`)。读取时若玩家未设置重生点,返回世界出生点。 + +```js +// 属性方式设置 +player.spawnPoint = new GameVector3(0, 100, 0); +console.log(player.spawnPoint); +``` + ### player.setRespawnPoint(pos) -✅ Box3 API | 设置玩家的重生点。 +✅ Box3 API | 设置玩家重生点(方法方式,与 `spawnPoint` 属性等价)。 + +### player.setSpawnPoint(pos) + +✅ Box3 API | 同 `setRespawnPoint`,Box3 标准命名。 ### player.respawn() @@ -234,7 +304,7 @@ player.teleport(new GameVector3(0, 70, 0)); ### player.kick(reason) -✅ Box3 API | 踢出玩家,自定义原因。 +✅ Box3 API | 踢出玩家,自定义踢出原因。 ```js player.kick("你已被移出游戏"); @@ -244,7 +314,7 @@ player.kick("你已被移出游戏"); ### player.directMessage(msg) -✅ Box3 API | 向玩家发送聊天栏消息。 +✅ Box3 API | 向玩家发送聊天栏消息(仅该玩家可见的系统消息)。 ### player.actionBar(msg) @@ -252,11 +322,11 @@ player.kick("你已被移出游戏"); ### player.title(title, subtitle) -✅ Box3 API | 向玩家发送屏幕标题。使用默认动画参数。 +✅ Box3 API | 向玩家发送屏幕标题。使用默认动画参数:淡入 10 tick、停留 70 tick、淡出 20 tick。 ### player.title(title, subtitle, fadeIn, stay, fadeOut) -⬆ MC 扩展 | 完全参数的标题。`fadeIn`/`stay`/`fadeOut` 单位均为 tick。 +⬆ MC 扩展 | 完全参数的标题。`fadeIn`/`stay`/`fadeOut` 单位均为 tick (20 tick = 1秒)。 ### player.dialog(config) @@ -272,11 +342,11 @@ player.directMessage("你选择了: " + result.value); ### player.link(href) -✅ Box3 API | 向玩家发送可点击链接。 +✅ Box3 API | 向玩家发送可点击的 URL 链接(蓝色下划线)。 ### player.onChat(handler) -✅ Box3 API | 为单个玩家注册聊天回调(更精细的控制,常用于对话树)。 +✅ Box3 API | 为单个玩家注册聊天回调(比全局 `world.onChat` 更精细的控制,常用于对话树)。 ```js player.directMessage("你好!"); @@ -286,7 +356,7 @@ player.link("https://example.com"); // 对话树 player.directMessage("输入你的选择: A 或 B"); -player.onChat((entity, msg, tick) => { +player.onChat(function(entity, msg, tick) { if (msg === "A") { player.directMessage("你选择了 A"); } @@ -312,8 +382,8 @@ player.onChat((entity, msg, tick) => { ⬆ MC 扩展 | 获取/设置饱和度(0–20,浮点数)。 ```js -player.xp = 10; // 设置 10 级 -player.addExperienceLevels(3); // 加 3 级 +player.xp = 10; // 设置 10 级 +player.addExperienceLevels(3); // 加 3 级 player.food = 20; player.saturation = 10; ``` @@ -326,30 +396,15 @@ player.saturation = 10; 给予物品。 -### player.clearInventory() - -清空背包。 - -### player.getHeldItem() +### player.giveEnchantedItem(itemId, count, enchants) -获取主手物品,返回 `{id, count}`。空手返回 `{id: "minecraft:air", count: 0}`。 +给予附魔物品。`enchants` 是 `{附魔ID: 等级}` 对象。 ```js player.giveItem("minecraft:diamond_sword", 1); player.giveItem("minecraft:golden_apple", 5); player.giveItem("minecraft:arrow", 64); -var held = player.getHeldItem(); -console.log(held.id, held.count); // "minecraft:diamond_sword" 1 - -player.clearInventory(); -``` - -### player.giveEnchantedItem(itemId, count, enchants) - -给予附魔物品。`enchants` 是 `{附魔ID: 等级}` 对象。 - -```js player.giveEnchantedItem("minecraft:diamond_sword", 1, { "minecraft:sharpness": 5, "minecraft:fire_aspect": 2, @@ -363,9 +418,22 @@ player.giveEnchantedItem("minecraft:bow", 1, { }); ``` +### player.giveCustomItem(id, count) + +⬆ MC 扩展 | 给予通过 `world.loadCustomItems()` 加载的自定义物品。物品以 `minecraft:paper` 为载体,通过 DataComponents 获得名称、贴图、食物等属性。 + +```js +// 先加载配置 +world.loadCustomItems("box3js-items"); +// 再给予物品 +player.giveCustomItem("arena_trophy", 1); +player.giveCustomItem("arena_stew", 4); +player.giveCustomItem("arena_medal", 16); +``` + ### player.giveNamedItem(itemId, count, name, lore) -给予带自定义名称和描述的物品。`lore` 为字符串数组。 +给予带自定义名称和描述的物品。`lore` 为字符串数组,每项一行描述文字。 ```js player.giveNamedItem("minecraft:gold_ingot", 1, "§6§l跑酷金牌", [ @@ -379,6 +447,23 @@ player.giveNamedItem("minecraft:diamond_sword", 1, "§c§l烈焰之刃", [ ]); ``` +### player.getHeldItem() + +获取主手物品,返回 `{id, count}`。空手返回 `{id: "minecraft:air", count: 0}`。 + +```js +var held = player.getHeldItem(); +console.log(held.id, held.count); // "minecraft:diamond_sword" 1 +``` + +### player.clearInventory() + +清空背包(包括盔甲槽和副手)。 + +```js +player.clearInventory(); +``` + ## 药水效果 ### player.addEffect(effectId, duration, amplifier) @@ -387,7 +472,7 @@ player.giveNamedItem("minecraft:diamond_sword", 1, "§c§l烈焰之刃", [ ### player.addEffect(effectId, duration, amplifier, hideParticles) -⬆ MC 扩展 | 添加效果并可选隐藏粒子。 +⬆ MC 扩展 | 添加效果并可选择隐藏粒子。 ### player.clearEffects() @@ -395,7 +480,7 @@ player.giveNamedItem("minecraft:diamond_sword", 1, "§c§l烈焰之刃", [ ```js player.addEffect("minecraft:speed", 600, 2); -player.addEffect("minecraft:jump_boost", 99999, 1, true); // 永久,无粒子 +player.addEffect("minecraft:jump_boost", 99999, 1, true); // 永久,无粒子 player.clearEffects(); ``` @@ -403,22 +488,38 @@ player.clearEffects(); ### player.playSound(path, volume, pitch) -⬆ MC 扩展 | 向该玩家播放音效。`path` 为命名空间 ID。 +⬆ MC 扩展 | 向该玩家单独播放音效。`path` 为命名空间 ID(如 `"minecraft:block.note_block.pling"`),`volume` 0–1,`pitch` 0.5–2。 ### player.runCommand(cmd) -⬆ MC 扩展 | 以玩家身份执行命令。 +⬆ MC 扩展 | 以该玩家身份执行 Minecraft 命令。 ```js player.playSound("minecraft:block.note_block.pling", 0.8, 1.5); player.runCommand("say hello"); ``` +## 成就 + +### player.grantAdvancement(advancementId) + +⬆ MC 扩展 | 为该玩家授予成就/进度。 + +### player.revokeAdvancement(advancementId) + +⬆ MC 扩展 | 撤销该玩家的成就/进度。 + +```js +player.grantAdvancement("minecraft:story/mine_stone"); +player.grantAdvancement("minecraft:adventure/kill_a_mob"); +player.revokeAdvancement("minecraft:story/mine_stone"); +``` + ## Tab 列表 ### player.setPlayerListName(name) -⬆ MC 扩展 | 修改该玩家在 Tab 列表中显示的名字。 +⬆ MC 扩展 | 修改该玩家在 Tab 列表中显示的名字(支持颜色代码)。 ```js player.setPlayerListName("§e[CP3] §f" + player.name); diff --git a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md index 431729b..92f9c46 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/player_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/player_en.md @@ -1,47 +1,58 @@ # player — Player API -The `player` object is obtained via `entity.player` and represents a logged-in player. It includes all `entity` capabilities plus player-specific features: inventory, XP, flight, messaging, teleport, etc. +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. ```js -world.onPlayerJoin((entity) => { - var p = entity.player; // p is the player object +world.onPlayerJoin(function(entity, tick) { + var p = entity.player; // p is the player object p.directMessage("Welcome back, " + p.name + "!"); }); ``` -## Basic Info +## Basic Identity ### player.name -✅ Box3 API | Read-only. Player name. +✅ Box3 API | Readonly. Player display name. ### player.userId -✅ Box3 API | Read-only. Player UUID string. +✅ Box3 API | Readonly. Player UUID string (same as `entity.id`). -### player.getOpLevel() +### player.opLevel -⬆ MC Extension | Get/set the player's operator permission level (0–4). 0 = normal player, 1 = bypass spawn protection, 2 = most commands, 3 = manage players, 4 = full access. +✅ Box3 API | Gets/sets the player's operator permission level (0–4). + +| Level | Description | +|-------|-------------| +| 0 | Normal player | +| 1 | Can bypass spawn protection | +| 2 | Can use most commands | +| 3 | Can manage players | +| 4 | Full admin (equivalent to `/op`) | ```js -if (player.getOpLevel() >= 2) { - // operations requiring permission level 2 +if (player.opLevel >= 2) { + // Operations requiring permission level 2 } -player.opLevel = 3; // set to level 3 +player.opLevel = 3; // Set to level 3 via property ``` +There is also `player.getOpLevel()` method returning the permission level number. + ## Appearance ### player.invisible -✅ Box3 API | Get/set whether the player is invisible. +✅ Box3 API | Gets/sets whether the player is invisible. ### player.scale -✅ Box3 API | Read-only. Player scale value. +✅ Box3 API | Readonly. Player model scale (Minecraft native scale, not Box3 scale). ```js -player.invisible = true; // invisible +player.invisible = true; // Invisible +console.log("Player scale: " + player.scale); ``` ## Movement @@ -50,31 +61,68 @@ All ✅ Box3 API. ### player.walkSpeed -Walking speed, corresponds to `MOVEMENT_SPEED` attribute. Default ~0.1. +Walk speed, maps to `MOVEMENT_SPEED` attribute base value. Default ~0.1. ### player.runSpeed -Running speed. Automatically maintained as `walkSpeed × 1.3`. +Run/sprint speed. Get returns `walkSpeed × 1.3`; set auto-calculates `walkSpeed` to maintain the 1.3× ratio. ### player.jumpPower -Jump strength, corresponds to `JUMP_STRENGTH` attribute. +Jump strength, maps to `JUMP_STRENGTH` attribute base value. Default ~0.42. + +### player.enableJump + +Gets/sets whether jumping is enabled. Default `true`. When set to `false`, saves the current jump strength and sets `JUMP_STRENGTH` to 0; set back to `true` to restore. + +```js +player.enableJump = false; // Disable jumping +player.enableJump = true; // Re-enable jumping +``` + +### player.crouchSpeed + +Gets/sets crouch speed (custom property, default `0.0`). MC has no independent sneak speed attribute; scripts can read this for custom logic. + +### player.swimSpeed + +Gets/sets swim speed. Backed by the `WATER_MOVEMENT_EFFICIENCY` attribute. + +```js +player.swimSpeed = 0.5; // Swim faster +``` ### player.moveState -Read-only. Current movement state: `"FLYING"`, `"SWIM"`, `"JUMP"`, `"FALL"`, `"GROUND"`. +Readonly. Current movement state string: + +| Value | Description | +|-------|-------------| +| `"FLYING"` | Currently flying | +| `"SWIM"` | In water | +| `"JUMP"` | Jumping upward | +| `"FALL"` | Falling | +| `"GROUND"` | On the ground | ### player.walkState -Read-only. Current walking state: `"CROUCH"`, `"RUN"`, `"WALK"`, `"NONE"`. +Readonly. Current walk state string: + +| Value | Description | +|-------|-------------| +| `"CROUCH"` | Crouching / sneaking | +| `"RUN"` | Sprinting | +| `"WALK"` | Walking | +| `"NONE"` | Standing still | ```js -player.walkSpeed = 0.2; // speed up -player.jumpPower = 0.6; // jump higher +player.walkSpeed = 0.2; // Speed up +player.jumpPower = 0.6; // Jump higher +player.swimSpeed = 0.3; // Swim speed -world.onTick(() => { +world.onTick(function() { if (player.walkState === "RUN") { - // player is sprinting + // Player is sprinting } }); ``` @@ -83,15 +131,15 @@ world.onTick(() => { ### player.canFly -✅ Box3 API | Get/set flight permission (`mayfly`). When `true`, player can take off by pressing jump. +✅ Box3 API | Gets/sets flight permission (`mayfly`). When `true`, player can take off by pressing jump. ### player.flying -✅ Box3 API | Get/set whether currently flying (`flying`). Requires `canFly = true` first. +✅ Box3 API | Gets/sets whether the player is currently flying (`flying`). Requires `canFly = true` first. ### player.flySpeed -✅ Box3 API | Flight speed. +✅ Box3 API | Flying speed. ### player.disableFly @@ -99,10 +147,10 @@ world.onTick(() => { ### player.spectator -✅ Box3 API | Read-only. Whether the player is in spectator mode. +✅ Box3 API | Readonly. Whether the player is in spectator mode. ```js -// Allow flight +// Enable flight player.canFly = true; player.flySpeed = 0.1; @@ -115,49 +163,55 @@ player.disableFly = true; ### player.collision -⬆ MC Extension | Get/set team collision. Set to `false` to prevent players pushing each other. Modifies the team's `CollisionRule` internally. +⬆ MC extension | Gets/sets team collision. Set to `false` to prevent players from pushing each other. Backed by the player's team `CollisionRule` (ALWAYS / NEVER). ```js -player.collision = false; // disable collision -console.log(player.collision); // false +player.collision = false; // Disable collision +console.log(player.collision); ``` ## Health -⬆ MC Extension | Get/set player health. `ServerPlayer` is a `LivingEntity`, so health is accessed directly. +⬆ MC extension | Gets/sets player health. `ServerPlayer` is a `LivingEntity`, so health is operated on directly. ### player.hp -Get/set current health. +Gets/sets current health. ### player.maxHp -Get/set maximum health. +Gets/sets maximum health. + +### player.dead + +Readonly. Whether the player is dead or dying (`isDeadOrDying()`). ```js -// Set class-based health -player.maxHp = 40; // Warrior 40 HP -player.hp = 40; // full health +// Set class-specific health +player.maxHp = 40; // Warrior 40 HP +player.hp = 40; // Full health -// If current HP exceeds new max, it's auto-capped +// Current health is clamped when max decreases player.maxHp = 20; -// player.hp is auto-clamped to 20 -``` +// player.hp auto-clamped to 20 -> Typically set during `!join` or `!ready` phase for class-based HP. Use `player.addEffect("minecraft:instant_health", ...)` for healing afterward. +if (player.dead) { + console.log("Player is dead"); +} +``` -## Gamemode +## Game Mode ### player.gameMode -✅ Box3 API | Get/set the player's gamemode. Get returns the name string; set accepts a string or number. +✅ Box3 API | Gets/sets game mode. Get returns a name string; set accepts a string or number. ```js -player.gameMode = "creative"; // creative mode -player.gameMode = "survival"; // survival mode -player.gameMode = "adventure"; // adventure mode -player.gameMode = "spectator"; // spectator mode -// or numbers: 0=survival, 1=creative, 2=adventure, 3=spectator +player.gameMode = "creative"; // Creative +player.gameMode = "survival"; // Survival +player.gameMode = "adventure"; // Adventure +player.gameMode = "spectator"; // Spectator +// Or by number: 0=survival, 1=creative, 2=adventure, 3=spectator ``` ## Camera @@ -166,27 +220,27 @@ All ✅ Box3 API. ### player.cameraMode -Get/set camera mode: `"FPS"` (first person) or `"FOLLOW"` (follow entity). +Gets/sets the camera mode: `"FPS"` (first-person) or `"FOLLOW"` (follow entity). Setting to `"FPS"` clears the follow target. ### player.cameraEntity -Set or get the followed entity object (`Box3JSEntity`). +Gets/sets the entity to follow (`GameEntity`). Setting an entity auto-switches camera mode to `"FOLLOW"`; setting `null` switches back to `"FPS"`. ### player.cameraPitch / player.cameraYaw -Camera pitch and yaw angles. +Camera pitch (vertical angle) and yaw (horizontal angle). Note: in MC, yaw maps to Y rotation (yRot), pitch maps to X rotation (xRot). ### player.facingDirection -Read-only `GameVector3`. The player's look direction unit vector. +Readonly `GameVector3`. The unit vector of the player's look direction. ### player.cameraTarget -Read-only `GameVector3`. The point 5 blocks ahead of the player's eyes. +Readonly `GameVector3`. A point 5 blocks ahead of the player's eyes. ### player.lookAt(x, y, z) -⬆ MC Extension | Make the player look at the given coordinates. +⬆ MC extension | Makes the player look at the given coordinates. ### player.lookAt(pos) @@ -196,7 +250,7 @@ Read-only `GameVector3`. The point 5 blocks ahead of the player's eyes. player.lookAt(10, 100, 10); player.lookAt(target.position); -// Get look direction +// Get view information var dir = player.facingDirection; var target = player.cameraTarget; ``` @@ -205,19 +259,33 @@ var target = player.cameraTarget; ### player.teleport(pos) -✅ Box3 API | Teleport the player to the given `GameVector3` coordinates. +✅ Box3 API | Teleports the player to the given `GameVector3` coordinates. + +### player.spawnPoint + +✅ Box3 API | Gets/sets the player's respawn point (`GameVector3`). When reading, returns the world spawn if the player hasn't set a personal respawn point. + +```js +// Property-style set +player.spawnPoint = new GameVector3(0, 100, 0); +console.log(player.spawnPoint); +``` ### player.setRespawnPoint(pos) -✅ Box3 API | Set the player's respawn point. +✅ Box3 API | Sets the player's respawn point (method-style, equivalent to `spawnPoint` property). + +### player.setSpawnPoint(pos) + +✅ Box3 API | Same as `setRespawnPoint`, Box3 standard naming. ### player.respawn() -✅ Box3 API | Force the player to respawn (only effective when dead). +✅ Box3 API | Forces the player to respawn (only works when dead). ### player.dimension -⬆ MC Extension | Get/set the player's dimension. Set can teleport cross-dimension. +⬆ MC extension | Gets/sets the player's dimension. Setting it performs a cross-dimensional teleport. ```js player.teleport(new GameVector3(0, 100, 0)); @@ -228,15 +296,15 @@ player.dimension = "minecraft:the_nether"; player.teleport(new GameVector3(0, 70, 0)); ``` -## Kick +## Kicking ### player.kick() -✅ Box3 API | Kick the player, default reason "Kicked". +✅ Box3 API | Kicks the player with the default reason "Kicked". ### player.kick(reason) -✅ Box3 API | Kick the player with a custom reason. +✅ Box3 API | Kicks the player with a custom reason. ```js player.kick("You have been removed from the game"); @@ -246,23 +314,23 @@ player.kick("You have been removed from the game"); ### player.directMessage(msg) -✅ Box3 API | Send a chat message to the player. +✅ Box3 API | Sends a chat message visible only to this player (system message). ### player.actionBar(msg) -✅ Box3 API | Send an action bar message (above the hotbar). +✅ Box3 API | Sends a message displayed on the action bar (above the hotbar). ### player.title(title, subtitle) -✅ Box3 API | Send a screen title to the player. Uses default animation parameters. +✅ Box3 API | Displays a screen title with default animation: fade-in 10 ticks, stay 70 ticks, fade-out 20 ticks. ### player.title(title, subtitle, fadeIn, stay, fadeOut) -⬆ MC Extension | Full-parameter title. `fadeIn`/`stay`/`fadeOut` are in ticks. +⬆ MC extension | Title with full animation parameters. `fadeIn`/`stay`/`fadeOut` are all in ticks (20 ticks = 1 second). ### player.dialog(config) -✅ Box3 API | Show a dialog. Pass `{content, options}` config, returns `{index, value}`. Currently implemented as a simplified system message in MC. +✅ Box3 API | Shows a dialog panel. Pass `{content, options}`, returns `{index, value}`. Currently simplified in MC — sends system messages. ```js var result = player.dialog({ @@ -274,11 +342,11 @@ player.directMessage("You chose: " + result.value); ### player.link(href) -✅ Box3 API | Send a clickable link to the player. +✅ Box3 API | Sends a clickable URL link to the player (blue underlined text). ### player.onChat(handler) -✅ Box3 API | Register a per-player chat callback (for finer control, commonly used in dialogue trees). +✅ Box3 API | Registers a per-player chat handler (more granular than global `world.onChat`, useful for dialog trees). ```js player.directMessage("Hello!"); @@ -286,72 +354,57 @@ player.actionBar("§eType !help for help"); player.title("§6§lBOSS FIGHT", "§7Defeat all enemies", 10, 60, 10); player.link("https://example.com"); -// Dialogue tree +// Dialog tree player.directMessage("Enter your choice: A or B"); -player.onChat((entity, msg, tick) => { +player.onChat(function(entity, msg, tick) { if (msg === "A") { player.directMessage("You chose A"); } }); ``` -## XP & Food +## Experience & Food ### player.xp -⬆ MC Extension | Get/set experience level. +⬆ MC extension | Gets/sets experience level. ### player.addExperienceLevels(levels) -⬆ MC Extension | Add `levels` experience levels. +⬆ MC extension | Adds `levels` experience levels. ### player.food -⬆ MC Extension | Get/set food level (0–20). +⬆ MC extension | Gets/sets food level (0–20). ### player.saturation -⬆ MC Extension | Get/set saturation (0–20, float). +⬆ MC extension | Gets/sets saturation level (0–20, floating-point). ```js -player.xp = 10; // set to level 10 -player.addExperienceLevels(3); // add 3 levels +player.xp = 10; // Set to level 10 +player.addExperienceLevels(3); // Add 3 levels player.food = 20; player.saturation = 10; ``` ## Inventory -All ⬆ MC Extension. +All ⬆ MC extension. ### player.giveItem(itemId, count) -Give an item. - -### player.clearInventory() - -Clear the inventory. +Gives items to the player. -### player.getHeldItem() +### player.giveEnchantedItem(itemId, count, enchants) -Get the main hand item, returns `{id, count}`. Empty hand returns `{id: "minecraft:air", count: 0}`. +Gives an enchanted item. `enchants` is an `{enchantmentId: level}` object. ```js player.giveItem("minecraft:diamond_sword", 1); player.giveItem("minecraft:golden_apple", 5); player.giveItem("minecraft:arrow", 64); -var held = player.getHeldItem(); -console.log(held.id, held.count); // "minecraft:diamond_sword" 1 - -player.clearInventory(); -``` - -### player.giveEnchantedItem(itemId, count, enchants) - -Give an enchanted item. `enchants` is a `{enchantmentId: level}` object. - -```js player.giveEnchantedItem("minecraft:diamond_sword", 1, { "minecraft:sharpness": 5, "minecraft:fire_aspect": 2, @@ -365,39 +418,69 @@ player.giveEnchantedItem("minecraft:bow", 1, { }); ``` +### player.giveCustomItem(id, count) + +⬆ MC extension | Gives a custom item loaded via `world.loadCustomItems()`. Items use `minecraft:paper` as a carrier with DataComponents for name, texture, food, etc. + +```js +// Load config first +world.loadCustomItems("box3js-items"); +// Then give items +player.giveCustomItem("arena_trophy", 1); +player.giveCustomItem("arena_stew", 4); +player.giveCustomItem("arena_medal", 16); +``` + ### player.giveNamedItem(itemId, count, name, lore) -Give an item with a custom name and lore. `lore` is a string array. +Gives an item with a custom name and lore. `lore` is a string array, one line per entry. ```js -player.giveNamedItem("minecraft:gold_ingot", 1, "§6§lParkour Gold Medal", [ +player.giveNamedItem("minecraft:gold_ingot", 1, "§6§lParkour Medal", [ "§7Sky Parkour Championship", - "§eFinish time: 1:23.450", + "§eFinishing time: 1:23.450", ]); -player.giveNamedItem("minecraft:diamond_sword", 1, "§c§lBlade of Flame", [ +player.giveNamedItem("minecraft:diamond_sword", 1, "§c§lFlameblade", [ "§7Bound: Fire", "§eRight-click: Launch fireball", ]); ``` -## Potion Effects +### player.getHeldItem() + +Returns the currently held main-hand item as `{id, count}`. Empty hand returns `{id: "minecraft:air", count: 0}`. + +```js +var held = player.getHeldItem(); +console.log(held.id, held.count); // "minecraft:diamond_sword" 1 +``` + +### player.clearInventory() + +Clears the entire inventory (including armor slots and offhand). + +```js +player.clearInventory(); +``` + +## Status Effects ### player.addEffect(effectId, duration, amplifier) -⬆ MC Extension | Add a potion effect. `duration` in ticks, `amplifier` starts at 0. +⬆ MC extension | Applies a status effect. `duration` in ticks, `amplifier` starts at 0. ### player.addEffect(effectId, duration, amplifier, hideParticles) -⬆ MC Extension | Add effect with optional particle hiding. +⬆ MC extension | Applies an effect, optionally hiding particles. ### player.clearEffects() -⬆ MC Extension | Remove all potion effects. +⬆ MC extension | Removes all status effects. ```js player.addEffect("minecraft:speed", 600, 2); -player.addEffect("minecraft:jump_boost", 99999, 1, true); // permanent, no particles +player.addEffect("minecraft:jump_boost", 99999, 1, true); // Permanent, no particles player.clearEffects(); ``` @@ -405,22 +488,38 @@ player.clearEffects(); ### player.playSound(path, volume, pitch) -⬆ MC Extension | Play a sound to this player. `path` is a namespaced ID. +⬆ MC extension | Plays a sound to this player only. `path` is a namespace ID (e.g. `"minecraft:block.note_block.pling"`), `volume` 0–1, `pitch` 0.5–2. ### player.runCommand(cmd) -⬆ MC Extension | Execute a command as this player. +⬆ MC extension | Executes a Minecraft command as this player. ```js player.playSound("minecraft:block.note_block.pling", 0.8, 1.5); player.runCommand("say hello"); ``` +## Advancements + +### player.grantAdvancement(advancementId) + +⬆ MC extension | Grants an advancement to this player. + +### player.revokeAdvancement(advancementId) + +⬆ MC extension | Revokes an advancement from this player. + +```js +player.grantAdvancement("minecraft:story/mine_stone"); +player.grantAdvancement("minecraft:adventure/kill_a_mob"); +player.revokeAdvancement("minecraft:story/mine_stone"); +``` + ## Tab List ### player.setPlayerListName(name) -⬆ MC Extension | Modify this player's displayed name in the tab list. +⬆ MC extension | Changes the player's display name in the tab list (supports color codes). ```js player.setPlayerListName("§e[CP3] §f" + player.name); @@ -429,46 +528,3 @@ player.setPlayerListName("§6★ §f" + player.name); // Reset to original name player.setPlayerListName(player.name); ``` - -## Box3 API List - -| API | Type | -| ----------------------------------------------------------- | ------- | -| `name` | ✅ Box3 | -| `userId` | ✅ Box3 | -| `invisible` | ✅ Box3 | -| `scale` | ✅ Box3 | -| `walkSpeed` / `runSpeed` / `jumpPower` | ✅ Box3 | -| `moveState` / `walkState` | ✅ Box3 | -| `canFly` / `flying` / `flySpeed` / `disableFly` | ✅ Box3 | -| `spectator` | ✅ Box3 | -| `gameMode` | ✅ Box3 | -| `cameraMode` / `cameraEntity` / `cameraPitch` / `cameraYaw` | ✅ Box3 | -| `facingDirection` / `cameraTarget` | ✅ Box3 | -| `setRespawnPoint()` / `respawn()` | ✅ Box3 | -| `kick()` | ✅ Box3 | -| `teleport()` | ✅ Box3 | -| `directMessage()` / `actionBar()` | ✅ Box3 | -| `title()` (2-param) | ✅ Box3 | -| `dialog()` | ✅ Box3 | -| `link()` | ✅ Box3 | -| `onChat()` (player-level) | ✅ Box3 | - -## MC Extension List - -| API | Type | -| ---------------------------------------------------- | ---- | -| `collision` | ⬆ MC | -| `title()` (5-param) | ⬆ MC | -| `hp` / `maxHp` | ⬆ MC | -| `xp` / `addExperienceLevels()` | ⬆ MC | -| `food` / `saturation` | ⬆ MC | -| `giveItem()` / `clearInventory()` / `getHeldItem()` | ⬆ MC | -| `giveEnchantedItem()` / `giveNamedItem()` | ⬆ MC | -| `addEffect()` (3/4-param) / `clearEffects()` | ⬆ MC | -| `playSound()` | ⬆ MC | -| `dimension` | ⬆ MC | -| `lookAt()` | ⬆ MC | -| `runCommand()` | ⬆ MC | -| `setPlayerListName()` | ⬆ MC | -| `getOpLevel()` / `opLevel` | ⬆ MC | diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world.md b/Box3JS-NeoForge-1.21.1/docs/api/world.md index 61e47fa..e7ce07c 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world.md @@ -4,20 +4,29 @@ ## 世界属性 -### world.projectName() +### world.projectName -⬆ MC 扩展 | 只读。服务端 MOTD 字符串。 +✅ Box3 API | 只读属性。服务端 MOTD 字符串。为兼容旧代码也可作为方法调用 `world.projectName()`。 ```js -console.log(world.projectName()); // "A Minecraft Server" +console.log(world.projectName); // "A Minecraft Server" ``` -### world.currentTick() +### world.serverId -✅ Box3 API | 只读。服务器自启动以来的总 tick 数。 +✅ Box3 API | 可读写属性。服务器标识符,映射到服务端 MOTD。 ```js -var uptime = world.currentTick(); +world.serverId = "My Cool Server"; +console.log(world.serverId); +``` + +### world.currentTick + +✅ Box3 API | 只读属性。服务器自启动以来的总 tick 数。为兼容旧代码也可作为方法调用 `world.currentTick()`。 + +```js +var uptime = world.currentTick; world.say("服务器已运行 " + Math.floor(uptime / 20 / 60) + " 分钟"); ``` @@ -148,15 +157,106 @@ zombie.setEquipment("mainhand", "minecraft:iron_sword"); zombie.setAI(true); ``` +### world.createEntity(config) + +✅ Box3 API | 使用完整配置对象生成实体。返回 `Box3JSEntity`。 + +支持的配置字段:`type`、`position`、`velocity`、`fixed`、`gravity`、`friction`、`mass`、`restitution`、`collides`、`meshInvisible`、`hp`、`maxHp`、`tags`(数组)。 + +```js +var entity = world.createEntity({ + type: "minecraft:skeleton", + position: new GameVector3(0, 100, 0), + velocity: new GameVector3(0, 0.5, 0), + fixed: false, + gravity: true, + collides: true, + hp: 30, + maxHp: 30, + tags: ["enemy", "undead"] +}); +``` + +## 音效属性 + +✅ Box3 API | 存储音效路径字符串,设为非空后触发时机如下: + +| 属性 | 触发时机 | +| -------------------- | ----------------------------------------------------- | +| `ambientSound` | 每 200 tick(10 秒)在世界出生点以 0.3 音量播放 | +| `playerJoinSound` | 玩家加入时在其所在位置以满音量播放 | +| `playerLeaveSound` | 玩家退出时在其所在位置以满音量播放 | +| `placeVoxelSound` | 方块放置时在方块位置以满音量播放 | +| `breakVoxelSound` | 方块破坏时在方块位置以满音量播放 | + +设为 `null` 或空字符串可停止自动播放。 + +```js +world.ambientSound = "minecraft:ambient.cave"; +world.playerJoinSound = "minecraft:block.note_block.pling"; +world.playerLeaveSound = "minecraft:block.note_block.bass"; +world.placeVoxelSound = "minecraft:block.stone.place"; +world.breakVoxelSound = "minecraft:block.stone.break"; +``` + +## 音效 + +### world.sound(config) + +✅ Box3 API | 播放音效。`config` 可以是路径字符串或 `{path, position, volume, pitch}` 对象。 + +```js +// 字符串简写 — 在原点以默认音量/音调播放 +world.sound("minecraft:block.note_block.pling"); + +// 完整配置 +world.sound({ + path: "minecraft:entity.experience_orb.pickup", + position: new GameVector3(0, 100, 0), + volume: 0.8, + pitch: 1.5 +}); +``` + +## 搜索包围盒 + +### world.searchBox(bounds) + +✅ Box3 API | 查询 GameBounds3 区域内的所有实体。 + +```js +var bounds = new GameBounds3( + new GameVector3(-10, 0, -10), + new GameVector3(10, 50, 10) +); +var entities = world.searchBox(bounds); +``` + ## 事件回调 -所有事件回调由 `world.onXxx(handler)` 注册。除 `onTick` 外,回调第一个参数通常是触发该事件的 `entity`(`Box3JSEntity`)。 +所有事件回调由 `world.onXxx(handler)` 注册,返回 `GameEventHandlerToken`。调用 `.cancel()` 取消注册,`.active()` 检查状态。除 `onTick` 外,回调第一个参数通常是触发该事件的 `entity`(`Box3JSEntity`)。 + +### GameEventHandlerToken + +| 方法 | 说明 | +|--------|-------------| +| `token.cancel()` | 取消事件监听 | +| `token.active()` | 返回 `true` 表示监听仍处于活跃状态 | +| `token.resume()` | 抛出 UnsupportedOperationException — 请重新注册 | + +```js +var token = world.onTick(function(info) { + if (info.tick > 6000) { + token.cancel(); + } +}); +``` | 事件 | 类型 | 回调签名 | 触发时机 | | ---------------------------- | ------- | ------------------------------------------------------ | ------------------------------- | -| `world.onTick(fn)` | ✅ Box3 | `()` | 每 tick | -| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity)` | 玩家登录 | -| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity)` | 玩家退出 | +| `world.onTick(fn)` | ✅ Box3 | `(info)` → `{tick, prevTick, elapsedTimeMS, skip}` | 每 tick | +| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity, tick)` | 玩家登录 | +| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity, tick)` | 玩家退出 | | `world.onChat(fn)` | ✅ Box3 | `(entity, message, tick)` | 玩家发送聊天消息 | | `world.onVoxelDestroy(fn)` | ✅ Box3 | `(entity, x, y, z, voxel, tick)` | 玩家破坏方块 | | `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | 玩家放置方块 | @@ -169,15 +269,21 @@ zombie.setAI(true); | `world.onFluidLeave(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | 实体离开液体 | | `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | 实体死亡;`killer` 可能为 null | | `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | 实体受伤(Pre 阶段) | -| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity)` | 玩家重生 | +| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity, tick)` | 玩家重生 | +| `world.onButtonPressed(fn)` | ⬆ MC | `(entity, button, tick)` | 玩家按下按钮(见 GameButtonType)| | `world.onMessage(fn)` | ⬆ MC | `(from, data)` | 收到 `world.sendMessage()` 消息 | +所有 `onXxx()` 方法返回 `GameEventHandlerToken` — 调用 `.cancel()` 取消监听。 + ```js -world.onTick(() => { - // 每 tick 执行 +world.onTick((info) => { + // info.tick, info.prevTick, info.elapsedTimeMS, info.skip + if (info.tick % 100 === 0) { + world.say("服务器 tick: " + info.tick); + } }); -world.onPlayerJoin((entity) => { +world.onPlayerJoin((entity, tick) => { var p = entity.player; world.say(p.name + " 加入了游戏"); p.teleport(new GameVector3(0, 100, 0)); @@ -644,6 +750,60 @@ console.log(biome); // "minecraft:plains" var biome = world.getBiome(entity.position); ``` +## 自定义物品 + +### world.loadCustomItems(packName) + +⬆ MC 扩展 | 从资源包加载自定义物品配置。读取 `resourcepacks//items.json`,解析其中以 Minecraft 原生数据组件 ID 为 key 的物品定义。所有自定义物品以 `minecraft:paper` 为载体,通过 `DataComponents` 实现名称、描述、贴图、食物等功能。 + +JSON 格式使用 MC 原版组件 ID: + +| JSON Key | 对应 DataComponent | 说明 | +|----------|-------------------|------| +| `minecraft:custom_model_data` | `CUSTOM_MODEL_DATA` | 模型切换值,匹配资源包 paper.json 的 override | +| `minecraft:custom_name` | `CUSTOM_NAME` | 物品显示名称 | +| `minecraft:lore` | `LORE` | 描述文字数组 | +| `minecraft:max_stack_size` | `MAX_STACK_SIZE` | 最大堆叠数 (1–64),默认 64 | +| `minecraft:enchantment_glint_override` | `ENCHANTMENT_GLINT_OVERRIDE` | 附魔光效 | +| `minecraft:rarity` | `RARITY` | 稀有度: `common`/`uncommon`/`rare`/`epic` | +| `minecraft:food` | `FOOD` | 食物属性,子字段见下 | + +**`minecraft:food` 子字段:** + +| 子字段 | 类型 | 说明 | +|--------|------|------| +| `nutrition` | int | 营养值 (1–20) | +| `saturation` | float | 饱和度修饰符 | +| `can_always_eat` | bool | 是否始终可食用 | +| `eat_seconds` | float | 食用时间 (秒),≤0.8 为快速食用 | + +```js +world.loadCustomItems("box3js-items"); +// 加载 resourcepacks/box3js-items/items.json 中定义的所有物品 +// 之后可通过 player.giveCustomItem("arena_trophy", 1) 给予 +``` + +**资源包结构参考:** +``` +resourcepacks/box3js-items/ +├── pack.mcmeta +├── items.json # 物品定义 +└── assets/ + ├── minecraft/models/item/ + │ └── paper.json # custom_model_data overrides + └── box3js/ + ├── models/item/ # 模型 JSON + │ ├── arena_trophy.json + │ ├── arena_stew.json + │ └── arena_medal.json + └── textures/item/ # PNG 贴图 + ├── arena_trophy.png + ├── arena_stew.png + └── arena_medal.png +``` + +**注意:** 贴图依赖客户端加载资源包。未加载时物品功能正常(名称/描述/食物),仅显示为 paper 默认外观。 + ## 跨脚本消息 ### world.sendMessage(target, data) @@ -658,3 +818,53 @@ var biome = world.getBiome(entity.position); world.runCommand("time set day"); world.runCommand("weather clear"); ``` + +## 结构与成就 + +### world.placeStructure(x, y, z, structureId) + +⬆ MC 扩展 | 在指定位置放置数据包中的结构模板 (NBT)。 + +### world.placeStructure(pos, structureId) + +⬆ GameVector3 重载。 + +```js +world.placeStructure(0, 100, 0, "minecraft:village/plains/houses/plains_small_house_1"); +world.placeStructure(pos, "box3js:arena"); +``` + +### world.grantAdvancement(playerName, advancementId) + +⬆ MC 扩展 | 为指定玩家授予成就/进度。 + +```js +world.grantAdvancement("Steve", "minecraft:story/mine_stone"); +``` + +## 配方管理 + +### world.listRecipes(filter) + +⬆ MC 扩展 | 按关键字搜索配方 ID 列表。 + +```js +var recipes = world.listRecipes("diamond"); +console.log(recipes); // ["minecraft:diamond_sword", "minecraft:diamond_block", ...] +``` + +### world.removeRecipe(recipeId) + +⬆ MC 扩展 | 将指定配方加入黑名单并立即生效。返回是否成功。 + +```js +world.removeRecipe("minecraft:iron_pickaxe"); +``` + +### world.clearRecipes() + +⬆ MC 扩展 | 清除配方黑名单,恢复所有原始配方。 + +```js +world.clearRecipes(); +``` diff --git a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md index 558110a..c2c825b 100644 --- a/Box3JS-NeoForge-1.21.1/docs/api/world_en.md +++ b/Box3JS-NeoForge-1.21.1/docs/api/world_en.md @@ -4,20 +4,29 @@ ## World Properties -### world.projectName() +### world.projectName -⬆ MC Extension | Read-only. The server MOTD string. +✅ Box3 API | Read-only property. The server MOTD string. Also callable as `world.projectName()` for backward compatibility. ```js -console.log(world.projectName()); // "A Minecraft Server" +console.log(world.projectName); // "A Minecraft Server" ``` -### world.currentTick() +### world.serverId -✅ Box3 API | Read-only. Total ticks since server startup. +✅ Box3 API | Read/write property. Server identifier, maps to the server MOTD. ```js -var uptime = world.currentTick(); +world.serverId = "My Cool Server"; +console.log(world.serverId); +``` + +### world.currentTick + +✅ Box3 API | Read-only property. Total ticks since server startup. Also callable as `world.currentTick()` for backward compatibility. + +```js +var uptime = world.currentTick; world.say( "Server has been running for " + Math.floor(uptime / 20 / 60) + " minutes", ); @@ -150,15 +159,106 @@ zombie.setEquipment("mainhand", "minecraft:iron_sword"); zombie.setAI(true); ``` +### world.createEntity(config) + +✅ Box3 API | Spawn an entity with a full configuration object. Returns `Box3JSEntity`. + +Supported config fields: `type`, `position`, `velocity`, `fixed`, `gravity`, `friction`, `mass`, `restitution`, `collides`, `meshInvisible`, `hp`, `maxHp`, `tags` (array). + +```js +var entity = world.createEntity({ + type: "minecraft:skeleton", + position: new GameVector3(0, 100, 0), + velocity: new GameVector3(0, 0.5, 0), + fixed: false, + gravity: true, + collides: true, + hp: 30, + maxHp: 30, + tags: ["enemy", "undead"] +}); +``` + +## Sound Properties + +✅ Box3 API | Sound path strings that auto-play when set to a non-empty value: + +| Property | Trigger | +| -------------------- | -------------------------------------------------------------- | +| `ambientSound` | Every 200 ticks (10s) at world spawn with 0.3 volume | +| `playerJoinSound` | At player's position with full volume when a player joins | +| `playerLeaveSound` | At player's position with full volume when a player leaves | +| `placeVoxelSound` | At block position with full volume when a block is placed | +| `breakVoxelSound` | At block position with full volume when a block is broken | + +Set to `null` or empty string to stop auto-play. + +```js +world.ambientSound = "minecraft:ambient.cave"; +world.playerJoinSound = "minecraft:block.note_block.pling"; +world.playerLeaveSound = "minecraft:block.note_block.bass"; +world.placeVoxelSound = "minecraft:block.stone.place"; +world.breakVoxelSound = "minecraft:block.stone.break"; +``` + +## Sound + +### world.sound(config) + +✅ Box3 API | Play a sound. `config` can be a path string or `{path, position, volume, pitch}` object. + +```js +// String shorthand — plays at origin with default volume/pitch +world.sound("minecraft:block.note_block.pling"); + +// Full config +world.sound({ + path: "minecraft:entity.experience_orb.pickup", + position: new GameVector3(0, 100, 0), + volume: 0.8, + pitch: 1.5 +}); +``` + +## Search Box + +### world.searchBox(bounds) + +✅ Box3 API | Query all entities within a GameBounds3 region. + +```js +var bounds = new GameBounds3( + new GameVector3(-10, 0, -10), + new GameVector3(10, 50, 10) +); +var entities = world.searchBox(bounds); +``` + ## Event Callbacks -All event callbacks are registered via `world.onXxx(handler)`. Except for `onTick`, the first callback parameter is usually the triggering `entity` (`Box3JSEntity`). +All event callbacks are registered via `world.onXxx(handler)`, returning a `GameEventHandlerToken`. Call `.cancel()` to unregister, `.active()` to check status. Except for `onTick`, the first callback parameter is usually the triggering `entity` (`Box3JSEntity`). + +### GameEventHandlerToken + +| Method | Description | +|--------|-------------| +| `token.cancel()` | Unregister the event handler | +| `token.active()` | Returns `true` if the handler is still active | +| `token.resume()` | Throws UnsupportedOperationException — re-register instead | + +```js +var token = world.onTick(function(info) { + if (info.tick > 6000) { + token.cancel(); + } +}); +``` | Event | Type | Callback Signature | Trigger | | ---------------------------- | ------- | ------------------------------------------------------ | -------------------------------------- | -| `world.onTick(fn)` | ✅ Box3 | `()` | Every tick | -| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity)` | Player logs in | -| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity)` | Player leaves | +| `world.onTick(fn)` | ✅ Box3 | `(info)` → `{tick, prevTick, elapsedTimeMS, skip}` | Every tick | +| `world.onPlayerJoin(fn)` | ✅ Box3 | `(entity, tick)` | Player logs in | +| `world.onPlayerLeave(fn)` | ✅ Box3 | `(entity, tick)` | Player leaves | | `world.onChat(fn)` | ✅ Box3 | `(entity, message, tick)` | Player sends chat message | | `world.onVoxelDestroy(fn)` | ✅ Box3 | `(entity, x, y, z, voxel, tick)` | Player breaks a block | | `world.onBlockPlace(fn)` | ⬆ MC | `(entity, x, y, z, voxel, voxelId, tick)` | Player places a block | @@ -171,15 +271,21 @@ All event callbacks are registered via `world.onXxx(handler)`. Except for `onTic | `world.onFluidLeave(fn)` | ✅ Box3 | `(entity, fluid, x, y, z, tick)` | Entity leaves a fluid | | `world.onEntityDeath(fn)` | ⬆ MC | `(entity, killer, tick)` | Entity dies; `killer` may be null | | `world.onEntityDamage(fn)` | ⬆ MC | `(entity, amount, source, attacker, tick)` | Entity takes damage (Pre phase) | -| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity)` | Player respawns | +| `world.onPlayerRespawn(fn)` | ⬆ MC | `(entity, tick)` | Player respawns | +| `world.onButtonPressed(fn)` | ⬆ MC | `(entity, button, tick)` | Player presses a button (see GameButtonType) | | `world.onMessage(fn)` | ⬆ MC | `(from, data)` | Receives `world.sendMessage()` message | +All `onXxx()` methods return `GameEventHandlerToken` — call `.cancel()` to unregister. + ```js -world.onTick(() => { - // runs every tick +world.onTick((info) => { + // info.tick, info.prevTick, info.elapsedTimeMS, info.skip + if (info.tick % 100 === 0) { + world.say("Server tick: " + info.tick); + } }); -world.onPlayerJoin((entity) => { +world.onPlayerJoin((entity, tick) => { var p = entity.player; world.say(p.name + " joined the game"); p.teleport(new GameVector3(0, 100, 0)); @@ -660,3 +766,88 @@ var biome = world.getBiome(entity.position); world.runCommand("time set day"); world.runCommand("weather clear"); ``` + +## Structures & Advancements + +### world.placeStructure(x, y, z, structureId) + +⬆ MC extension | Places a datapack structure template (NBT) at the given position. + +### world.placeStructure(pos, structureId) + +⬆ GameVector3 overload. + +```js +world.placeStructure(0, 100, 0, "minecraft:village/plains/houses/plains_small_house_1"); +world.placeStructure(pos, "box3js:arena"); +``` + +### world.grantAdvancement(playerName, advancementId) + +⬆ MC extension | Grants an advancement to a player by name. + +```js +world.grantAdvancement("Steve", "minecraft:story/mine_stone"); +``` + +## Recipe Management + +### world.listRecipes(filter) + +⬆ MC extension | Searches recipe IDs matching a keyword. + +```js +var recipes = world.listRecipes("diamond"); +console.log(recipes); // ["minecraft:diamond_sword", "minecraft:diamond_block", ...] +``` + +### world.removeRecipe(recipeId) + +⬆ MC extension | Blacklists a recipe so it's no longer craftable. Returns whether successful. + +```js +world.removeRecipe("minecraft:iron_pickaxe"); +``` + +### world.clearRecipes() + +⬆ MC extension | Clears the recipe blacklist, restoring all original recipes. + +```js +world.clearRecipes(); +``` + +## Custom Items + +### world.loadCustomItems(packName) + +⬆ MC extension | Loads custom item definitions from a resource pack's `items.json`. Reads `resourcepacks//items.json`, parses item definitions using Minecraft's native data component IDs as JSON keys. All items use `minecraft:paper` as the base, with `DataComponents` providing name, lore, texture, food, etc. + +JSON format uses MC component ID prefixes: + +| JSON Key | DataComponent | Description | +|----------|--------------|-------------| +| `minecraft:custom_model_data` | `CUSTOM_MODEL_DATA` | Model predicate value, matched by paper.json overrides | +| `minecraft:custom_name` | `CUSTOM_NAME` | Display name | +| `minecraft:lore` | `LORE` | Lore text array | +| `minecraft:max_stack_size` | `MAX_STACK_SIZE` | Max stack size (1–64), default 64 | +| `minecraft:enchantment_glint_override` | `ENCHANTMENT_GLINT_OVERRIDE` | Enchantment foil effect | +| `minecraft:rarity` | `RARITY` | Rarity: `common`/`uncommon`/`rare`/`epic` | +| `minecraft:food` | `FOOD` | Food properties (see sub-fields below) | + +**`minecraft:food` sub-fields:** + +| Sub-field | Type | Description | +|-----------|------|-------------| +| `nutrition` | int | Nutrition value (1–20) | +| `saturation` | float | Saturation modifier | +| `can_always_eat` | bool | Always edible | +| `eat_seconds` | float | Eat time in seconds, ≤0.8 = fast | + +```js +world.loadCustomItems("box3js-items"); +// Loads all items defined in resourcepacks/box3js-items/items.json +// Items can then be given via player.giveCustomItem("arena_trophy", 1) +``` + +**Note:** Textures require the client to load the resource pack. Without it, items still function (name/lore/food), but display the default paper texture. diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md new file mode 100644 index 0000000..1a470f0 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md @@ -0,0 +1,230 @@ +# 教程一:从零开始 + +本教程将带你创建第一个 Box3JS 脚本,逐步掌握控制台输出、聊天命令和基础 API。 + +## 前置要求 + +- 服务端已安装 Box3JS 模组 +- 了解基础的 JavaScript/TypeScript 语法 + +## 1.1 创建项目 + +在游戏内执行: + +``` +/box3script sandbox mytutorial +``` + +这会在 `config/box3/script/mytutorial/` 下创建一个 TypeScript 项目模板。如果你想用纯 JavaScript,直接把 `src/app.ts` 当 JS 写即可——构建工具不会检查类型。 + +目录结构: + +``` +config/box3/script/mytutorial/ +├── src/ +│ └── app.ts ← 入口文件,代码写在这里 +├── types/ +│ └── globals.d.ts ← API 类型声明(只读) +├── build.mjs ← 构建脚本 +└── package.json +``` + +每次改完代码后,在 `mytutorial/` 目录下执行: + +```bash +node build.mjs +``` + +构建成功后,在游戏内开启脚本: + +``` +/box3script on mytutorial +``` + +## 1.2 第一个脚本 + +打开 `src/app.ts`,清空内容,写入: + +```js +// 脚本加载时执行 +console.log("[MyTutorial] Hello, Box3JS!"); + +// 服务器启动后执行 +world.onPlayerJoin((entity, tick) => { + entity.player.directMessage("§a欢迎来到服务器!"); +}); +``` + +构建并开启脚本后,玩家加入就会看到欢迎消息。 + +## 1.3 控制台输出 + +`console` 对象有 4 个级别: + +```js +console.log("普通日志"); // [Box3JS] [mytutorial] 普通日志 +console.debug("调试信息"); // [Box3JS] [mytutorial] [DEBUG] 调试信息 +console.warn("警告"); // [Box3JS] [mytutorial] [WARN] 警告 +console.error("错误"); // [Box3JS] [mytutorial] [ERROR] 错误 +``` + +输出会显示在服务端控制台,格式为 `[Box3JS] [项目名] message`。 + +## 1.4 简单聊天命令 + +让我们写一个聊天命令系统: + +```js +world.onChat((entity, message, tick) => { + const player = entity.player; + + // 用 switch 处理不同命令 + switch (message) { + case "!hello": + player.directMessage("§e你好," + player.name + "!"); + return false; // 取消原始消息 + + case "!time": + player.directMessage("§e当前游戏时间: §f" + world.time); + return false; + + case "!pos": + const pos = player.position; + player.directMessage( + "§e你的位置: §f" + + Math.floor(pos.x) + ", " + + Math.floor(pos.y) + ", " + + Math.floor(pos.z), + ); + return false; + + case "!online": + const count = world.querySelectorAll("*").length; + player.directMessage("§e在线玩家: §f" + count + " 人"); + return false; + } + + return true; // 不是命令的消息正常发送 +}); +``` + +**关键点:** `onChat` 回调返回 `false` 会阻止消息在聊天栏显示,返回 `true` 则正常发送。 + +## 1.5 定时任务 + +```js +// 每 5 分钟广播一次 +world.setInterval(() => { + const online = world.querySelectorAll("*").length; + if (online > 0) { + world.say("§6[服务器] §f当前在线 " + online + " 人"); + } +}, 6000); // 6000 ticks = 5 分钟 + +// 30 秒后执行一次 +world.setTimeout(() => { + world.say("§6[服务器] §f已运行 30 秒"); +}, 600); // 600 ticks = 30 秒 +``` + +**Ticks 换算:** 20 ticks = 1 秒。`setInterval(fn, 20)` = 每秒执行一次。 + +## 1.6 世界属性 + +```js +// 时间控制 +world.time = 6000; // 正午 (0=日出, 6000=正午, 12000=日落, 18000=午夜) +world.timeScale = 0; // 暂停时间 +world.timeScale = 1; // 恢复 + +// 天气 +world.rainDensity = 1.0; // 下雨 +world.thunderDensity = 0.5; // 雷暴 +world.clearWeather(); // 晴天 + +// 难度 +world.difficulty = "hard"; // peaceful / easy / normal / hard + +// 游戏规则 +world.setGameRule("keepInventory", true); // 死亡不掉落 +world.setGameRule("doFireTick", false); // 火焰不蔓延 +``` + +## 1.7 广播与消息类型 + +```js +// 全服广播 (聊天栏) +world.say("§6[公告] §f服务器将在 5 分钟后重启!"); + +// 单独发送 (仅该玩家可见) +player.directMessage("§a这是一个私密消息"); + +// 动作栏 (快捷栏上方) +player.actionBar("§e当前在线: " + world.querySelectorAll("*").length); + +// 屏幕标题 +player.title("§6§lBOSS名称", "§7这是一个危险的敌人"); + +// 完整标题参数: title, subtitle, fadeIn, stay, fadeOut (单位: ticks) +player.title("§4§l警告", "§c你正在进入危险区域", 10, 60, 10); +``` + +## 1.8 检查清单 + +把你的 `app.ts` 整理一下,最终应该看起来像这样: + +```js +// ═══════════════════════════════════ +// MyTutorial — 基础示例脚本 +// ═══════════════════════════════════ + +console.log("[MyTutorial] 脚本已加载"); + +// 欢迎消息 +world.onPlayerJoin((entity, tick) => { + entity.player.directMessage("§a欢迎!输入 !help 查看命令"); +}); + +// 定时公告 +world.setInterval(() => { + const online = world.querySelectorAll("*").length; + if (online > 0) world.say("§7在线: " + online + " 人"); +}, 6000); + +// 聊天命令 +world.onChat((entity, message, tick) => { + const p = entity.player; + + switch (message) { + case "!help": + p.directMessage("§e命令: §f!hello !time !pos !online !day !clear"); + return false; + case "!hello": + p.directMessage("§e你好," + p.name + "!"); + return false; + case "!time": + p.directMessage("§e时间: §f" + world.time); + return false; + case "!pos": + const pos = p.position; + p.directMessage("§e位置: §f" + Math.floor(pos.x) + " " + Math.floor(pos.y) + " " + Math.floor(pos.z)); + return false; + case "!online": + p.directMessage("§e在线: §f" + world.querySelectorAll("*").length); + return false; + case "!day": + world.time = 1000; + world.say("§e" + p.name + " §f将时间设为白天"); + return false; + case "!clear": + world.clearWeather(); + world.say("§e" + p.name + " §f清除了天气"); + return false; + } + return true; +}); +``` + +## 下一步 + +教程二将介绍玩家操作:传送、物品给予、生命值、经验值、飞行等。 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items.md new file mode 100644 index 0000000..701c457 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/02-player-items.md @@ -0,0 +1,213 @@ +# 教程二:玩家与物品 + +本教程涵盖玩家属性操作、物品给予和背包管理。 + +## 2.1 玩家基本信息 + +```js +world.onPlayerJoin((entity, tick) => { + const p = entity.player; + + // 只读属性 + console.log("玩家名: " + p.name); + console.log("UUID: " + p.userId); + + // entity 上的属性也可以通过 p 访问 + console.log("血量: " + p.hp + "/" + p.maxHp); + console.log("位置: " + p.position); + + // 生命值 + p.maxHp = 40; // 设置最大血量 (20 = 默认) + p.hp = 40; // 回满血 + + // 饱食度 + p.food = 20; + p.saturation = 10; +}); +``` + +## 2.2 移动控制 + +```js +// 速度 (默认 walkSpeed ≈ 0.1) +p.walkSpeed = 0.2; // 走路翻倍 +p.runSpeed = 0.26; // 跑步翻倍 (自动维持 walkSpeed × 1.3) +p.jumpPower = 0.6; // 跳得更高 (默认约 0.42) +p.swimSpeed = 0.3; // 游泳更快 + +// 禁止跳跃 +p.enableJump = false; +// p.enableJump = true; // 恢复 + +// 飞行 +p.canFly = true; +p.flying = true; +p.flySpeed = 0.15; + +// 传送 +p.teleport(new GameVector3(0, 100, 0)); +``` + +## 2.3 游戏模式与维度 + +```js +p.gameMode = "creative"; // survival / creative / adventure / spectator +p.gameMode = 1; // 也可以用数字: 0=生存, 1=创造, 2=冒险, 3=旁观 + +// 跨维度传送 +p.dimension = "minecraft:the_nether"; // 地狱 +p.teleport(new GameVector3(0, 70, 0)); +p.dimension = "minecraft:overworld"; // 主世界 +p.dimension = "minecraft:the_end"; // 末地 +``` + +## 2.4 经验与音效 + +```js +p.xp = 10; // 设为 10 级 +p.addExperienceLevels(3); // 加 3 级 + +// 播放音效 (仅该玩家听到) +p.playSound("minecraft:block.note_block.pling", 1.0, 1.5); +// 参数: (音效ID, 音量 0-1, 音高 0.5-2) + +// 常用音效: +// minecraft:block.note_block.pling 铃铛 +// minecraft:entity.experience_orb.pickup 经验球 +// minecraft:entity.player.levelup 升级 +// minecraft:block.anvil.land 铁砧落地 +// minecraft:entity.ender_dragon.growl 末影龙吼 +``` + +## 2.5 给予物品 + +```js +// 基础物品 +p.giveItem("minecraft:diamond_sword", 1); +p.giveItem("minecraft:golden_apple", 5); +p.giveItem("minecraft:arrow", 64); + +// 带附魔的物品 +p.giveEnchantedItem("minecraft:diamond_sword", 1, { + "minecraft:sharpness": 5, + "minecraft:fire_aspect": 2, + "minecraft:unbreaking": 3, +}); + +// 带自定义名称和描述的物品 +p.giveNamedItem("minecraft:diamond_sword", 1, "§c§l烈焰之刃", [ + "§7绑定: 火焰", + "§e右键: 发射火球", +]); + +p.giveNamedItem("minecraft:gold_ingot", 1, "§6§l通关金牌", [ + "§7天空跑酷锦标赛", + "§e完赛时间: 1:23.450", +]); +``` + +## 2.6 物品栏操作 + +```js +// 获取手持物品 +const held = p.getHeldItem(); +if (held.id !== "minecraft:air") { + p.directMessage("你手持: " + held.id + " x" + held.count); +} + +// 清空背包 (包括盔甲和副手) +p.clearInventory(); +``` + +## 2.7 自定义物品 + +自定义物品通过资源包 + JSON 配置实现,无需修改 Java 代码。 + +**第一步:** 在 `resourcepacks/mypack/items.json` 定义物品: + +```json +{ + "base_item": "minecraft:paper", + "items": { + "magic_wand": { + "minecraft:custom_model_data": 12001, + "minecraft:custom_name": "§d§l魔法杖 §r§5★", + "minecraft:lore": [ + "§7蕴藏着神秘力量的魔法杖", + "", + "§6稀有度: §5史诗" + ], + "minecraft:max_stack_size": 1, + "minecraft:enchantment_glint_override": true, + "minecraft:rarity": "epic" + }, + "energy_drink": { + "minecraft:custom_model_data": 12002, + "minecraft:custom_name": "§b能量饮料", + "minecraft:lore": ["§7恢复少量生命值", "§7§o咕噜咕噜..."], + "minecraft:food": { + "nutrition": 4, + "saturation": 0.6, + "can_always_eat": true, + "eat_seconds": 0.8 + } + } + } +} +``` + +**第二步:** 准备资源包结构(贴图 + 模型 JSON)。 + +**第三步:** 在脚本中加载并给予: + +```js +world.loadCustomItems("mypack"); +// 之后就可以: +p.giveCustomItem("magic_wand", 1); +p.giveCustomItem("energy_drink", 8); +``` + +## 2.8 踢出与管理员 + +```js +// 踢出玩家 +p.kick("你已被移出游戏"); + +// 权限等级 +p.opLevel = 4; // 最高权限 (等同 /op) +console.log(p.opLevel); // 0=普通, 1-4=管理员级别 + +// 以玩家身份执行命令 +p.runCommand("say 大家好"); +``` + +## 2.9 完整示例:新手礼包 + +```js +world.onPlayerJoin((entity, tick) => { + const p = entity.player; + + // 欢迎标题 + p.title("§6§l欢迎来到服务器!", "§7准备开始冒险", 10, 60, 10); + + // 新手礼包 + p.giveItem("minecraft:stone_sword", 1); + p.giveItem("minecraft:stone_pickaxe", 1); + p.giveItem("minecraft:stone_axe", 1); + p.giveItem("minecraft:stone_shovel", 1); + p.giveItem("minecraft:bread", 16); + p.giveItem("minecraft:torch", 16); + + // 命名特殊物品 + p.giveNamedItem("minecraft:shield", 1, "§b§l初心者之盾", [ + "§7只有真正的新手才能拥有的盾牌", + "§7§o它看起来不太结实...", + ]); + + p.directMessage("§a你已收到新手礼包!输入 !help 查看帮助"); +}); +``` + +## 下一步 + +教程三将介绍事件系统:方块交互、实体交互、伤害/死亡事件等。 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 new file mode 100644 index 0000000..38893ec --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/03-events-entities.md @@ -0,0 +1,261 @@ +# 教程三:事件系统与实体操控 + +本教程深入讲解事件回调机制、实体生成与控制、以及计分板/队伍等游戏系统。 + +## 3.1 事件回调基础 + +所有事件通过 `world.onXxx(handler)` 注册,返回 `GameEventHandlerToken`。 + +```js +// 注册事件,拿到 token +const token = world.onTick((info) => { + console.log("Tick: " + info.tick); +}); + +// 取消监听 +token.cancel(); + +// 检查是否活跃 +if (token.active()) { + console.log("回调仍在运行"); +} +``` + +### 事件一览 + +| 注册方法 | 回调参数 | 触发时机 | +|----------|---------|------| +| `world.onTick(fn)` | `(info)` | 每 tick (20次/秒) | +| `world.onPlayerJoin(fn)` | `(entity, tick)` | 玩家加入 | +| `world.onPlayerLeave(fn)` | `(entity, tick)` | 玩家离开 | +| `world.onChat(fn)` | `(entity, message, tick)` | 聊天消息 | +| `world.onBlockActivate(fn)` | `(entity, x, y, z, voxel, tick)` | 右键方块 | +| `world.onVoxelDestroy(fn)` | `(entity, x, y, z, voxel, tick)` | 破坏方块 | +| `world.onBlockPlace(fn)` | `(entity, x, y, z, voxel, voxelId, tick)` | 放置方块 | +| `world.onInteract(fn)` | `(entity, target, tick)` | 右键实体 | +| `world.onEntityDeath(fn)` | `(entity, killer, tick)` | 实体死亡 | +| `world.onEntityDamage(fn)` | `(entity, amount, source, attacker, tick)` | 实体受伤 | +| `world.onPlayerRespawn(fn)` | `(entity, tick)` | 玩家重生 | +| `world.onButtonPressed(fn)` | `(entity, button, tick)` | 按钮按下 | +| `world.onMessage(fn)` | `(from, data)` | 跨脚本消息 | + +## 3.2 方块交互 + +```js +// 右键方块保护 +world.onBlockActivate((entity, x, y, z, voxel, tick) => { + const p = entity.player; + if (voxel === "minecraft:chest" && p.opLevel < 2) { + p.directMessage("§c你没有权限打开这个箱子!"); + // 注意: 右键方块事件无法阻止交互, 仅能检测 + } +}); + +// 记录破坏日志 +world.onVoxelDestroy((entity, x, y, z, voxel, tick) => { + console.log(entity.player.name + " 破坏了 " + voxel + " 在 (" + x + "," + y + "," + z + ")"); +}); + +// 禁止放置特定方块 +world.onBlockPlace((entity, x, y, z, voxel, voxelId, tick) => { + if (voxel === "minecraft:tnt" && entity.player.opLevel < 2) { + // 放置后用 voxels 替换为空气 + voxels.setVoxel(x, y, z, "minecraft:air"); + entity.player.directMessage("§c禁止放置TNT!"); + } +}); +``` + +## 3.3 实体交互与战斗 + +```js +// 死亡奖励 +world.onEntityDeath((entity, killer, tick) => { + if (killer && killer.isPlayer()) { + const p = killer.player; + p.addExperienceLevels(1); + p.actionBar("§e击杀 " + entity.entityType + "! +1 经验等级"); + + // 掉落额外物品 + const pos = entity.position; + world.dropItem(pos, "minecraft:diamond", 1); + } +}); + +// 受伤日志 +world.onEntityDamage((entity, amount, source, attacker, tick) => { + if (attacker && attacker.isPlayer()) { + const p = attacker.player; + p.actionBar("§c造成 " + amount + " 点伤害"); + } +}); + +// 右键实体 +world.onInteract((entity, target, tick) => { + const p = entity.player; + p.directMessage("§e你点击了: §f" + target.entityType); + + // 如果是村民,显示信息 + if (target.entityType === "minecraft:villager") { + p.directMessage("§7这个村民看起来不想说话..."); + } +}); +``` + +## 3.4 实体生成与属性 + +```js +// 生成僵尸 +const zombie = world.spawnEntity("minecraft:zombie", new GameVector3(0, 100, 0)); + +// 自定义属性 +zombie.setNameTag("§c§l精英僵尸"); +zombie.maxHp = 60; +zombie.hp = 60; +zombie.walkSpeed = 0.3; + +// 装备 +zombie.setEquipment("mainhand", "minecraft:iron_sword"); +zombie.setEquipment("head", "minecraft:iron_helmet"); +// 槽位: mainhand / offhand / head(helmet/helm) / chest(chestplate) / legs(leggings) / feet(boots) + +// 掉落概率 +zombie.setDropChance("mainhand", 0.3); +zombie.setDropChance("all", 0); // 全部不掉落 + +// 效果 +zombie.addEffect("minecraft:speed", 99999, 1); // 永久速度 II + +// AI +zombie.setAI(true); // 启用寻路 +``` + +### 使用完整配置生成 + +```js +const entity = world.createEntity({ + type: "minecraft:skeleton", + position: new GameVector3(0, 100, 0), + velocity: new GameVector3(0, 0.5, 0), // 向上弹射 + fixed: false, + gravity: true, + friction: 0.5, + collides: true, + hp: 40, + maxHp: 40, + tags: ["elite", "undead"], +}); + +entity.setEquipment("mainhand", "minecraft:bow"); + +// 设置攻击目标 +entity.setTarget(somePlayerEntity); +entity.clearTarget(); + +// 让生物导航到指定位置 +entity.navigateTo(10, 100, 10, 0.5); + +// 设置死亡回调 +entity.setOnDestroy((e) => { + console.log("精英骷髅已被击败"); +}); +``` + +## 3.5 计分板 + +```js +// 创建计分板 +world.addScoreboard("kills"); +world.addScoreboard("deaths", "deathCount"); // 死亡计数 (自动统计) + +// 设置分数 +world.setScore("Steve", "kills", 5); +world.setScore(entity, "kills", 10); // 也可以用实体对象 + +// 读取 +const kills = world.getScore("Steve", "kills"); + +// 显示在屏幕右侧 +world.showScoreboard("sidebar", "kills"); + +// 显示在 Tab 列表 +world.showScoreboard("list", "deaths"); + +// 列出所有分数 +const scores = world.listScores("kills"); +// [{name: "Steve", value: 5}, {name: "Alex", value: 3}, ...] + +// 清除显示 +world.hideScoreboard("sidebar"); +world.removeScoreboard("kills"); +``` + +### 实战:击杀计数 + +```js +world.addScoreboard("kills"); +world.showScoreboard("sidebar", "kills"); + +world.onEntityDeath((entity, killer, tick) => { + if (killer && killer.isPlayer()) { + const p = killer.player; + const current = world.getScore(p.name, "kills"); + world.setScore(p.name, "kills", current + 1); + p.actionBar("§e击杀: §f" + (current + 1)); + } +}); +``` + +## 3.6 队伍系统 + +```js +// 创建队伍 +world.createTeam("red", "red"); +world.createTeam("blue", "blue"); + +// 划分队伍 +world.onPlayerJoin((entity, tick) => { + const online = world.querySelectorAll("*").length; + const team = online % 2 === 0 ? "red" : "blue"; + world.joinTeam(entity, team); + entity.player.directMessage("§7你加入了 " + team + " 队"); +}); + +// 获取队伍 +const team = world.getTeamOf(entity); +console.log(team); // "red" 或 null + +// 移出队伍 +world.leaveTeam(entity); + +// 删除队伍 +world.removeTeam("red"); +``` + +## 3.7 碰撞与标签 + +```js +// 实体碰撞 +world.onEntityContact((entityA, entityB, tick) => { + // 两个实体开始接触 + if (entityA.isPlayer() && entityB.entityType === "minecraft:zombie") { + entityA.player.actionBar("§c小心僵尸!"); + } +}); + +world.onEntitySeparate((entityA, entityB, tick) => { + // 两个实体分离 +}); + +// 实体标签 +entity.addTag("boss"); +entity.removeTag("elite"); +if (entity.hasTag("boss")) { + // 特殊处理 Boss +} +const tags = entity.tags(); // ["boss", "undead"] +``` + +## 下一步 + +教程四将介绍高级游戏系统:BossBar、计时器、粒子/烟花/闪电、定时任务,以及一个完整的 PvP 小游戏示例。 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 new file mode 100644 index 0000000..9bbd254 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/04-advanced-systems.md @@ -0,0 +1,704 @@ +# 教程四:高级游戏系统 + +本教程涵盖 BossBar、粒子/烟花/闪电、世界边界、抛射物、爆炸等视觉效果,以及跨脚本通信。 + +## 4.1 BossBar 血条 + +BossBar 在屏幕上方显示一个带标题的进度条,常用于 Boss 战或全局倒计时。 + +```js +// 基本用法 +world.showBossbar("boss_hp", "§c§l远古巨龙", 1.0, "red"); + +// 更新进度 +world.showBossbar("boss_hp", "§c§l远古巨龙 §7[50%]", 0.5, "yellow"); + +// 移除 +world.removeBossbar("boss_hp"); +``` + +颜色选项:`"blue"`、`"green"`、`"pink"`、`"purple"`、`"red"`、`"white"`、`"yellow"`。 + +### 实战:Boss 血量同步 + +```js +const bossBarId = "dragon_boss"; + +world.onEntityDamage((entity, amount, source, attacker, tick) => { + if (!entity.hasTag("boss")) return; + + const hpPercent = entity.hp / entity.maxHp; + if (hpPercent <= 0) { + world.removeBossbar(bossBarId); + return; + } + + let color = "green"; + if (hpPercent < 0.3) color = "red"; + else if (hpPercent < 0.6) color = "yellow"; + + world.showBossbar( + bossBarId, + `§c§lBoss §f${entity.nameTag} §7[${Math.ceil(entity.hp)}/${entity.maxHp}]`, + hpPercent, + color, + ); +}); +``` + +### 实战:全局倒计时 + +```js +let timeLeft = 300; // 5 分钟 + +const timerId = world.setInterval(() => { + timeLeft--; + + if (timeLeft <= 0) { + world.removeBossbar("countdown"); + world.clearInterval(timerId); + world.say("§c时间到!"); + return; + } + + const progress = timeLeft / 300; + const mins = Math.floor(timeLeft / 60); + const secs = timeLeft % 60; + const color = progress > 0.3 ? "green" : progress > 0.1 ? "yellow" : "red"; + + world.showBossbar( + "countdown", + `§e剩余时间: §f${mins}:${secs.toString().padStart(2, "0")}`, + progress, + color, + ); +}, 20); // 每秒更新 +``` + +## 4.2 粒子效果 + +```js +// 单点粒子: (类型, x, y, z, 数量, dx, dy, dz, 速度) +world.spawnParticle("minecraft:flame", 0, 100, 0, 20, 0.5, 0.5, 0.5, 0.05); + +// 圆形粒子圈: (x, y, z, 半径, 类型, 数量) +world.spawnParticleCircle(0, 100, 0, 3.0, "minecraft:happy_villager", 30); + +// 常用粒子: +// minecraft:flame 火焰 +// minecraft:cloud 烟雾 +// minecraft:happy_villager 绿色粒子 +// minecraft:witch 紫色粒子 +// minecraft:portal 传送门 +// minecraft:end_rod 末地烛光 +// minecraft:heart 爱心 +// minecraft:note 音符 +// minecraft:dragon_breath 龙息 +// minecraft:angry_villager 愤怒粒子 +``` + +### 实战:Boss 出场特效 + +```js +function bossSpawnEffect(pos) { + // 螺旋上升粒子 + for (let i = 0; i < 40; i++) { + const angle = (i / 40) * Math.PI * 4; + const radius = 2; + const px = pos.x + Math.cos(angle) * radius; + const pz = pos.z + Math.sin(angle) * radius; + const py = pos.y + i * 0.2; + + world.setTimeout(() => { + world.spawnParticle("minecraft:portal", px, py, pz, 3, 0, 0, 0, 0); + }, i * 2); + } + + // 地面圆形粒子 + world.spawnParticleCircle(pos.x, pos.y, pos.z, 3, "minecraft:end_rod", 50); +} +``` + +## 4.3 烟花与闪电 + +```js +// 闪电 (x, y, z, 伤害) +world.strikeLightning(0, 100, 0); // 默认伤害 +world.strikeLightning(0, 100, 0, 10); // 10 点伤害 + +// 烟花 (x, y, z, 颜色, 形状) +world.launchFirework(0, 100, 0, "gold", "large_ball"); +world.launchFirework(pos, "red", "star"); +``` + +烟花形状:`"ball"`、`"large_ball"`、`"star"`、`"creeper"`、`"burst"` + +烟花颜色:`"red"`、`"blue"`、`"green"`、`"yellow"`、`"gold"`、`"white"`、`"aqua"`、`"pink"`、`"purple"` + +### 实战:击杀烟花 + +```js +world.onEntityDeath((entity, killer, tick) => { + if (!killer || !killer.isPlayer()) return; + + const pos = entity.position; + + // Boss 击杀特效 + if (entity.hasTag("boss")) { + world.strikeLightning(pos, 0); // 无伤害闪电,纯视觉效果 + world.setTimeout(() => world.launchFirework(pos.x, pos.y + 2, pos.z, "gold", "large_ball"), 5); + world.setTimeout(() => world.launchFirework(pos.x, pos.y + 2, pos.z, "red", "star"), 10); + world.setTimeout(() => world.launchFirework(pos.x, pos.y + 2, pos.z, "purple", "burst"), 15); + world.say("§6" + killer.player.name + " §f击败了 §c" + entity.nameTag + "§f!"); + } +}); +``` + +## 4.4 世界边界 + +世界边界可以制造动态缩圈效果,配合 PvP 或生存玩法。 + +```js +// 设置边界 +world.setBorderCenter(0, 0); +world.borderSize = 500; +world.setBorderDamage(2); // 边界外每秒伤害 +world.setBorderWarning(10); // 屏幕变红预警距离 + +// 平滑缩圈到 100 格,耗时 120 秒 +world.shrinkBorder(100, 120); + +// 读取当前大小 +console.log(world.borderSize); +``` + +### 实战:缩圈公告 + +```js +function startShrinkCycle() { + const stages = [ + { size: 300, delay: 600, duration: 60 }, + { size: 150, delay: 2400, duration: 90 }, + { size: 50, delay: 4800, duration: 120 }, + ]; + + world.setBorderCenter(0, 0); + world.borderSize = 500; + world.setBorderDamage(1); + world.setBorderWarning(10); + + world.say("§c边界将在 30 秒后开始缩小!"); + + stages.forEach((stage) => { + world.setTimeout(() => { + world.say(`§c边界缩小至 ${stage.size} 格!`); + world.shrinkBorder(stage.size, stage.duration); + }, stage.delay); + }); +} +``` + +## 4.5 抛射物与爆炸 + +```js +// 抛射物: (类型, 起点x, y, z, 目标x, y, z, 速度) +const proj = world.launchProjectile("minecraft:fireball", 0, 100, 0, 10, 100, 10, 2); +// 也可用 pos 重载 +world.launchProjectile("minecraft:arrow", pos, targetPos, 3); + +// 爆炸: (x, y, z, 威力, 是否引火) +world.explode(0, 100, 0, 4); // 威力 4,不引火 +world.explode(0, 100, 0, 8, true); // 威力 8,引火 +``` + +### 实战:Boss 技能——火球连射 + +```js +function bossFireballAttack(boss) { + const players = world.querySelectorAll("*"); + if (players.length === 0) return; + + // 向每个玩家发射火球 + players.forEach((player, i) => { + world.setTimeout(() => { + const bossPos = boss.position; + const targetPos = player.position; + world.launchProjectile( + "minecraft:fireball", + bossPos.x, bossPos.y + 1, bossPos.z, + targetPos.x, targetPos.y, targetPos.z, + 1.5, + ); + }, i * 200); // 间隔 200 ticks + }); +} + +// 每 10 秒攻击一次 +world.setInterval(() => { + const bosses = world.querySelectorAll(".boss"); + bosses.forEach((boss) => bossFireballAttack(boss)); +}, 200); +``` + +## 4.6 音效 + +```js +// 全局音效(所有玩家听到) +world.playSound("minecraft:block.note_block.pling", 0, 100, 0, 1.0, 1.5); + +// 仅某个玩家听到 +player.playSound("minecraft:block.note_block.pling", 1.0, 1.5); + +// 常用音效: +// minecraft:block.note_block.pling 铃铛 +// minecraft:entity.experience_orb.pickup 经验球 +// minecraft:entity.player.levelup 升级 +// minecraft:entity.ender_dragon.growl 末影龙吼 +// minecraft:entity.wither.spawn 凋零生成 +// minecraft:entity.lightning_bolt.thunder 雷鸣 +// minecraft:block.anvil.land 铁砧落地 +``` + +## 4.7 跨脚本通信 + +不同脚本项目之间可以通过 `sendMessage` / `onMessage` 通信。 + +脚本 A(发送方): +```js +// 发送给指定项目 +world.sendMessage("minigame_hub", { action: "start", level: 2 }); + +// 广播给所有项目 +world.sendMessage("*", { action: "reload_config" }); +``` + +脚本 B(接收方): +```js +world.onMessage((from, data) => { + console.log("收到来自 " + from + " 的消息:", data); + + if (data.action === "start") { + startGame(data.level); + } else if (data.action === "reload_config") { + reloadConfig(); + } +}); +``` + +## 4.8 射线检测 + +```js +// 从实体眼睛位置向下检测 +const down = new GameVector3(0, -1, 0); +const result = world.raycast(player.position, down, 50); + +if (result.hit) { + console.log("命中方块:", result.voxel, "距离:", result.distance); + if (result.entity) { + console.log("命中实体:", result.entity.entityType); + } +} +``` + +返回值:`{ hit, x, y, z, normalX, normalY, normalZ, distance, entity, voxel }` + +## 4.9 完整示例:PvP 竞技场 + +一个完整的队伍 PvP 小游戏,整合了事件、BossBar、粒子、烟花、边界缩圈、抛射物等系统。 + +```js +// ═══════════════════════════════════════════ +// PvP 竞技场 — 完整示例 +// ═══════════════════════════════════════════ + +console.log("[PvPArena] 脚本已加载"); + +// ── 配置 ── +const ARENA_CENTER = new GameVector3(0, 70, 0); +const ARENA_RADIUS = 80; +const GAME_DURATION = 300; // 300 秒 +const SHRINK_START = 120; // 120 秒后开始缩圈 +const MAX_PLAYERS = 16; + +// ── 游戏状态 ── +let gameState = "waiting"; // waiting | starting | playing | ending +let gameTimer = null; +let lobbyTimer = null; +let playersReady = 0; +let redSpawn = new GameVector3(-20, 70, 0); +let blueSpawn = new GameVector3(20, 70, 0); + +// ── 初始化 ── +world.setGameRule("keepInventory", false); +world.setGameRule("doMobSpawning", false); +world.clearWeather(); +world.time = 6000; // 正午 +world.timeScale = 0; // 冻结时间 + +// 创建计分板 +world.addScoreboard("pvp_kills"); +world.addScoreboard("pvp_score"); +world.showScoreboard("sidebar", "pvp_score"); + +// 创建队伍 +world.createTeam("red", "red"); +world.createTeam("blue", "blue"); + +// ── 聊天命令 ── +world.onChat((entity, message, tick) => { + const p = entity.player; + + switch (message) { + case "!join": + if (gameState !== "waiting") { + p.directMessage("§c游戏已开始,无法加入"); + return false; + } + if (playersReady >= MAX_PLAYERS) { + p.directMessage("§c竞技场已满"); + return false; + } + playersReady++; + p.directMessage("§a你已加入竞技场!当前 " + playersReady + "/" + MAX_PLAYERS + " 人"); + + // 当足够人数后开始倒计时 + if (playersReady >= 2 && gameState === "waiting") { + startLobbyCountdown(); + } + return false; + + case "!leave": + if (gameState === "waiting") { + playersReady = Math.max(0, playersReady - 1); + p.directMessage("§7你已退出竞技场"); + } + return false; + + case "!pvp": + p.directMessage("§e── PvP 竞技场帮助 ──"); + p.directMessage("§f!join §7- 加入竞技场"); + p.directMessage("§f!leave §7- 退出等待"); + p.directMessage("§f当前状态: " + gameState + " | 玩家: " + playersReady); + return false; + } + return true; +}); + +// ── 大厅倒计时 ── +function startLobbyCountdown() { + gameState = "starting"; + let countdown = 30; + + lobbyTimer = world.setInterval(() => { + countdown--; + + if (countdown <= 0) { + world.clearInterval(lobbyTimer); + startGame(); + } else if (countdown <= 5) { + world.say("§e游戏将在 §c" + countdown + " §e秒后开始!"); + world.playSound("minecraft:block.note_block.pling", ARENA_CENTER, 1.0, 1.5); + } else if (countdown % 10 === 0) { + world.say("§7游戏将在 " + countdown + " 秒后开始..."); + } + }, 20); +} + +// ── 游戏开始 ── +function startGame() { + gameState = "playing"; + world.timeScale = 1; + + const allPlayers = world.querySelectorAll("*"); + + // 分配队伍 + allPlayers.forEach((entity, i) => { + const p = entity.player; + + // 清空背包 + p.clearInventory(); + p.hp = 20; + p.maxHp = 20; + p.food = 20; + + if (i % 2 === 0) { + world.joinTeam(entity, "red"); + p.teleport(redSpawn); + p.directMessage("§c你加入了 §l红队"); + p.setPlayerListName("§c[红] §f" + p.name); + // 红队装备 + p.giveItem("minecraft:iron_sword", 1); + p.giveItem("minecraft:bow", 1); + p.giveItem("minecraft:arrow", 32); + p.giveItem("minecraft:golden_apple", 3); + } else { + world.joinTeam(entity, "blue"); + p.teleport(blueSpawn); + p.directMessage("§9你加入了 §l蓝队"); + p.setPlayerListName("§9[蓝] §f" + p.name); + // 蓝队装备 + p.giveItem("minecraft:iron_sword", 1); + p.giveItem("minecraft:crossbow", 1); + p.giveItem("minecraft:arrow", 32); + p.giveItem("minecraft:golden_apple", 3); + } + + p.giveItem("minecraft:cooked_beef", 16); + p.directMessage("§7竞技场半径: " + ARENA_RADIUS + " 格"); + + // 出场粒子效果 + const pos = p.position; + world.spawnParticleCircle(pos.x, pos.y, pos.z, 1.5, "minecraft:happy_villager", 20); + world.playSound("minecraft:entity.player.levelup", pos, 1.0, 1.0); + }); + + // 边界初始化 + world.setBorderCenter(ARENA_CENTER.x, ARENA_CENTER.z); + world.borderSize = ARENA_RADIUS * 2; + world.setBorderDamage(1); + world.setBorderWarning(5); + + // 全局公告 + world.say("§c§l⚔ 竞技场开始!击杀敌人获取积分 ⚔"); + world.playSound("minecraft:entity.ender_dragon.growl", ARENA_CENTER, 1.0, 1.0); + + // 倒计时显示 + let timeRemaining = GAME_DURATION; + const gameTimerId = world.setInterval(() => { + timeRemaining--; + + const progress = timeRemaining / GAME_DURATION; + const mins = Math.floor(timeRemaining / 60); + const secs = timeRemaining % 60; + const color = progress > 0.3 ? "green" : progress > 0.1 ? "yellow" : "red"; + + world.showBossbar( + "pvp_timer", + `§e战斗剩余: §f${mins}:${secs.toString().padStart(2, "0")}`, + progress, + color, + ); + + // 缩圈触发 + if (timeRemaining === SHRINK_START) { + world.say("§c边界开始缩小!向中心移动!"); + world.shrinkBorder(20, 60); + world.playSound("minecraft:block.note_block.bass", ARENA_CENTER, 1.0, 1.0); + } + + if (timeRemaining === 60) { + world.say("§c最后一分钟!"); + } + + if (timeRemaining === 30) { + // 向中心召唤闪电 + world.strikeLightning(ARENA_CENTER.x, ARENA_CENTER.y, ARENA_CENTER.z, 0); + } + + if (timeRemaining <= 0) { + world.clearInterval(gameTimerId); + endGame(); + } + }, 20); + gameTimer = gameTimerId; + + // 定时奖励空投 + world.setInterval(() => { + if (gameState !== "playing") return; + + const angle = Math.random() * Math.PI * 2; + const dist = Math.random() * ARENA_RADIUS * 0.6; + const dropX = ARENA_CENTER.x + Math.cos(angle) * dist; + const dropZ = ARENA_CENTER.z + Math.sin(angle) * dist; + + // 空投降落特效 + world.strikeLightning(dropX, ARENA_CENTER.y + 30, dropZ, 0); + world.setTimeout(() => { + world.dropItem(dropX, ARENA_CENTER.y + 1, dropZ, "minecraft:ender_pearl", 2); + world.dropItem(dropX, ARENA_CENTER.y + 1, dropZ, "minecraft:golden_apple", 2); + world.launchFirework(dropX, ARENA_CENTER.y + 3, dropZ, "yellow", "ball"); + world.say("§e☄ 空投已降落!"); + }, 20); + }, 1200); // 每 60 秒 +} + +// ── 击杀计分 ── +world.onEntityDeath((entity, killer, tick) => { + if (gameState !== "playing") return; + + // 玩家击杀 + if (killer && killer.isPlayer() && entity.isPlayer()) { + const kp = killer.player; + const team = world.getTeamOf(killer); + + // 增加击杀数 + const currentKills = world.getScore(kp.name, "pvp_kills"); + world.setScore(kp.name, "pvp_kills", currentKills + 1); + + // 团队分数 + const teamScore = world.getScore(team, "pvp_score"); + world.setScore(team, "pvp_score", teamScore + 1); + + // 个人奖励 + kp.addExperienceLevels(2); + kp.playSound("minecraft:entity.player.levelup", 1.0, 1.0); + + // 击杀特效 + const pos = entity.position; + world.spawnParticleCircle(pos.x, pos.y, pos.z, 2, "minecraft:angry_villager", 15); + world.launchFirework(pos.x, pos.y + 1, pos.z, "red", "star"); + + // 全局击杀播报 + const killedTeam = world.getTeamOf(entity); + if (killedTeam !== team) { + world.say( + `§c[${team}] §f${kp.name} §7击杀了 §f[${killedTeam}] ${entity.player.name} §7(${currentKills + 1} 杀)` + ); + } + } +}); + +// ── 死亡处理 ── +world.onPlayerRespawn((entity, tick) => { + if (gameState !== "playing") return; + + const team = world.getTeamOf(entity); + const p = entity.player; + + if (team === "red") { + p.teleport(redSpawn); + } else if (team === "blue") { + p.teleport(blueSpawn); + } + + // 重生后补装备 + p.giveItem("minecraft:iron_sword", 1); + p.giveItem("minecraft:cooked_beef", 4); + p.addEffect("minecraft:regeneration", 100, 1, true); + p.addEffect("minecraft:resistance", 100, 2, true); // 短暂无敌 +}); + +// ── 玩家离开处理 ── +world.onPlayerLeave((entity, tick) => { + if (gameState === "waiting" || gameState === "starting") { + playersReady = Math.max(0, playersReady - 1); + } +}); + +// ── 游戏结束 ── +function endGame() { + gameState = "ending"; + world.removeBossbar("pvp_timer"); + + // 统计分数 + const redScore = world.getScore("red", "pvp_score"); + const blueScore = world.getScore("blue", "pvp_score"); + + let winner = ""; + let color = ""; + + if (redScore > blueScore) { + winner = "红队"; + color = "c"; + } else if (blueScore > redScore) { + winner = "蓝队"; + color = "9"; + } else { + winner = ""; + color = "e"; + } + + // 胜利公告 + const allPlayers = world.querySelectorAll("*"); + allPlayers.forEach((entity) => { + const p = entity.player; + p.title(`§${color}§l${winner ? winner + " 获胜!" : "平局!"}`, "§7竞技场结束", 10, 80, 10); + p.playSound( + "minecraft:ui.toast.challenge_complete", + 1.0, + 1.0, + ); + }); + + if (winner) { + world.say( + `§${color}§l🏆 ${winner} §f以 §e${Math.max(redScore, blueScore)} §f分获胜!` + ); + } else { + world.say(`§e§l🤝 平局!双方各得 §f${redScore} §e分`); + } + + world.say("§7红队: " + redScore + " 分 | 蓝队: " + blueScore + " 分"); + + // 烟花庆祝 + for (let i = 0; i < 10; i++) { + world.setTimeout(() => { + const colors = ["red", "gold", "green", "blue", "purple"]; + const shapes = ["ball", "large_ball", "star", "burst"]; + const c = colors[Math.floor(Math.random() * colors.length)]; + const s = shapes[Math.floor(Math.random() * shapes.length)]; + world.launchFirework( + ARENA_CENTER.x + (Math.random() - 0.5) * 20, + ARENA_CENTER.y + Math.random() * 5, + ARENA_CENTER.z + (Math.random() - 0.5) * 20, + c, + s, + ); + }, i * 400); + } + + // 30 秒后重置 + world.setTimeout(() => { + resetGame(); + }, 600); +} + +function resetGame() { + gameState = "waiting"; + playersReady = 0; + + world.removeScoreboard("pvp_kills"); + world.removeScoreboard("pvp_score"); + world.addScoreboard("pvp_kills"); + world.addScoreboard("pvp_score"); + world.hideScoreboard("sidebar"); + world.showScoreboard("sidebar", "pvp_score"); + + world.removeTeam("red"); + world.removeTeam("blue"); + world.createTeam("red", "red"); + world.createTeam("blue", "blue"); + + world.clearWeather(); + world.time = 6000; + + // 恢复边界 + world.setBorderCenter(0, 0); + world.borderSize = 60000000; + + world.say("§a竞技场已重置,输入 §f!join §a加入下一局"); +} +``` + +## 4.10 小游戏设计模式总结 + +| 系统 | 用途 | 关键 API | +|------|------|----------| +| BossBar | 倒计时、Boss 血量、全局进度 | `world.showBossbar()` / `removeBossbar()` | +| 记分板 | 击杀数、积分、排行榜 | `world.addScoreboard()` / `setScore()` / `showScoreboard()` | +| 队伍 | 分队、友好标记 | `world.createTeam()` / `joinTeam()` | +| 世界边界 | 缩圈、毒圈 | `world.borderSize` / `shrinkBorder()` | +| 粒子 | 出/退场特效、区域标记 | `world.spawnParticle()` / `spawnParticleCircle()` | +| 烟花 | 庆祝、击杀特效 | `world.launchFirework()` | +| 闪电 | 警告、空投标记 | `world.strikeLightning()` | +| 音效 | 提示、氛围 | `world.playSound()` / `player.playSound()` | +| 定时器 | 倒计时、阶段推进、定时事件 | `world.setInterval()` / `setTimeout()` | +| 跨脚本消息 | 模块间通信 | `world.sendMessage()` / `onMessage()` | + +## 下一步 + +## 下一步 + +教程五收集了更多独立实用示例:聊天命令、传送系统、防破坏、波次刷怪、赛跑检查点、捉迷藏、计分板应用等。 + +更多 API 细节请参考 `docs/api/` 中的完整 API 文档。 diff --git a/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md new file mode 100644 index 0000000..601c241 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/docs/tutorial/05-examples.md @@ -0,0 +1,981 @@ +# 教程五:实用示例集 + +本文收集了各种独立、可直接使用的小脚本示例,按场景分类。 + +## 5.1 聊天命令 + +### 弹幕颜色命令 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + const colors = { "r": "c", "g": "a", "b": "9", "y": "e", "p": "d", "w": "f" }; + const match = message.match(/^!(\w)\s(.+)/); + + if (match && colors[match[1]]) { + world.say(`§${colors[match[1]]}[${p.name}] §f${match[2]}`); + return false; // 阻止原始消息 + } + return true; +}); +// 用法: !r 大家好 → 红色发送 +``` + +### 新手帮助命令 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + + switch (message) { + case "!help": + p.directMessage("§6── 服务器命令帮助 ──"); + p.directMessage("§f!home §7- 传送回家"); + p.directMessage("§f!shop §7- 打开商店"); + p.directMessage("§f!tpa <玩家> §7- 请求传送"); + p.directMessage("§f!ignore <玩家> §7- 屏蔽玩家"); + p.directMessage("§f!vote §7- 投票换图"); + return false; + } + return true; +}); +``` + +### 私聊系统 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + const match = message.match(/^!msg\s+(\S+)\s+(.+)/); + + if (match) { + const targetName = match[1]; + const msg = match[2]; + const targets = world.querySelectorAll("*"); + let found = false; + + targets.forEach((e) => { + if (e.player.name.toLowerCase() === targetName.toLowerCase()) { + e.player.directMessage(`§d[${p.name} → 你] §f${msg}`); + p.directMessage(`§d[你 → ${e.player.name}] §f${msg}`); + found = true; + } + }); + + if (!found) p.directMessage(`§c玩家 ${targetName} 不在线`); + return false; + } + return true; +}); +``` + +## 5.2 传送系统 + +### 家传送 + +```js +// 玩家用属性存储家坐标 +world.onChat((entity, message, tick) => { + const p = entity.player; + + switch (message) { + case "!sethome": + entity.homeX = entity.position.x; + entity.homeY = entity.position.y; + entity.homeZ = entity.position.z; + p.directMessage("§a家已设置!输入 !home 回家"); + return false; + + case "!home": + if (entity.homeX === undefined) { + p.directMessage("§c你还没有设置家!先输入 !sethome"); + return false; + } + p.teleport(new GameVector3(entity.homeX, entity.homeY, entity.homeZ)); + p.directMessage("§a已传送回家!"); + return false; + } + return true; +}); +``` + +### 坐标分享 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!sharepos") { + const pos = entity.position; + world.say( + `§e${p.name} §f的坐标: §a[${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)}]` + ); + return false; + } + return true; +}); +``` + +### 随机传送 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!rtp") { + const range = 500; + const x = (Math.random() - 0.5) * range * 2; + const z = (Math.random() - 0.5) * range * 2; + p.teleport(new GameVector3(x, 150, z)); + p.directMessage(`§a已随机传送到 (${Math.floor(x)}, ~, ${Math.floor(z)})`); + return false; + } + return true; +}); +``` + +### 传送请求 (TPA) + +```js +// 存储待处理的传送请求 +// tpRequest 的属性: { fromName, fromEntity } + +world.onChat((entity, message, tick) => { + const p = entity.player; + const match = message.match(/^!tpa\s+(\S+)/); + + if (match) { + const targetName = match[1]; + const targets = world.querySelectorAll("*"); + + targets.forEach((target) => { + if (target.player.name.toLowerCase() === targetName.toLowerCase()) { + // 在目标上存储请求 + target.tpRequest = { + fromName: p.name, + fromEntity: entity, + }; + target.player.directMessage( + `§e${p.name} §f想传送到你这里!输入 §a!tpaccept §f接受` + ); + p.directMessage(`§a已向 ${targetName} 发送传送请求`); + } + }); + return false; + } + + if (message === "!tpaccept") { + const req = entity.tpRequest; + if (!req) { + p.directMessage("§c没有待处理的传送请求"); + return false; + } + req.fromEntity.player.teleport(entity.position); + req.fromEntity.player.directMessage(`§a已传送到 ${p.name} 身边`); + p.directMessage(`§a${req.fromName} 已传送到你身边`); + entity.tpRequest = undefined; + return false; + } + + return true; +}); +``` + +## 5.3 公告与定时消息 + +### 定时公告轮播 + +```js +const announcements = [ + "§e欢迎来到服务器!输入 !help 查看帮助", + "§b遵守服务器规则,文明游戏", + "§a遇到问题请联系管理员", + "§d服务器每天凌晨 4 点重启", +]; + +let index = 0; +world.setInterval(() => { + const online = world.querySelectorAll("*").length; + if (online > 0) { + world.say(`§6[公告] §f${announcements[index]}`); + index = (index + 1) % announcements.length; + } +}, 6000); // 每 5 分钟一条 +``` + +### 自动重启提醒 + +```js +// 每 2 小时提醒一次 +world.setInterval(() => { + world.say("§4[系统] §c服务器将在 5 分钟后自动重启!"); + world.playSound("minecraft:block.note_block.bass", new GameVector3(0, 100, 0), 1.0, 1.0); + + // 4 分钟后 1 分钟警告 + world.setTimeout(() => { + world.say("§4[系统] §c距离重启还有 1 分钟!"); + }, 4800); + + // 5 分钟后执行重启命令 + world.setTimeout(() => { + world.say("§4[系统] §c服务器正在重启..."); + world.runCommand("stop"); + }, 6000); +}, 144000); // 7200 秒 +``` + +## 5.4 防破坏与保护 + +### 出生点保护 + +```js +const SPAWN = new GameVector3(0, 70, 0); +const PROTECT_RADIUS = 50; + +// 阻止破坏 +world.onVoxelDestroy((entity, x, y, z, voxel, tick) => { + const dx = x - SPAWN.x; + const dz = z - SPAWN.z; + if (Math.sqrt(dx * dx + dz * dz) < PROTECT_RADIUS) { + if (entity.player.opLevel < 2) { + entity.player.directMessage("§c出生点范围禁止破坏方块!"); + // 注:事件无法阻止操作,仅作提示 + } + } +}); + +// 阻止放置 +world.onBlockPlace((entity, x, y, z, voxel, voxelId, tick) => { + const dx = x - SPAWN.x; + const dz = z - SPAWN.z; + if (Math.sqrt(dx * dx + dz * dz) < PROTECT_RADIUS) { + if (entity.player.opLevel < 2) { + voxels.setVoxel(x, y, z, "minecraft:air"); + entity.player.directMessage("§c出生点范围禁止放置方块!"); + } + } +}); +``` + +### 禁用物品 + +```js +const BANNED_ITEMS = ["minecraft:tnt", "minecraft:lava_bucket", "minecraft:flint_and_steel"]; + +world.onBlockPlace((entity, x, y, z, voxel, voxelId, tick) => { + if (BANNED_ITEMS.includes(voxel) && entity.player.opLevel < 2) { + voxels.setVoxel(x, y, z, "minecraft:air"); + entity.player.directMessage(`§c物品 ${voxel} 禁止放置!`); + } +}); +``` + +## 5.5 实体小游戏 + +### 波次刷怪 + +```js +const SPAWN_POS = new GameVector3(0, 70, 20); +let wave = 0; +let mobsAlive = 0; + +function startWave() { + wave++; + const count = wave * 3; // 每波增加 3 只 + mobsAlive = count; + + world.say(`§c§l⚔ 第 ${wave} 波开始!§f生成 ${count} 只僵尸`); + + for (let i = 0; i < count; i++) { + world.setTimeout(() => { + const x = SPAWN_POS.x + (Math.random() - 0.5) * 10; + const z = SPAWN_POS.z + (Math.random() - 0.5) * 10; + const zombie = world.spawnEntity("minecraft:zombie", new GameVector3(x, 70, z)); + zombie.setNameTag(`§7[第${wave}波] 僵尸`); + zombie.maxHp = 20 + wave * 5; + zombie.hp = zombie.maxHp; + zombie.setAI(true); + zombie.addTag("wave_mob"); + }, i * 200); // 逐个生成,间隔 200 ticks + } +} + +// 击杀检测 +world.onEntityDeath((entity, killer, tick) => { + if (!entity.hasTag("wave_mob")) return; + mobsAlive--; + + if (killer && killer.isPlayer()) { + killer.player.actionBar(`§a击杀! 剩余: ${mobsAlive}`); + } + + if (mobsAlive <= 0) { + world.say("§a§l✔ 第 " + wave + " 波清除!"); + world.setTimeout(() => startWave(), 200); // 10 秒后下一波 + } +}); + +// 命令启动 +world.onChat((entity, message, tick) => { + if (message === "!wave" && entity.player.opLevel >= 2) { + wave = 0; + startWave(); + return false; + } + return true; +}); +``` + +### 赛跑检查点 + +```js +const checkpoints = [ + new GameVector3(0, 70, 50), + new GameVector3(50, 75, 50), + new GameVector3(50, 80, 0), + new GameVector3(0, 85, 0), +]; + +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!race") { + entity.raceCheckpoint = 0; + entity.raceStart = world.currentTick; + p.teleport(checkpoints[0]); + p.giveItem("minecraft:leather_boots", 1); + p.addEffect("minecraft:speed", 99999, 2); + p.directMessage("§e到达每个检查点后输入 !cp 前往下一站"); + return false; + } + + if (message === "!cp") { + const cp = entity.raceCheckpoint || 0; + const pos = entity.position; + + // 检查是否在检查点 5 格范围内 + const target = checkpoints[cp]; + const dx = pos.x - target.x; + const dy = pos.y - target.y; + const dz = pos.z - target.z; + const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist > 5) { + p.directMessage(`§c你还没有到达检查点 ${cp + 1}!距离: ${Math.floor(dist)} 格`); + return false; + } + + entity.raceCheckpoint = cp + 1; + + if (cp + 1 >= checkpoints.length) { + // 完赛 + const elapsed = Math.floor((world.currentTick - entity.raceStart) / 20); + const mins = Math.floor(elapsed / 60); + const secs = elapsed % 60; + world.say( + `§6🏆 ${p.name} §f完成了赛跑!用时 §e${mins}:${secs.toString().padStart(2, "0")}` + ); + p.playSound("minecraft:ui.toast.challenge_complete", 1.0, 1.0); + world.launchFirework(pos.x, pos.y + 2, pos.z, "gold", "large_ball"); + p.clearEffects(); + } else { + p.directMessage(`§a到达检查点 ${cp + 1}!下一站→`); + p.playSound("minecraft:block.note_block.pling", 1.0, 1.5); + } + return false; + } + return true; +}); +``` + +### 隐藏玩法 (捉迷藏) + +```js +let seeker = null; + +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!seek" && !seeker) { + seeker = entity; + p.teleport(new GameVector3(0, 70, 0)); + p.giveItem("minecraft:diamond_sword", 1); + p.addEffect("minecraft:speed", 99999, 1); + p.addEffect("minecraft:glowing", 99999, 0); + p.directMessage("§c你是鬼!找到所有人!"); + world.say(`§c${p.name} 成为了鬼!快躲起来!`); + return false; + } + + if (message === "!hide" && entity !== seeker) { + p.giveItem("minecraft:leather_helmet", 1); + p.addEffect("minecraft:invisibility", 99999, 0, true); + p.directMessage("§a你已隐藏!鬼要来找你了!"); + return false; + } + + return true; +}); + +world.onEntityDeath((entity, killer, tick) => { + if (killer === seeker && entity.isPlayer() && entity !== seeker) { + entity.player.directMessage("§c你被鬼抓住了!"); + world.say(`§c${entity.player.name} 被鬼抓住了!`); + + const remaining = world.querySelectorAll("*").filter( + (e) => e !== seeker && !e.player.dead + ).length; + + if (remaining <= 0) { + world.say("§6👻 鬼赢了!所有人都被找到了!"); + seeker = null; + } + } +}); +``` + +## 5.6 物品与装备 + +### 彩色装备发放 + +```js +const ARMOR_COLORS = { + "red": ["minecraft:red_wool", "minecraft:red_concrete"], + "blue": ["minecraft:blue_wool", "minecraft:blue_concrete"], + "green": ["minecraft:green_wool", "minecraft:lime_concrete"], + "yellow": ["minecraft:yellow_wool", "minecraft:yellow_concrete"], +}; + +world.onChat((entity, message, tick) => { + const p = entity.player; + const color = ARMOR_COLORS[message.replace("!", "")]; + + if (color) { + p.clearInventory(); + p.giveItem("minecraft:iron_sword", 1); + p.giveItem("minecraft:bow", 1); + p.giveItem("minecraft:arrow", 64); + p.giveNamedItem("minecraft:leather_helmet", 1, `§${message[1]}头盔`, []); + p.giveNamedItem("minecraft:leather_chestplate", 1, `§${message[1]}胸甲`, []); + p.giveNamedItem("minecraft:leather_leggings", 1, `§${message[1]}护腿`, []); + p.giveNamedItem("minecraft:leather_boots", 1, `§${message[1]}靴子`, []); + p.giveItem("minecraft:golden_apple", 8); + p.directMessage(`§a已发放 ${message} 色装备!`); + return false; + } + return true; +}); +// 用法: !red !blue !green !yellow +``` + +### 物品兑换 + +```js +const EXCHANGE = { + "64 minecraft:emerald": { id: "minecraft:diamond_sword", count: 1, name: "钻石剑" }, + "32 minecraft:diamond": { id: "minecraft:netherite_ingot", count: 2, name: "下界合金锭" }, + "16 minecraft:gold_ingot": { id: "minecraft:ender_pearl", count: 4, name: "末影珍珠" }, +}; + +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!shop") { + p.directMessage("§6── 兑换商店 ──"); + for (const [cost, reward] of Object.entries(EXCHANGE)) { + p.directMessage(`§f${cost} §7→ §f${reward.count}x ${reward.name}`); + } + p.directMessage("§7输入 !buy <编号> 购买"); + return false; + } + + return true; +}); +``` + +### 蹦极跳 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!bungee") { + const pos = entity.position; + // 向上弹射 + entity.velocity.set(0, 4, 0); + p.addEffect("minecraft:slow_falling", 160, 0, true); + p.playSound("minecraft:entity.breeze.wind_burst", 1.0, 1.5); + world.spawnParticle("minecraft:cloud", pos.x, pos.y, pos.z, 30, 1, 0.5, 1, 0.02); + return false; + } + return true; +}); +``` + +## 5.7 可视化特效 + +### 玩家登录/退出特效 + +```js +world.onPlayerJoin((entity, tick) => { + const pos = entity.position; + world.say(`§a[+] §e${entity.player.name} §f加入了游戏`); + world.playSound("minecraft:block.note_block.pling", pos, 1.0, 1.5); + world.spawnParticleCircle(pos.x, pos.y, pos.z, 2, "minecraft:happy_villager", 20); + world.launchFirework(pos.x, pos.y + 2, pos.z, "green", "ball"); +}); + +world.onPlayerLeave((entity, tick) => { + const pos = entity.position; + world.say(`§c[-] §e${entity.player.name} §f离开了游戏`); + world.playSound("minecraft:block.note_block.bass", pos, 1.0, 1.0); + world.spawnParticle("minecraft:cloud", pos.x, pos.y, pos.z, 15, 0.5, 0.5, 0.5, 0.02); +}); +``` + +### 区域粒子标记 + +```js +// 在指定区域持续显示粒子边框 +function markArea(cx, cy, cz, radius, particleType, interval) { + return world.setInterval(() => { + const segments = 16; + for (let i = 0; i < segments; i++) { + const angle1 = (i / segments) * Math.PI * 2; + const angle2 = ((i + 0.5) / segments) * Math.PI * 2; + world.spawnParticle( + particleType, + cx + Math.cos(angle1) * radius, cy, cz + Math.sin(angle1) * radius, + 1, 0, 0, 0, 0, + ); + } + }, interval); +} + +// 用法: 在竞技场周围显示火焰环 +// markArea(0, 70, 0, 10, "minecraft:flame", 10); +``` + +### 技能冷却流光 + +```js +function cooldownIndicator(player) { + const p = player.player || player; + const pos = player.position; + + // 脚下粒子圈 + world.spawnParticleCircle(pos.x, pos.y - 0.9, pos.z, 0.8, "minecraft:end_rod", 8); + p.playSound("minecraft:block.note_block.bell", 0.3, 2.0); +} + +// 绑定聊天命令触发的技能 +world.onChat((entity, message, tick) => { + if (message === "!skill") { + const now = world.currentTick; + + // 5 秒冷却 + if (entity.skillCooldown && now - entity.skillCooldown < 100) { + const remain = Math.ceil((100 - (now - entity.skillCooldown)) / 20); + entity.player.directMessage(`§c技能冷却中... ${remain} 秒`); + return false; + } + + entity.skillCooldown = now; + + // 自身周围爆炸粒子 + const pos = entity.position; + world.spawnParticleCircle(pos.x, pos.y, pos.z, 3, "minecraft:witch", 40); + world.explode(pos.x, pos.y, pos.z, 2, false); + entity.player.addEffect("minecraft:strength", 100, 1); + entity.player.directMessage("§6技能释放!力量 II 持续 5 秒"); + + return false; + } + return true; +}); +``` + +## 5.8 计分板应用 + +### 在线时长排行榜 + +```js +world.addScoreboard("playtime", "dummy"); +world.showScoreboard("sidebar", "playtime"); + +// 每 60 秒更新一次 +world.setInterval(() => { + const players = world.querySelectorAll("*"); + players.forEach((entity) => { + const current = world.getScore(entity.player.name, "playtime"); + world.setScore(entity.player.name, "playtime", current + 1); + }); +}, 1200); + +// 玩家加入时初始化 +world.onPlayerJoin((entity, tick) => { + world.setScore(entity.player.name, "playtime", 0); + entity.player.setPlayerListName( + "§7[§f" + entity.player.name + "§7]" + ); +}); +``` + +### 死亡排行榜 + +```js +world.addScoreboard("deaths", "deathCount"); // MC 自动统计死亡 +world.showScoreboard("sidebar", "deaths"); +``` + +### 自定义货币系统 + +```js +world.addScoreboard("coins", "dummy"); + +world.onChat((entity, message, tick) => { + const p = entity.player; + const coins = () => world.getScore(p.name, "coins"); + + switch (message) { + case "!coins": + p.directMessage("§e你的金币: §6" + coins() + " ⛀"); + return false; + + case "!daily": + // 每日签到 + if (entity.lastDaily) { + const dayInTicks = 24000; + if (world.currentTick - entity.lastDaily < dayInTicks) { + const remain = Math.ceil((dayInTicks - (world.currentTick - entity.lastDaily)) / 20 / 60); + p.directMessage(`§c签到冷却中,还需等待 ${remain} 分钟`); + return false; + } + } + entity.lastDaily = world.currentTick; + world.setScore(p.name, "coins", coins() + 100); + p.directMessage("§a签到成功!+100 金币"); + p.playSound("minecraft:entity.experience_orb.pickup", 1.0, 1.0); + return false; + } + return true; +}); +``` + +## 5.9 队伍应用 + +### 队伍聊天前缀 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + const team = world.getTeamOf(entity) || ""; + + const teamPrefix = { + "red": "§c[红]", + "blue": "§9[蓝]", + }[team] || "§7"; + + // 不是命令的正常消息,加队伍前缀 + if (!message.startsWith("!")) { + world.say(`${teamPrefix}§f${p.name}: ${message}`); + } + return true; +}); +``` + +### PvP 模式切换 + +```js +let pvpEnabled = false; + +world.onChat((entity, message, tick) => { + if (message === "!pvp" && entity.player.opLevel >= 2) { + pvpEnabled = !pvpEnabled; + + if (pvpEnabled) { + world.setGameRule("doMobSpawning", false); + world.say("§c§l⚔ PvP 模式已开启!玩家可以互相攻击!"); + world.playSound("minecraft:entity.wither.spawn", new GameVector3(0, 70, 0), 1.0, 1.0); + } else { + world.say("§a§l☮ PvP 模式已关闭"); + } + return false; + } + return true; +}); +``` + +## 5.10 环境控制 + +### 投票换天气 + +```js +let voteClear = 0; +let voteRain = 0; +let votedPlayers = []; + +world.onChat((entity, message, tick) => { + const p = entity.player; + + switch (message) { + case "!voteclear": + if (votedPlayers.includes(entity.id)) { + p.directMessage("§c你已经投过票了!"); + return false; + } + voteClear++; + votedPlayers.push(entity.id); + world.say(`§e${p.name} §f投票 §a晴天 §7(${voteClear}/${voteRain})`); + break; + + case "!voterain": + if (votedPlayers.includes(entity.id)) { + p.directMessage("§c你已经投过票了!"); + return false; + } + voteRain++; + votedPlayers.push(entity.id); + world.say(`§e${p.name} §f投票 §b雨天 §7(${voteRain}/${voteClear})`); + break; + + default: + return true; + } + + const total = voteClear + voteRain; + const online = world.querySelectorAll("*").length; + + if (total >= online) { + if (voteClear > voteRain) { + world.clearWeather(); + world.say("§a☀ 投票结果: 晴天!"); + } else { + world.rainDensity = 1.0; + world.say("§b🌧 投票结果: 雨天!"); + } + voteClear = 0; + voteRain = 0; + votedPlayers = []; + } + + return false; +}); +``` + +### 时间段控制 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + + const times = { + "!day": 1000, + "!noon": 6000, + "!night": 13000, + "!midnight": 18000, + }; + + if (times[message]) { + world.time = times[message]; + world.say(`§e${p.name} §f将时间设为 ${message.replace("!", "")}`); + return false; + } + return true; +}); +``` + +## 5.11 AI 敌人 + +### 巡逻守卫 + +```js +function spawnPatrol(name, startPos, waypointsArr, speed) { + const guard = world.spawnEntity("minecraft:skeleton", startPos); + guard.setNameTag(name); + guard.maxHp = 50; + guard.hp = 50; + guard.setEquipment("mainhand", "minecraft:bow"); + guard.setEquipment("head", "minecraft:iron_helmet"); + guard.setPersistent(true); + guard.setAI(true); + + let wpIndex = 0; + guard.waypoints = waypointsArr; + guard.speed = speed; + + // 巡逻循环 + const tid = world.setInterval(() => { + if (guard.destroyed) { + world.clearInterval(tid); + return; + } + + const wp = guard.waypoints[wpIndex]; + const pos = guard.position; + const dist = Math.sqrt((pos.x - wp.x) ** 2 + (pos.y - wp.y) ** 2 + (pos.z - wp.z) ** 2); + + if (dist < 2) { + wpIndex = (wpIndex + 1) % guard.waypoints.length; + } + + const target = guard.waypoints[wpIndex]; + guard.navigateTo(target.x, target.y, target.z, guard.speed); + + // 附近有玩家就攻击 + const nearby = world.entitiesInRadius(pos, 8); + nearby.forEach((e) => { + if (e.isPlayer() && !guard.getTarget()) { + guard.setTarget(e); + } + }); + }, 40); // 每 2 秒检查一次 + + return guard; +} + +// 用法: +const route = [ + new GameVector3(0, 70, 0), + new GameVector3(10, 70, 0), + new GameVector3(10, 70, 10), + new GameVector3(0, 70, 10), +]; +// spawnPatrol("§c守卫A", route[0], route, 1.0); +``` + +### 自爆苦力怕 + +```js +function spawnBomber(pos, targetPos) { + const creeper = world.spawnEntity("minecraft:creeper", pos); + creeper.setNameTag("§c§l自爆者"); + creeper.addEffect("minecraft:speed", 99999, 2, true); + creeper.setAI(true); + creeper.addTag("bomber"); + + // 导航到目标 + creeper.navigateTo(targetPos.x, targetPos.y, targetPos.z, 1.2); + + // 接近目标后引爆 + const checkId = world.setInterval(() => { + if (creeper.destroyed) { + world.clearInterval(checkId); + return; + } + const dist = Math.sqrt( + (creeper.position.x - targetPos.x) ** 2 + + (creeper.position.z - targetPos.z) ** 2, + ); + if (dist < 3) { + world.explode(creeper.position, 6, false); + creeper.destroy(); + world.clearInterval(checkId); + } + }, 10); + + return creeper; +} +``` + +## 5.12 实用工具 + +### 每日重置 + +```js +// 计算距下次重置的 tick 数 +function ticksUntilReset(hour, minute) { + const dayTicks = 24000; + const targetTicks = hour * 1000 + (minute / 60) * 1000; // 近似 + const currentTicks = world.time % dayTicks; + return (targetTicks - currentTicks + dayTicks) % dayTicks || dayTicks; +} + +function setupDailyReset(hour, minute, callback) { + function schedule() { + const delay = ticksUntilReset(hour, minute); + world.setTimeout(() => { + callback(); + schedule(); // 安排下一天 + }, delay); + } + schedule(); +} + +// 用法: 每天 6:00 重置 +// setupDailyReset(6, 0, () => { +// world.say("§e新的一天开始了!每日奖励已刷新"); +// }); +``` + +### 座位/坐下 + +```js +world.onChat((entity, message, tick) => { + const p = entity.player; + + if (message === "!sit") { + // 在玩家位置生成一个不可见的固定实体作为"椅子" + const pos = entity.position; + const chair = world.createEntity({ + type: "minecraft:area_effect_cloud", + position: new GameVector3(pos.x, pos.y - 0.5, pos.z), + fixed: true, + gravity: false, + collides: false, + meshInvisible: true, + }); + chair.addTag("chair"); + + // 让玩家骑上去(注:具体骑乘 API 取决于你的实现) + p.directMessage("§7你坐下了... 输入 !stand 站起来"); + + // 存储椅子引用 + entity.myChair = chair; + return false; + } + + if (message === "!stand" && entity.myChair) { + entity.myChair.destroy(); + entity.myChair = undefined; + p.directMessage("§7你站起来了"); + return false; + } + + return true; +}); +``` + +### 欢迎礼包(仅首次) + +```js +// 用一个简单的数组跟踪已领取的玩家 +let claimedPlayers = []; + +world.onPlayerJoin((entity, tick) => { + const p = entity.player; + + // 显示标题欢迎 + p.title("§6§l欢迎回来", "§7" + p.name, 10, 60, 10); + + // 首次加入检测 + if (!claimedPlayers.includes(p.userId)) { + claimedPlayers.push(p.userId); + p.directMessage("§a首次加入!获得新手礼包!"); + p.giveItem("minecraft:stone_sword", 1); + p.giveItem("minecraft:stone_pickaxe", 1); + p.giveItem("minecraft:stone_axe", 1); + p.giveItem("minecraft:stone_shovel", 1); + p.giveItem("minecraft:bread", 32); + p.giveItem("minecraft:torch", 16); + p.giveNamedItem("minecraft:shield", 1, "§b新手之盾", [ + "§7只有真正的初始玩家才能拥有", + ]); + p.playSound("minecraft:entity.player.levelup", 1.0, 1.0); + } +}); +``` + +--- + +所有示例均可独立运行。将其整合到你的 `app.ts` 中即可使用。 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 1bc4343..e18942f 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 @@ -1,5 +1,7 @@ package com.box3lab.box3js; +import com.box3lab.box3js.registries.Box3JSCustomItems; +import com.box3lab.box3js.registries.Box3JSRecipeManager; import com.box3lab.box3js.script.Box3ScriptCommand; import com.box3lab.box3js.script.Box3ScriptEngine; import com.mojang.logging.LogUtils; @@ -8,6 +10,8 @@ import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.common.NeoForge; + +import java.nio.file.Path; import net.neoforged.neoforge.event.ServerChatEvent; import net.neoforged.neoforge.event.entity.living.LivingDamageEvent; import net.neoforged.neoforge.event.entity.living.LivingDeathEvent; @@ -25,6 +29,9 @@ public class Box3JS { public static final Logger LOGGER = LogUtils.getLogger(); public Box3JS(IEventBus modEventBus, ModContainer modContainer) { + // Custom items via data components + resource pack (no DeferredRegister, no registry sync) + Box3JSCustomItems.init(Path.of(".").toAbsolutePath().normalize()); + // Script commands NeoForge.EVENT_BUS.addListener(Box3ScriptCommand::register); @@ -66,13 +73,40 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickBlock event) -> { if (event.getEntity() instanceof ServerPlayer sp) { Box3ScriptEngine.get().fireBlockActivate(sp, event.getPos(), event.getLevel().getBlockState(event.getPos())); + Box3ScriptEngine.get().fireActionButton(sp, "ACTION1"); + } + }); + + // Left-click (ACTION0) — block and air + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireActionButton(sp, "ACTION0"); + } + }); + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickEmpty event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireActionButton(sp, "ACTION0"); + } + }); + + // Right-click (ACTION1) — item and empty (block already covered above) + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickItem event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireActionButton(sp, "ACTION1"); + } + }); + NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickEmpty event) -> { + if (event.getEntity() instanceof ServerPlayer sp) { + Box3ScriptEngine.get().fireActionButton(sp, "ACTION1"); } }); // Chat NeoForge.EVENT_BUS.addListener((ServerChatEvent event) -> { if (event.getPlayer() instanceof ServerPlayer sp) { - Box3ScriptEngine.get().fireChat(sp, event.getMessage().getString()); + if (Box3ScriptEngine.get().fireChat(sp, event.getMessage().getString())) { + event.setCanceled(true); + } } }); @@ -99,6 +133,7 @@ public Box3JS(IEventBus modEventBus, ModContainer modContainer) { // Auto-load scripts from config/box3/script//app.js on server start NeoForge.EVENT_BUS.addListener((ServerStartedEvent event) -> { Box3ScriptEngine.get().autoLoad(event.getServer()); + Box3JSRecipeManager.init(event.getServer()); }); LOGGER.info("Box3JS script engine initialized."); diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/registries/Box3JSCustomItems.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/registries/Box3JSCustomItems.java new file mode 100644 index 0000000..ca7e25c --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/registries/Box3JSCustomItems.java @@ -0,0 +1,209 @@ +package com.box3lab.box3js.registries; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.logging.LogUtils; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.food.FoodProperties; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Rarity; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.item.component.ItemLore; +import org.slf4j.Logger; + +import java.io.Reader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class Box3JSCustomItems { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map ITEMS = new LinkedHashMap<>(); + private static String baseItemId = "minecraft:paper"; + + public static void init(Path gameDir) { + loadFromPack(gameDir.resolve("resourcepacks/box3js-items/items.json")); + } + + /** Load custom items from a resource pack's items.json. Called from JS. */ + public static void loadFromPack(Path itemsFile) { + if (!Files.exists(itemsFile)) { + LOGGER.warn("Box3JS: Custom item config not found: {}", itemsFile); + return; + } + + JsonObject root; + try (Reader r = Files.newBufferedReader(itemsFile)) { + root = JsonParser.parseReader(r).getAsJsonObject(); + } catch (Exception e) { + LOGGER.error("Box3JS: Failed to parse {}: {}", itemsFile, e.getMessage()); + return; + } + + if (root.has("base_item")) { + baseItemId = root.get("base_item").getAsString(); + } + + JsonObject itemsObj = root.has("items") ? root.getAsJsonObject("items") : root; + int loaded = 0; + for (var entry : itemsObj.entrySet()) { + String id = entry.getKey(); + if (id.equals("base_item") || !entry.getValue().isJsonObject()) continue; + Box3JSCustomItemDef def = Box3JSCustomItemDef.fromMcJson(id, entry.getValue().getAsJsonObject()); + ITEMS.put(id, def); + loaded++; + } + + LOGGER.info("Box3JS loaded {} custom items from {} (base: {}).", loaded, itemsFile, baseItemId); + } + + public static Box3JSCustomItemDef get(String id) { + return ITEMS.get(id); + } + + public static Collection getIds() { + return ITEMS.keySet(); + } + + /** Create an ItemStack for the given custom item ID. */ + public static ItemStack createStack(String id, int count) { + Box3JSCustomItemDef def = ITEMS.get(id); + if (def == null) return null; + + net.minecraft.world.item.Item baseItem = BuiltInRegistriesShim.getItem(baseItemId); + if (baseItem == null) { + LOGGER.error("Box3JS: Base item '{}' not found.", baseItemId); + return null; + } + + ItemStack stack = new ItemStack(baseItem, Math.max(1, Math.min(count, def.maxStack))); + stack.set(DataComponents.CUSTOM_NAME, Component.literal(def.name)); + if (!def.lore.isEmpty()) { + List loreComponents = new ArrayList<>(); + for (String line : def.lore) { + loreComponents.add(Component.literal(line)); + } + stack.set(DataComponents.LORE, new ItemLore(loreComponents)); + } + stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(def.modelData)); + if (def.maxStack != 64) { + stack.set(DataComponents.MAX_STACK_SIZE, def.maxStack); + } + if (def.glint) { + stack.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true); + } + if (def.rarity != null) { + stack.set(DataComponents.RARITY, def.rarity); + } + if (def.food != null) { + stack.set(DataComponents.FOOD, def.food); + } + + return stack; + } + + public static class Box3JSCustomItemDef { + public final String id; + public final int modelData; + public final String name; + public final List lore; + public final int maxStack; + public final boolean glint; + public final Rarity rarity; + public final FoodProperties food; + + public Box3JSCustomItemDef(String id, int modelData, String name, List lore, + int maxStack, boolean glint, Rarity rarity, FoodProperties food) { + this.id = id; + this.modelData = modelData; + this.name = name; + this.lore = lore; + this.maxStack = maxStack; + this.glint = glint; + this.rarity = rarity; + this.food = food; + } + + /** Parse from JSON using Minecraft component IDs as keys. */ + public static Box3JSCustomItemDef fromMcJson(String id, JsonObject obj) { + // minecraft:custom_model_data + int modelData = getInt(obj, "minecraft:custom_model_data", 0); + + // minecraft:custom_name + String name = getString(obj, "minecraft:custom_name", id); + + // minecraft:lore + List lore = new ArrayList<>(); + if (obj.has("minecraft:lore") && obj.get("minecraft:lore").isJsonArray()) { + for (JsonElement e : obj.getAsJsonArray("minecraft:lore")) { + lore.add(e.getAsString()); + } + } + + // minecraft:max_stack_size + int maxStack = clamp(getInt(obj, "minecraft:max_stack_size", 64), 1, 64); + + // minecraft:enchantment_glint_override + boolean glint = getBool(obj, "minecraft:enchantment_glint_override", false); + + // minecraft:rarity + Rarity rarity = null; + String rarityStr = getString(obj, "minecraft:rarity", null); + if (rarityStr != null) { + try { rarity = Rarity.valueOf(rarityStr.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ignored) {} + } + + // minecraft:food + FoodProperties food = null; + if (obj.has("minecraft:food") && obj.get("minecraft:food").isJsonObject()) { + JsonObject f = obj.getAsJsonObject("minecraft:food"); + int nutrition = clamp(getInt(f, "nutrition", 4), 1, 20); + float saturation = getFloat(f, "saturation", 0.6f); + boolean alwaysEdible = getBool(f, "can_always_eat", false); + float eatSeconds = getFloat(f, "eat_seconds", 1.6f); + + FoodProperties.Builder builder = new FoodProperties.Builder() + .nutrition(nutrition) + .saturationModifier(saturation); + if (alwaysEdible) builder.alwaysEdible(); + if (eatSeconds <= 0.8f) builder.fast(); + food = builder.build(); + } + + return new Box3JSCustomItemDef(id, modelData, name, lore, maxStack, glint, rarity, food); + } + + private static int getInt(JsonObject obj, String key, int def) { + return obj.has(key) ? obj.get(key).getAsInt() : def; + } + + private static float getFloat(JsonObject obj, String key, float def) { + return obj.has(key) ? obj.get(key).getAsFloat() : def; + } + + private static boolean getBool(JsonObject obj, String key, boolean def) { + return obj.has(key) ? obj.get(key).getAsBoolean() : def; + } + + private static String getString(JsonObject obj, String key, String def) { + return obj.has(key) ? obj.get(key).getAsString() : def; + } + + private static int clamp(int v, int min, int max) { + return Math.max(min, Math.min(max, v)); + } + } + + /** Shim to look up vanilla items without touching DeferredRegister. */ + private static class BuiltInRegistriesShim { + static net.minecraft.world.item.Item getItem(String id) { + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return null; + return net.minecraft.core.registries.BuiltInRegistries.ITEM.getOptional(rl).orElse(null); + } + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/registries/Box3JSRecipeManager.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/registries/Box3JSRecipeManager.java new file mode 100644 index 0000000..d258295 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/registries/Box3JSRecipeManager.java @@ -0,0 +1,69 @@ +package com.box3lab.box3js.registries; + +import com.mojang.logging.LogUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeManager; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class Box3JSRecipeManager { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static RecipeManager recipeManager; + private static List> originalRecipes = List.of(); + private static final Set blacklist = ConcurrentHashMap.newKeySet(); + + public static void init(MinecraftServer server) { + recipeManager = server.getRecipeManager(); + originalRecipes = List.copyOf(recipeManager.getRecipes()); + LOGGER.info("Box3JS recipe manager initialized with {} recipes.", originalRecipes.size()); + } + + public static List listRecipes(String filter) { + if (recipeManager == null) return List.of(); + String lower = filter.toLowerCase(); + return originalRecipes.stream() + .filter(r -> r.id().toString().toLowerCase().contains(lower)) + .map(r -> r.id().toString()) + .sorted() + .toList(); + } + + public static boolean removeRecipe(String id) { + if (recipeManager == null) return false; + ResourceLocation rl = ResourceLocation.tryParse(id); + if (rl == null) return false; + blacklist.add(rl); + applyBlacklist(); + return true; + } + + public static void clearRecipes() { + if (recipeManager == null) return; + blacklist.clear(); + recipeManager.replaceRecipes(originalRecipes); + } + + private static void applyBlacklist() { + if (blacklist.isEmpty()) { + recipeManager.replaceRecipes(originalRecipes); + return; + } + List> filtered = new ArrayList<>(); + for (var recipe : originalRecipes) { + if (!blacklist.contains(recipe.id())) { + filtered.add(recipe); + } + } + recipeManager.replaceRecipes(filtered); + LOGGER.info("Box3JS recipe blacklist applied: {} recipes active ({} removed).", + filtered.size(), blacklist.size()); + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java index bcb4564..5ca2932 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSCallbacks.java @@ -2,12 +2,12 @@ @FunctionalInterface interface PlayerJoinCallback { - void onJoin(Box3JSEntity entity); + void onJoin(Box3JSEntity entity, long tick); } @FunctionalInterface interface PlayerLeaveCallback { - void onLeave(Box3JSEntity entity); + void onLeave(Box3JSEntity entity, long tick); } @FunctionalInterface @@ -27,7 +27,7 @@ interface InteractCallback { @FunctionalInterface interface ChatCallback { - void onChat(Box3JSEntity entity, String message, long tick); + Object onChat(Box3JSEntity entity, String message, long tick); } @FunctionalInterface @@ -62,7 +62,7 @@ interface EntityDeathCallback { @FunctionalInterface interface PlayerRespawnCallback { - void onRespawn(Box3JSEntity entity); + void onRespawn(Box3JSEntity entity, long tick); } @FunctionalInterface @@ -75,6 +75,11 @@ interface EntityDamageCallback { void onDamage(Box3JSEntity entity, double amount, String source, Box3JSEntity attacker, long tick); } +@FunctionalInterface +interface ButtonPressedCallback { + void onButtonPressed(Box3JSEntity entity, String button, long tick); +} + @FunctionalInterface interface MessageCallback { void onMessage(String from, Object data); 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 d26d157..a87ac25 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 @@ -104,6 +104,10 @@ public void removeTag(String tag) { entity.removeTag(tag); } + public String[] tags() { + return entity.getTags().toArray(new String[0]); + } + // ---- Glowing (MC extension) ---- public boolean isGlowing() { return entity.isCurrentlyGlowing(); } @@ -184,6 +188,38 @@ private void trackIfSandboxed() { engine.getSandbox().trackEntityModify(engine.getCurrentProject(), entity); } + // ---- Physics ---- + + public boolean getCollides() { return getProp("collides", true); } + public void setCollides(boolean v) { + trackIfSandboxed(); + setProp("collides", v); + if (!v && entity instanceof LivingEntity le) le.setNoGravity(true); + } + + public boolean getFixed() { return getProp("fixed", false); } + public void setFixed(boolean v) { + trackIfSandboxed(); + setProp("fixed", v); + if (v && entity instanceof LivingEntity le) le.setNoGravity(true); + } + + public boolean getGravity() { return getProp("gravity", true); } + public void setGravity(boolean v) { + trackIfSandboxed(); + setProp("gravity", v); + if (!v && entity instanceof LivingEntity le) le.setNoGravity(true); + } + + public double getFriction() { return getProp("friction", 0.0); } + public void setFriction(double v) { trackIfSandboxed(); setProp("friction", v); } + + public double getMass() { return getProp("mass", 1.0); } + public void setMass(double v) { trackIfSandboxed(); setProp("mass", v); } + + public double getRestitution() { return getProp("restitution", 0.0); } + public void setRestitution(double v) { trackIfSandboxed(); setProp("restitution", v); } + // ---- Invulnerable (MC extension) ---- public boolean isInvulnerable() { return entity.isInvulnerable(); } @@ -331,12 +367,6 @@ public void destroy() { engine.clearCustomProps(entity.getUUID()); } - /** Remove entity without triggering onDestroy callback */ - public void remove() { - entity.discard(); - engine.clearCustomProps(entity.getUUID()); - } - public void setOnDestroy(Function handler) { this._onDestroyHandler = handler; } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java index d617a19..db77627 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/Box3JSEventBus.java @@ -26,6 +26,7 @@ class Box3JSEventBus { final Map> respawnCallbacks = new ConcurrentHashMap<>(); final Map> blockActivateCallbacks = new ConcurrentHashMap<>(); final Map> entityDamageCallbacks = new ConcurrentHashMap<>(); + final Map> buttonPressedCallbacks = new ConcurrentHashMap<>(); final Map> messageCallbacks = new ConcurrentHashMap<>(); // Tracking state — per-project @@ -33,6 +34,7 @@ class Box3JSEventBus { final Map> fluidStateTracked = new ConcurrentHashMap<>(); final Map> entityContactPairs = new ConcurrentHashMap<>(); final Map> playerChatHandlers = new ConcurrentHashMap<>(); + final Map> previousButtonStates = new ConcurrentHashMap<>(); final Map> entityCustomProps = new HashMap<>(); final Map> timers = new ConcurrentHashMap<>(); final Map timerIdCounters = new ConcurrentHashMap<>(); @@ -57,28 +59,56 @@ int nextTimerId(String project) { return timerIdCounters.merge(project, 1, Integer::sum); } - // ---- Add callbacks ---- - - void addTick(String project, Runnable cb) { add(tickCallbacks, project, cb); } - void addJoin(String project, PlayerJoinCallback cb) { add(joinCallbacks, project, cb); } - void addLeave(String project, PlayerLeaveCallback cb) { add(leaveCallbacks, project, cb); } - void addVoxelDestroy(String project, VoxelDestroyCallback cb) { add(voxelDestroyCallbacks, project, cb); } - void addVoxelContact(String project, VoxelContactCallback cb) { add(voxelContactCallbacks, project, cb); } - void addInteract(String project, InteractCallback cb) { add(interactCallbacks, project, cb); } - void addChat(String project, ChatCallback cb) { add(chatCallbacks, project, cb); } - void addFluidEnter(String project, FluidEnterCallback cb) { add(fluidEnterCallbacks, project, cb); } - void addFluidLeave(String project, FluidLeaveCallback cb) { add(fluidLeaveCallbacks, project, cb); } - void addEntityContact(String project, EntityContactCallback cb) { add(entityContactCallbacks, project, cb); } - void addEntitySeparate(String project, EntitySeparateCallback cb) { add(entitySeparateCallbacks, project, cb); } - void addBlockPlace(String project, BlockPlaceCallback cb) { add(blockPlaceCallbacks, project, cb); } - void addEntityDeath(String project, EntityDeathCallback cb) { add(entityDeathCallbacks, project, cb); } - void addRespawn(String project, PlayerRespawnCallback cb) { add(respawnCallbacks, project, cb); } - void addBlockActivate(String project, BlockActivateCallback cb) { add(blockActivateCallbacks, project, cb); } - void addEntityDamage(String project, EntityDamageCallback cb) { add(entityDamageCallbacks, project, cb); } - void addMessage(String project, MessageCallback cb) { add(messageCallbacks, project, cb); } - - private static void add(Map> map, String project, T cb) { + // ---- Add callbacks (return the stored wrapper for later removal) ---- + + Runnable addTick(String project, Runnable cb) { return add(tickCallbacks, project, cb); } + PlayerJoinCallback addJoin(String project, PlayerJoinCallback cb) { return add(joinCallbacks, project, cb); } + PlayerLeaveCallback addLeave(String project, PlayerLeaveCallback cb) { return add(leaveCallbacks, project, cb); } + VoxelDestroyCallback addVoxelDestroy(String project, VoxelDestroyCallback cb) { return add(voxelDestroyCallbacks, project, cb); } + VoxelContactCallback addVoxelContact(String project, VoxelContactCallback cb) { return add(voxelContactCallbacks, project, cb); } + InteractCallback addInteract(String project, InteractCallback cb) { return add(interactCallbacks, project, cb); } + ChatCallback addChat(String project, ChatCallback cb) { return add(chatCallbacks, project, cb); } + FluidEnterCallback addFluidEnter(String project, FluidEnterCallback cb) { return add(fluidEnterCallbacks, project, cb); } + FluidLeaveCallback addFluidLeave(String project, FluidLeaveCallback cb) { return add(fluidLeaveCallbacks, project, cb); } + EntityContactCallback addEntityContact(String project, EntityContactCallback cb) { return add(entityContactCallbacks, project, cb); } + EntitySeparateCallback addEntitySeparate(String project, EntitySeparateCallback cb) { return add(entitySeparateCallbacks, project, cb); } + BlockPlaceCallback addBlockPlace(String project, BlockPlaceCallback cb) { return add(blockPlaceCallbacks, project, cb); } + EntityDeathCallback addEntityDeath(String project, EntityDeathCallback cb) { return add(entityDeathCallbacks, project, cb); } + PlayerRespawnCallback addRespawn(String project, PlayerRespawnCallback cb) { return add(respawnCallbacks, project, cb); } + BlockActivateCallback addBlockActivate(String project, BlockActivateCallback cb) { return add(blockActivateCallbacks, project, cb); } + EntityDamageCallback addEntityDamage(String project, EntityDamageCallback cb) { return add(entityDamageCallbacks, project, cb); } + ButtonPressedCallback addButtonPressed(String project, ButtonPressedCallback cb) { return add(buttonPressedCallbacks, project, cb); } + MessageCallback addMessage(String project, MessageCallback cb) { return add(messageCallbacks, project, cb); } + + private static T add(Map> map, String project, T cb) { map.computeIfAbsent(project, k -> new CopyOnWriteArrayList<>()).add(cb); + return cb; + } + + // ---- Remove single callbacks ---- + + void removeTick(String project, Runnable cb) { remove(tickCallbacks, project, cb); } + void removeJoin(String project, PlayerJoinCallback cb) { remove(joinCallbacks, project, cb); } + void removeLeave(String project, PlayerLeaveCallback cb) { remove(leaveCallbacks, project, cb); } + void removeVoxelDestroy(String project, VoxelDestroyCallback cb) { remove(voxelDestroyCallbacks, project, cb); } + void removeVoxelContact(String project, VoxelContactCallback cb) { remove(voxelContactCallbacks, project, cb); } + void removeInteract(String project, InteractCallback cb) { remove(interactCallbacks, project, cb); } + void removeChat(String project, ChatCallback cb) { remove(chatCallbacks, project, cb); } + void removeFluidEnter(String project, FluidEnterCallback cb) { remove(fluidEnterCallbacks, project, cb); } + void removeFluidLeave(String project, FluidLeaveCallback cb) { remove(fluidLeaveCallbacks, project, cb); } + void removeEntityContact(String project, EntityContactCallback cb) { remove(entityContactCallbacks, project, cb); } + void removeEntitySeparate(String project, EntitySeparateCallback cb) { remove(entitySeparateCallbacks, project, cb); } + void removeBlockPlace(String project, BlockPlaceCallback cb) { remove(blockPlaceCallbacks, project, cb); } + void removeEntityDeath(String project, EntityDeathCallback cb) { remove(entityDeathCallbacks, project, cb); } + void removeRespawn(String project, PlayerRespawnCallback cb) { remove(respawnCallbacks, project, cb); } + void removeBlockActivate(String project, BlockActivateCallback cb) { remove(blockActivateCallbacks, project, cb); } + void removeEntityDamage(String project, EntityDamageCallback cb) { remove(entityDamageCallbacks, project, cb); } + void removeButtonPressed(String project, ButtonPressedCallback cb) { remove(buttonPressedCallbacks, project, cb); } + void removeMessage(String project, MessageCallback cb) { remove(messageCallbacks, project, cb); } + + private static void remove(Map> map, String project, T cb) { + List list = map.get(project); + if (list != null) list.remove(cb); } // ---- Remove one project ---- @@ -100,6 +130,7 @@ void removeProject(String project) { respawnCallbacks.remove(project); blockActivateCallbacks.remove(project); entityDamageCallbacks.remove(project); + buttonPressedCallbacks.remove(project); messageCallbacks.remove(project); voxelContactTracked.remove(project); fluidStateTracked.remove(project); @@ -128,7 +159,9 @@ void clearAll() { respawnCallbacks.clear(); blockActivateCallbacks.clear(); entityDamageCallbacks.clear(); + buttonPressedCallbacks.clear(); messageCallbacks.clear(); + previousButtonStates.clear(); voxelContactTracked.clear(); fluidStateTracked.clear(); entityContactPairs.clear(); 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 6a92e15..0fece63 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 @@ -29,13 +29,42 @@ public class Box3JSPlayer { private final ServerPlayer player; private final MinecraftServer server; private final Box3ScriptEngine engine; + private final GameVector3 _position, _velocity, _bounds; public Box3JSPlayer(ServerPlayer player, MinecraftServer server, Box3ScriptEngine engine) { this.player = player; this.server = server; this.engine = engine; + this._position = new GameVector3(); + this._velocity = new GameVector3(); + this._bounds = new GameVector3(); } + // ---- Position / Velocity / Bounds ---- + + public GameVector3 getPosition() { + _position.x = player.getX(); + _position.y = player.getY(); + _position.z = player.getZ(); + return _position; + } + + public GameVector3 getVelocity() { + var v = player.getDeltaMovement(); + _velocity.x = v.x; _velocity.y = v.y; _velocity.z = v.z; + return _velocity; + } + + public GameVector3 getBounds() { + var bb = player.getBoundingBox(); + _bounds.x = (bb.maxX - bb.minX) / 2.0; + _bounds.y = (bb.maxY - bb.minY) / 2.0; + _bounds.z = (bb.maxZ - bb.minZ) / 2.0; + return _bounds; + } + + public boolean getOnGround() { return player.onGround(); } + // ---- Info ---- public String getName() { return player.getGameProfile().getName(); } @@ -97,6 +126,32 @@ public String getWalkState() { return "NONE"; } + // ---- Jump / Sneak / Swim ---- + + public boolean getEnableJump() { return getProp("enableJump", true); } + public void setEnableJump(boolean v) { + trackIfSandboxed(); + setProp("enableJump", v); + if (!v) { + setProp("_savedJumpStrength", player.getAttributeValue(Attributes.JUMP_STRENGTH)); + player.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(0); + } else { + double saved = getProp("_savedJumpStrength", 0.42); + player.getAttribute(Attributes.JUMP_STRENGTH).setBaseValue(saved); + } + } + + public double getCrouchSpeed() { return getProp("crouchSpeed", 0.0); } + public void setCrouchSpeed(double v) { trackIfSandboxed(); setProp("crouchSpeed", v); } + + public double getSwimSpeed() { + return player.getAttributeValue(net.minecraft.world.entity.ai.attributes.Attributes.WATER_MOVEMENT_EFFICIENCY); + } + public void setSwimSpeed(double v) { + trackIfSandboxed(); + player.getAttribute(net.minecraft.world.entity.ai.attributes.Attributes.WATER_MOVEMENT_EFFICIENCY).setBaseValue(v); + } + // ---- Fly / Spectator ---- public boolean getCanFly() { return player.getAbilities().mayfly; } @@ -211,6 +266,8 @@ public GameVector3 getCameraTarget() { // ---- Respawn ---- + public boolean getDead() { return player.isDeadOrDying(); } + public void setRespawnPoint(GameVector3 pos) { player.setRespawnPosition( player.level().dimension(), @@ -218,6 +275,19 @@ public void setRespawnPoint(GameVector3 pos) { 0, true, false); } + public GameVector3 getSpawnPoint() { + var pos = player.getRespawnPosition(); + if (pos == null) { + var worldSpawn = server.overworld().getSharedSpawnPos(); + return new GameVector3(worldSpawn.getX(), worldSpawn.getY(), worldSpawn.getZ()); + } + return new GameVector3(pos.getX(), pos.getY(), pos.getZ()); + } + + public void setSpawnPoint(GameVector3 pos) { + setRespawnPoint(pos); + } + public void respawn() { if (player.isDeadOrDying()) { player.respawn(); @@ -364,6 +434,13 @@ public void giveItem(String itemId, int count) { if (stack != null) player.getInventory().add(stack); } + public void giveCustomItem(String id, int count) { + ItemStack stack = com.box3lab.box3js.registries.Box3JSCustomItems.createStack(id, count); + if (stack != null) { + player.getInventory().add(stack); + } + } + public void giveEnchantedItem(String itemId, int count, NativeObject enchants) { ItemStack stack = makeItemStack(itemId, count, enchants); if (stack != null) player.getInventory().add(stack); @@ -409,6 +486,30 @@ public void clearInventory() { player.getInventory().clearContent(); } + // ---- Advancements ---- + + public void grantAdvancement(String advancementId) { + ResourceLocation rl = ResourceLocation.tryParse(advancementId); + if (rl == null) return; + var holder = player.server.getAdvancements().get(rl); + if (holder != null) { + for (String criterion : holder.value().criteria().keySet()) { + player.getAdvancements().award(holder, criterion); + } + } + } + + public void revokeAdvancement(String advancementId) { + ResourceLocation rl = ResourceLocation.tryParse(advancementId); + if (rl == null) return; + var holder = player.server.getAdvancements().get(rl); + if (holder != null) { + for (String criterion : holder.value().criteria().keySet()) { + player.getAdvancements().revoke(holder, criterion); + } + } + } + // ---- Effects ---- public void addEffect(String effectId, int duration, int amplifier) { 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 7f10914..e451901 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 @@ -1,13 +1,14 @@ package com.box3lab.box3js.script; -import net.minecraft.commands.CommandSourceStack; +import com.box3lab.box3js.registries.Box3JSRecipeManager; +import java.nio.file.Path; + import net.minecraft.core.BlockPos; -import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponents; -import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.network.protocol.game.ClientboundSoundPacket; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerLevel; import net.minecraft.sounds.SoundSource; import net.minecraft.world.Difficulty; @@ -21,10 +22,13 @@ import net.minecraft.world.item.component.Fireworks; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; -import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.border.WorldBorder; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; import net.minecraft.world.level.storage.ServerLevelData; import org.mozilla.javascript.Function; +import org.mozilla.javascript.NativeArray; +import org.mozilla.javascript.NativeObject; +import org.mozilla.javascript.ScriptableObject; import java.util.*; @@ -70,8 +74,13 @@ private void trackIfSandboxed() { // ---- World properties ---- public String projectName() { return server.getMotd(); } + public String getProjectName() { return server.getMotd(); } public int currentTick() { return server.getTickCount(); } + public int getCurrentTick() { return server.getTickCount(); } + + public String getServerId() { return server.getMotd(); } + public void setServerId(String id) { server.setMotd(id); } public double getRainDensity() { return server.overworld().getRainLevel(1.0f); } public void setRainDensity(double v) { trackIfSandboxed(); server.overworld().getLevelData().setRaining(v > 0); } @@ -164,73 +173,177 @@ public Box3JSEntity spawnEntity(String type, GameVector3 pos) { return new Box3JSEntity(entity, server, engine); } + // ---- createEntity(config) ---- + + public Box3JSEntity createEntity(NativeObject config) { + String type = config.containsKey("type") ? config.get("type").toString() : "minecraft:pig"; + EntityType eType = Box3ScriptUtils.lookupEntityType(type); + if (eType == null) return null; + Entity entity = eType.create(server.overworld()); + if (entity == null) return null; + + GameVector3 pos = config.containsKey("position") ? (GameVector3) config.get("position") + : new GameVector3(0, 0, 0); + entity.setPos(pos.x, pos.y, pos.z); + server.overworld().addFreshEntity(entity); + engine.getSandbox().trackEntity(engine.getCurrentProject(), entity); + Box3JSEntity be = new Box3JSEntity(entity, server, engine); + + if (config.containsKey("velocity")) { + GameVector3 v = (GameVector3) config.get("velocity"); + if (v != null) entity.setDeltaMovement(v.x, v.y, v.z); + } + if (config.containsKey("fixed")) be.setFixed(Box3ScriptUtils.coerceBool(config.get("fixed"))); + if (config.containsKey("gravity")) be.setGravity(Box3ScriptUtils.coerceBool(config.get("gravity"))); + if (config.containsKey("friction")) be.setFriction(((Number) config.get("friction")).doubleValue()); + if (config.containsKey("mass")) be.setMass(((Number) config.get("mass")).doubleValue()); + if (config.containsKey("restitution")) be.setRestitution(((Number) config.get("restitution")).doubleValue()); + if (config.containsKey("collides")) be.setCollides(Box3ScriptUtils.coerceBool(config.get("collides"))); + if (config.containsKey("meshInvisible")) be.setMeshInvisible(Box3ScriptUtils.coerceBool(config.get("meshInvisible"))); + if (config.containsKey("hp")) be.setHp(((Number) config.get("hp")).doubleValue()); + if (config.containsKey("maxHp")) be.setMaxHp(((Number) config.get("maxHp")).doubleValue()); + if (config.containsKey("tags")) { + Object tags = config.get("tags"); + if (tags instanceof NativeArray arr) { + for (int i = 0; i < arr.getLength(); i++) be.addTag(arr.get(i).toString()); + } + } + return be; + } + + // ---- sound(config) ---- + + public void sound(Object cfg) { + if (cfg instanceof String path) { + playSound(path, 0, 0, 0, 1.0, 1.0); + } else if (cfg instanceof NativeObject obj) { + String path = obj.containsKey("path") ? obj.get("path").toString() : ""; + double x = 0, y = 0, z = 0; + if (obj.containsKey("position")) { + GameVector3 pos = (GameVector3) obj.get("position"); + x = pos.x; y = pos.y; z = pos.z; + } + double vol = obj.containsKey("volume") ? ((Number) obj.get("volume")).doubleValue() : 1.0; + double pitch = obj.containsKey("pitch") ? ((Number) obj.get("pitch")).doubleValue() : 1.0; + playSound(path, x, y, z, vol, pitch); + } + } + + // ---- searchBox(bounds) ---- + + public List searchBox(GameBounds3 bounds) { + return entitiesInArea(bounds.lo, bounds.hi); + } + + // ---- Sound properties ---- + + public String getAmbientSound() { return ambientSound; } + public void setAmbientSound(String path) { ambientSound = path; } + + public String getPlayerJoinSound() { return playerJoinSound; } + public void setPlayerJoinSound(String path) { playerJoinSound = path; } + + public String getPlayerLeaveSound() { return playerLeaveSound; } + public void setPlayerLeaveSound(String path) { playerLeaveSound = path; } + + public String getPlaceVoxelSound() { return placeVoxelSound; } + public void setPlaceVoxelSound(String path) { placeVoxelSound = path; } + + public String getBreakVoxelSound() { return breakVoxelSound; } + public void setBreakVoxelSound(String path) { breakVoxelSound = path; } + + private String ambientSound, playerJoinSound, playerLeaveSound, placeVoxelSound, breakVoxelSound; + private long lastAmbientPlayTick; + + void tickAmbientSound(long currentTick) { + if (ambientSound != null && !ambientSound.isEmpty() && currentTick - lastAmbientPlayTick >= 200) { + lastAmbientPlayTick = currentTick; + var pos = server.overworld().getSharedSpawnPos(); + playSound(ambientSound, pos.getX(), pos.getY(), pos.getZ(), 0.3, 1.0); + } + } + // ---- Events ---- - public void onTick(Function handler) { - engine.addTickCallback(() -> engine.callFunction(handler)); + public GameEventHandlerToken onTick(Function handler) { + return new GameEventHandlerToken(engine.addTickCallback(() -> { + long tick = server.getTickCount(); + long prevTick = engine.getPrevTick(); + NativeObject info = new NativeObject(); + ScriptableObject.putProperty(info, "tick", tick); + ScriptableObject.putProperty(info, "prevTick", prevTick); + ScriptableObject.putProperty(info, "elapsedTimeMS", tick * 50); + ScriptableObject.putProperty(info, "skip", 0); + engine.callFunction(handler, info); + })); + } + public GameEventHandlerToken onPlayerJoin(Function handler) { + return new GameEventHandlerToken(engine.addJoinCallback((entity, tick) -> engine.callFunction(handler, entity, tick))); } - public void onPlayerJoin(Function handler) { - engine.addJoinCallback(entity -> engine.callFunction(handler, entity)); + public GameEventHandlerToken onPlayerLeave(Function handler) { + return new GameEventHandlerToken(engine.addLeaveCallback((entity, tick) -> engine.callFunction(handler, entity, tick))); } - public void onPlayerLeave(Function handler) { - engine.addLeaveCallback(entity -> engine.callFunction(handler, entity)); + public GameEventHandlerToken onVoxelDestroy(Function handler) { + return new GameEventHandlerToken(engine.addVoxelDestroyCallback((entity, x, y, z, voxel, tick) -> + engine.callFunction(handler, entity, x, y, z, voxel, tick))); } - public void onVoxelDestroy(Function handler) { - engine.addVoxelDestroyCallback((entity, x, y, z, voxel, tick) -> - engine.callFunction(handler, entity, x, y, z, voxel, tick)); + public GameEventHandlerToken onVoxelContact(Function handler) { + return new GameEventHandlerToken(engine.addVoxelContactCallback((entity, voxel, x, y, z, axis, force, tick) -> + engine.callFunction(handler, entity, voxel, x, y, z, axis, force, tick))); } - public void onVoxelContact(Function handler) { - engine.addVoxelContactCallback((entity, voxel, x, y, z, axis, force, tick) -> - engine.callFunction(handler, entity, voxel, x, y, z, axis, force, tick)); + public GameEventHandlerToken onInteract(Function handler) { + return new GameEventHandlerToken(engine.addInteractCallback((entity, target, tick) -> + engine.callFunction(handler, entity, target, tick))); } - public void onInteract(Function handler) { - engine.addInteractCallback((entity, target, tick) -> - engine.callFunction(handler, entity, target, tick)); + public GameEventHandlerToken onChat(Function handler) { + return new GameEventHandlerToken(engine.addChatCallback((entity, message, tick) -> + engine.callFunction(handler, entity, message, tick))); } - public void onChat(Function handler) { - engine.addChatCallback((entity, message, tick) -> - engine.callFunction(handler, entity, message, tick)); + public GameEventHandlerToken onFluidEnter(Function handler) { + return new GameEventHandlerToken(engine.addFluidEnterCallback((entity, fluid, x, y, z, tick) -> + engine.callFunction(handler, entity, fluid, x, y, z, tick))); } - public void onFluidEnter(Function handler) { - engine.addFluidEnterCallback((entity, fluid, x, y, z, tick) -> - engine.callFunction(handler, entity, fluid, x, y, z, tick)); + public GameEventHandlerToken onFluidLeave(Function handler) { + return new GameEventHandlerToken(engine.addFluidLeaveCallback((entity, fluid, x, y, z, tick) -> + engine.callFunction(handler, entity, fluid, x, y, z, tick))); } - public void onFluidLeave(Function handler) { - engine.addFluidLeaveCallback((entity, fluid, x, y, z, tick) -> - engine.callFunction(handler, entity, fluid, x, y, z, tick)); + public GameEventHandlerToken onEntityContact(Function handler) { + return new GameEventHandlerToken(engine.addEntityContactCallback((entity, other, tick) -> + engine.callFunction(handler, entity, other, tick))); } - public void onEntityContact(Function handler) { - engine.addEntityContactCallback((entity, other, tick) -> - engine.callFunction(handler, entity, other, tick)); + public GameEventHandlerToken onEntitySeparate(Function handler) { + return new GameEventHandlerToken(engine.addEntitySeparateCallback((entity, other, tick) -> + engine.callFunction(handler, entity, other, tick))); } - public void onEntitySeparate(Function handler) { - engine.addEntitySeparateCallback((entity, other, tick) -> - engine.callFunction(handler, entity, other, tick)); + public GameEventHandlerToken onBlockPlace(Function handler) { + return new GameEventHandlerToken(engine.addBlockPlaceCallback((entity, x, y, z, voxel, voxelId, tick) -> + engine.callFunction(handler, entity, x, y, z, voxel, voxelId, tick))); } - public void onBlockPlace(Function handler) { - engine.addBlockPlaceCallback((entity, x, y, z, voxel, voxelId, tick) -> - engine.callFunction(handler, entity, x, y, z, voxel, voxelId, tick)); + public GameEventHandlerToken onEntityDeath(Function handler) { + return new GameEventHandlerToken(engine.addEntityDeathCallback((entity, killer, tick) -> + engine.callFunction(handler, entity, killer, tick))); } - public void onEntityDeath(Function handler) { - engine.addEntityDeathCallback((entity, killer, tick) -> - engine.callFunction(handler, entity, killer, tick)); + public GameEventHandlerToken onPlayerRespawn(Function handler) { + return new GameEventHandlerToken(engine.addRespawnCallback((entity, tick) -> engine.callFunction(handler, entity, tick))); } - public void onPlayerRespawn(Function handler) { - engine.addRespawnCallback(entity -> engine.callFunction(handler, entity)); + public GameEventHandlerToken onBlockActivate(Function handler) { + return new GameEventHandlerToken(engine.addBlockActivateCallback((entity, x, y, z, voxel, tick) -> + engine.callFunction(handler, entity, x, y, z, voxel, tick))); } - public void onBlockActivate(Function handler) { - engine.addBlockActivateCallback((entity, x, y, z, voxel, tick) -> - engine.callFunction(handler, entity, x, y, z, voxel, tick)); + public GameEventHandlerToken onEntityDamage(Function handler) { + return new GameEventHandlerToken(engine.addEntityDamageCallback((entity, amount, source, attacker, tick) -> + engine.callFunction(handler, entity, amount, source, attacker, tick))); } - public void onEntityDamage(Function handler) { - engine.addEntityDamageCallback((entity, amount, source, attacker, tick) -> - engine.callFunction(handler, entity, amount, source, attacker, tick)); + public GameEventHandlerToken onButtonPressed(Function handler) { + return new GameEventHandlerToken(engine.addButtonPressedCallback((entity, button, tick) -> + engine.callFunction(handler, entity, button, tick))); } - public void onMessage(Function handler) { + public GameEventHandlerToken onMessage(Function handler) { String project = engine.getCurrentProject(); if (project != null) { - engine.addMessageCallback(project, (from, d) -> engine.callFunction(handler, from, d)); + return new GameEventHandlerToken(engine.addMessageCallback(project, (from, d) -> engine.callFunction(handler, from, d))); } + return new GameEventHandlerToken(() -> {}); } // ---- Entity Query ---- @@ -441,6 +554,61 @@ public void playSound(String path, GameVector3 pos, double volume, double pitch) playSound(path, pos.x, pos.y, pos.z, volume, pitch); } + // ---- Structure ---- + + public void placeStructure(double x, double y, double z, String structureId) { + ResourceLocation rl = ResourceLocation.tryParse(structureId); + if (rl == null) return; + server.overworld().getStructureManager().get(rl).ifPresent(template -> { + template.placeInWorld(server.overworld(), + new BlockPos(0, 0, 0), + new BlockPos((int) x, (int) y, (int) z), + new StructurePlaceSettings().setKnownShape(true), + server.overworld().getRandom(), 3); + }); + } + public void placeStructure(GameVector3 pos, String structureId) { + placeStructure(pos.x, pos.y, pos.z, structureId); + } + + // ---- Advancement ---- + + public void grantAdvancement(String playerName, String advancementId) { + ServerPlayer sp = server.getPlayerList().getPlayerByName(playerName); + if (sp == null) return; + ResourceLocation rl = ResourceLocation.tryParse(advancementId); + if (rl == null) return; + var holder = server.getAdvancements().get(rl); + if (holder != null) { + for (String criterion : holder.value().criteria().keySet()) { + sp.getAdvancements().award(holder, criterion); + } + } + } + + // ---- Custom Items ---- + + /** Load custom items from a resource pack's items.json using MC component IDs. */ + public void loadCustomItems(String packName) { + Path itemsFile = Path.of(".").toAbsolutePath().normalize() + .resolve("resourcepacks").resolve(packName).resolve("items.json"); + com.box3lab.box3js.registries.Box3JSCustomItems.loadFromPack(itemsFile); + } + + // ---- Recipe ---- + + public List listRecipes(String filter) { + return Box3JSRecipeManager.listRecipes(filter != null ? filter : ""); + } + + public boolean removeRecipe(String recipeId) { + return Box3JSRecipeManager.removeRecipe(recipeId); + } + + public void clearRecipes() { + Box3JSRecipeManager.clearRecipes(); + } + // ---- Message ---- public void sendMessage(String target, Object data) { 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 e2121d5..73deeed 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 @@ -33,6 +33,7 @@ public class Box3ScriptEngine { final Box3JSEventBus bus = new Box3JSEventBus(); private String currentProject; + private long currentTick, prevTick; private Consumer errorReporter; private final Map projectRequires = new HashMap<>(); @@ -107,74 +108,118 @@ Box3ScriptEngine withErrorReporter(Consumer reporter) { } void clearErrorReporter() { this.errorReporter = null; } - // ---- Callback registration (all wrap project context) ---- + // ---- Callback registration (all return removal Runnables) ---- - public void addTickCallback(Runnable cb) { + public Runnable addTickCallback(Runnable cb) { String project = currentProject; - bus.addTick(project, wrapContext(project, cb)); + Runnable wrapped = wrapContext(project, cb); + bus.addTick(project, wrapped); + return () -> bus.removeTick(project, wrapped); } - public void addJoinCallback(PlayerJoinCallback cb) { + public Runnable addJoinCallback(PlayerJoinCallback cb) { String project = currentProject; - bus.addJoin(project, (e) -> runInContext(project, () -> cb.onJoin(e))); + PlayerJoinCallback wrapped = (e, t) -> runInContext(project, () -> cb.onJoin(e, t)); + bus.addJoin(project, wrapped); + return () -> bus.removeJoin(project, wrapped); } - public void addLeaveCallback(PlayerLeaveCallback cb) { + public Runnable addLeaveCallback(PlayerLeaveCallback cb) { String project = currentProject; - bus.addLeave(project, (e) -> runInContext(project, () -> cb.onLeave(e))); + PlayerLeaveCallback wrapped = (e, t) -> runInContext(project, () -> cb.onLeave(e, t)); + bus.addLeave(project, wrapped); + return () -> bus.removeLeave(project, wrapped); } - public void addVoxelDestroyCallback(VoxelDestroyCallback cb) { + public Runnable addVoxelDestroyCallback(VoxelDestroyCallback cb) { String project = currentProject; - bus.addVoxelDestroy(project, (e, x, y, z, v, t) -> runInContext(project, () -> cb.onDestroy(e, x, y, z, v, t))); + VoxelDestroyCallback wrapped = (e, x, y, z, v, t) -> runInContext(project, () -> cb.onDestroy(e, x, y, z, v, t)); + bus.addVoxelDestroy(project, wrapped); + return () -> bus.removeVoxelDestroy(project, wrapped); } - public void addVoxelContactCallback(VoxelContactCallback cb) { + public Runnable addVoxelContactCallback(VoxelContactCallback cb) { String project = currentProject; - bus.addVoxelContact(project, (e, v, x, y, z, a, f, t) -> runInContext(project, () -> cb.onContact(e, v, x, y, z, a, f, t))); + VoxelContactCallback wrapped = (e, v, x, y, z, a, f, t) -> runInContext(project, () -> cb.onContact(e, v, x, y, z, a, f, t)); + bus.addVoxelContact(project, wrapped); + return () -> bus.removeVoxelContact(project, wrapped); } - public void addInteractCallback(InteractCallback cb) { + public Runnable addInteractCallback(InteractCallback cb) { String project = currentProject; - bus.addInteract(project, (e, tgt, tick) -> runInContext(project, () -> cb.onInteract(e, tgt, tick))); + InteractCallback wrapped = (e, tgt, tick) -> runInContext(project, () -> cb.onInteract(e, tgt, tick)); + bus.addInteract(project, wrapped); + return () -> bus.removeInteract(project, wrapped); } - public void addChatCallback(ChatCallback cb) { + public Runnable addChatCallback(ChatCallback cb) { String project = currentProject; - bus.addChat(project, (e, msg, tick) -> runInContext(project, () -> cb.onChat(e, msg, tick))); + ChatCallback wrapped = (e, msg, tick) -> { + java.util.concurrent.atomic.AtomicReference result = new java.util.concurrent.atomic.AtomicReference<>(); + runInContext(project, () -> result.set(cb.onChat(e, msg, tick))); + return result.get(); + }; + bus.addChat(project, wrapped); + return () -> bus.removeChat(project, wrapped); + } + public Runnable addFluidEnterCallback(FluidEnterCallback cb) { + String project = currentProject; + FluidEnterCallback wrapped = (e, f, x, y, z, t) -> runInContext(project, () -> cb.onEnter(e, f, x, y, z, t)); + bus.addFluidEnter(project, wrapped); + return () -> bus.removeFluidEnter(project, wrapped); } - public void addFluidEnterCallback(FluidEnterCallback cb) { + public Runnable addFluidLeaveCallback(FluidLeaveCallback cb) { String project = currentProject; - bus.addFluidEnter(project, (e, f, x, y, z, t) -> runInContext(project, () -> cb.onEnter(e, f, x, y, z, t))); + FluidLeaveCallback wrapped = (e, f, x, y, z, t) -> runInContext(project, () -> cb.onLeave(e, f, x, y, z, t)); + bus.addFluidLeave(project, wrapped); + return () -> bus.removeFluidLeave(project, wrapped); } - public void addFluidLeaveCallback(FluidLeaveCallback cb) { + public Runnable addEntityContactCallback(EntityContactCallback cb) { String project = currentProject; - bus.addFluidLeave(project, (e, f, x, y, z, t) -> runInContext(project, () -> cb.onLeave(e, f, x, y, z, t))); + EntityContactCallback wrapped = (e, o, t) -> runInContext(project, () -> cb.onContact(e, o, t)); + bus.addEntityContact(project, wrapped); + return () -> bus.removeEntityContact(project, wrapped); } - public void addEntityContactCallback(EntityContactCallback cb) { + public Runnable addEntitySeparateCallback(EntitySeparateCallback cb) { String project = currentProject; - bus.addEntityContact(project, (e, o, t) -> runInContext(project, () -> cb.onContact(e, o, t))); + EntitySeparateCallback wrapped = (e, o, t) -> runInContext(project, () -> cb.onSeparate(e, o, t)); + bus.addEntitySeparate(project, wrapped); + return () -> bus.removeEntitySeparate(project, wrapped); } - public void addEntitySeparateCallback(EntitySeparateCallback cb) { + public Runnable addBlockPlaceCallback(BlockPlaceCallback cb) { String project = currentProject; - bus.addEntitySeparate(project, (e, o, t) -> runInContext(project, () -> cb.onSeparate(e, o, t))); + BlockPlaceCallback wrapped = (e, x, y, z, v, vid, t) -> runInContext(project, () -> cb.onPlace(e, x, y, z, v, vid, t)); + bus.addBlockPlace(project, wrapped); + return () -> bus.removeBlockPlace(project, wrapped); } - public void addBlockPlaceCallback(BlockPlaceCallback cb) { + public Runnable addEntityDeathCallback(EntityDeathCallback cb) { String project = currentProject; - bus.addBlockPlace(project, (e, x, y, z, v, vid, t) -> runInContext(project, () -> cb.onPlace(e, x, y, z, v, vid, t))); + EntityDeathCallback wrapped = (e, k, t) -> runInContext(project, () -> cb.onDeath(e, k, t)); + bus.addEntityDeath(project, wrapped); + return () -> bus.removeEntityDeath(project, wrapped); } - public void addEntityDeathCallback(EntityDeathCallback cb) { + public Runnable addRespawnCallback(PlayerRespawnCallback cb) { String project = currentProject; - bus.addEntityDeath(project, (e, k, t) -> runInContext(project, () -> cb.onDeath(e, k, t))); + PlayerRespawnCallback wrapped = (e, t) -> runInContext(project, () -> cb.onRespawn(e, t)); + bus.addRespawn(project, wrapped); + return () -> bus.removeRespawn(project, wrapped); } - public void addRespawnCallback(PlayerRespawnCallback cb) { + public Runnable addBlockActivateCallback(BlockActivateCallback cb) { String project = currentProject; - bus.addRespawn(project, (e) -> runInContext(project, () -> cb.onRespawn(e))); + BlockActivateCallback wrapped = (e, x, y, z, v, t) -> runInContext(project, () -> cb.onActivate(e, x, y, z, v, t)); + bus.addBlockActivate(project, wrapped); + return () -> bus.removeBlockActivate(project, wrapped); } - public void addBlockActivateCallback(BlockActivateCallback cb) { + public Runnable addEntityDamageCallback(EntityDamageCallback cb) { String project = currentProject; - bus.addBlockActivate(project, (e, x, y, z, v, t) -> runInContext(project, () -> cb.onActivate(e, x, y, z, v, t))); + EntityDamageCallback wrapped = (e, a, s, at, t) -> runInContext(project, () -> cb.onDamage(e, a, s, at, t)); + bus.addEntityDamage(project, wrapped); + return () -> bus.removeEntityDamage(project, wrapped); } - public void addEntityDamageCallback(EntityDamageCallback cb) { + public Runnable addButtonPressedCallback(ButtonPressedCallback cb) { String project = currentProject; - bus.addEntityDamage(project, (e, a, s, at, t) -> runInContext(project, () -> cb.onDamage(e, a, s, at, t))); + ButtonPressedCallback wrapped = (e, btn, t) -> runInContext(project, () -> cb.onButtonPressed(e, btn, t)); + bus.addButtonPressed(project, wrapped); + return () -> bus.removeButtonPressed(project, wrapped); } - public void addMessageCallback(String project, MessageCallback cb) { - bus.addMessage(project, (from, d) -> runInContext(project, () -> cb.onMessage(from, d))); + public Runnable addMessageCallback(String project, MessageCallback cb) { + MessageCallback wrapped = (from, d) -> runInContext(project, () -> cb.onMessage(from, d)); + bus.addMessage(project, wrapped); + return () -> bus.removeMessage(project, wrapped); } public void setPlayerChatHandler(UUID uuid, Function handler) { String project = currentProject; @@ -200,6 +245,7 @@ public void setCurrentProject(String name) { worldBinding.setProjectName(name); } public String getCurrentProject() { return currentProject; } + long getPrevTick() { return prevTick; } Box3ScriptSandbox getSandbox() { return sandbox; } @@ -274,10 +320,64 @@ private void fireTimers() { } } + // ---- Button press tracking ---- + + private void checkButtonPresses() { + if (bus.buttonPressedCallbacks.isEmpty()) return; + long tick = server.getTickCount(); + boolean anyProjectCares = false; + for (var list : bus.buttonPressedCallbacks.values()) { + if (!list.isEmpty()) { anyProjectCares = true; break; } + } + if (!anyProjectCares) return; + + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + UUID uuid = player.getUUID(); + Set current = new HashSet<>(); + + if (player.isCrouching()) current.add("CROUCH"); + if (player.isSprinting()) current.add("RUN"); + var delta = player.getDeltaMovement(); + if (Math.abs(delta.x) > 0.01 || Math.abs(delta.z) > 0.01) { + if (player.onGround() && !player.isSprinting()) current.add("WALK"); + } + if (!player.onGround() && delta.y > 0.01) current.add("JUMP"); + if (player.getAbilities().flying) current.add("FLY"); + + Set previous = bus.previousButtonStates.get(uuid); + if (previous != null) { + for (String btn : current) { + if (!previous.contains(btn)) { + fireButtonPressed(player, btn, tick); + } + } + } + bus.previousButtonStates.put(uuid, current); + } + } + + private void fireButtonPressed(ServerPlayer sp, String button, long tick) { + Box3JSEntity entity = new Box3JSEntity(sp, server, this); + for (var entry : bus.buttonPressedCallbacks.entrySet()) { + if (entry.getValue().isEmpty()) continue; + runInContext(entry.getKey(), () -> { + for (var cb : entry.getValue()) cb.onButtonPressed(entity, button, tick); + }); + } + } + + public void fireActionButton(ServerPlayer sp, String button) { + long tick = server.getTickCount(); + fireButtonPressed(sp, button, tick); + } + // ---- Tick ---- public void fireTick() { + prevTick = currentTick; + currentTick = server.getTickCount(); fireTimers(); + checkButtonPresses(); for (var list : bus.tickCallbacks.values()) { for (Runnable cb : list) cb.run(); } @@ -346,6 +446,7 @@ public void fireTick() { } } } + worldBinding.tickAmbientSound(currentTick); } private void tickFluid(String project, List enter, List leave) { @@ -398,6 +499,8 @@ public void fireVoxelDestroy(ServerPlayer player, BlockPos pos) { for (var cb : entry.getValue()) cb.onDestroy(e, pos.getX(), pos.getY(), pos.getZ(), v, t); }); } + String s = worldBinding.getBreakVoxelSound(); + if (s != null && !s.isEmpty()) worldBinding.playSound(s, pos.getX(), pos.getY(), pos.getZ(), 1.0, 1.0); } public void fireInteract(ServerPlayer player, net.minecraft.world.entity.Entity target) { @@ -416,7 +519,9 @@ public void fireInteract(ServerPlayer player, net.minecraft.world.entity.Entity } } - public void fireChat(ServerPlayer player, String message) { + /** @return true if any chat callback returned false to cancel */ + public boolean fireChat(ServerPlayer player, String message) { + java.util.concurrent.atomic.AtomicBoolean cancelled = new java.util.concurrent.atomic.AtomicBoolean(false); Box3JSEntity entity = null; long tick = -1; for (var entry : bus.chatCallbacks.entrySet()) { @@ -424,9 +529,15 @@ public void fireChat(ServerPlayer player, String message) { Box3JSEntity e = entity; long t = tick; runInContext(entry.getKey(), () -> { - for (var cb : entry.getValue()) cb.onChat(e, message, t); + for (var cb : entry.getValue()) { + Object result = cb.onChat(e, message, t); + if (result instanceof Boolean && !((Boolean) result)) { + cancelled.set(true); + } + } }); } + if (cancelled.get()) return true; // Per-player chat handlers for (var entry : bus.playerChatHandlers.entrySet()) { Function handler = entry.getValue().get(player.getUUID()); @@ -437,6 +548,7 @@ public void fireChat(ServerPlayer player, String message) { runInContext(project, () -> callFunction(handler, e, message, t)); } } + return cancelled.get(); } public void fireBlockPlace(ServerPlayer player, BlockPos pos, BlockState state) { @@ -455,6 +567,8 @@ public void fireBlockPlace(ServerPlayer player, BlockPos pos, BlockState state) for (var cb : entry.getValue()) cb.onPlace(e, pos.getX(), pos.getY(), pos.getZ(), v, vid, t); }); } + String s = worldBinding.getPlaceVoxelSound(); + if (s != null && !s.isEmpty()) worldBinding.playSound(s, pos.getX(), pos.getY(), pos.getZ(), 1.0, 1.0); } public void fireEntityDeath(net.minecraft.world.entity.Entity deadEntity, net.minecraft.world.entity.Entity attacker) { @@ -475,12 +589,14 @@ public void fireEntityDeath(net.minecraft.world.entity.Entity deadEntity, net.mi public void firePlayerRespawn(ServerPlayer player) { Box3JSEntity entity = null; + long tick = -1; for (var entry : bus.respawnCallbacks.entrySet()) { if (entry.getValue().isEmpty()) continue; - if (entity == null) entity = new Box3JSEntity(player, server, this); + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); } Box3JSEntity e = entity; + long t = tick; runInContext(entry.getKey(), () -> { - for (var cb : entry.getValue()) cb.onRespawn(e); + for (var cb : entry.getValue()) cb.onRespawn(e, t); }); } } @@ -519,26 +635,34 @@ public void fireEntityDamage(net.minecraft.world.entity.Entity damagedEntity, do public void firePlayerJoin(ServerPlayer player) { Box3JSEntity entity = null; + long tick = -1; for (var entry : bus.joinCallbacks.entrySet()) { if (entry.getValue().isEmpty()) continue; - if (entity == null) entity = new Box3JSEntity(player, server, this); + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); } Box3JSEntity e = entity; + long t = tick; runInContext(entry.getKey(), () -> { - for (var cb : entry.getValue()) cb.onJoin(e); + for (var cb : entry.getValue()) cb.onJoin(e, t); }); } + String s = worldBinding.getPlayerJoinSound(); + if (s != null && !s.isEmpty()) worldBinding.playSound(s, player.getX(), player.getY(), player.getZ(), 1.0, 1.0); } public void firePlayerLeave(ServerPlayer player) { Box3JSEntity entity = null; + long tick = -1; for (var entry : bus.leaveCallbacks.entrySet()) { if (entry.getValue().isEmpty()) continue; - if (entity == null) entity = new Box3JSEntity(player, server, this); + if (entity == null) { entity = new Box3JSEntity(player, server, this); tick = server.getTickCount(); } Box3JSEntity e = entity; + long t = tick; runInContext(entry.getKey(), () -> { - for (var cb : entry.getValue()) cb.onLeave(e); + for (var cb : entry.getValue()) cb.onLeave(e, t); }); } + String s = worldBinding.getPlayerLeaveSound(); + if (s != null && !s.isEmpty()) worldBinding.playSound(s, player.getX(), player.getY(), player.getZ(), 1.0, 1.0); } /** Call a JS function from Java, managing Rhino context */ @@ -551,13 +675,6 @@ public Object callFunction(Function fn, Object... args) { } } - /** Wrap a Java object for return to JS */ - public Object wrap(Object obj) { - return Context.javaToJS(obj, scope); - } - - public ScriptableObject getScope() { return scope; } - public Map getCustomProps(UUID uuid) { return bus.entityCustomProps.computeIfAbsent(uuid, k -> new HashMap<>()); } @@ -650,14 +767,13 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar ScriptableObject.putProperty(scope, "GameRGBColor", new NativeJavaClass(scope, GameRGBColor.class)); ScriptableObject.putProperty(scope, "GameRGBAColor", new NativeJavaClass(scope, GameRGBAColor.class)); ScriptableObject.putProperty(scope, "GameQuaternion", new NativeJavaClass(scope, GameQuaternion.class)); + ScriptableObject.putProperty(scope, "GameEventHandlerToken", new NativeJavaClass(scope, GameEventHandlerToken.class)); cx.evaluateString(scope, - "GameDialogType = { TEXT: 'TEXT', INPUT: 'INPUT', SELECT: 'SELECT' }; " + "GameButtonType = { WALK: 'WALK', RUN: 'RUN', CROUCH: 'CROUCH', JUMP: 'JUMP', " + - " DOUBLE_JUMP: 'DOUBLE_JUMP', FLY: 'FLY', ACTION0: 'ACTION0', ACTION1: 'ACTION1' }; " + - "GameInputDirection = { NONE: 0, VERTICAL: 1, HORIZONTAL: 2, BOTH: 3 }; " + - "GameCameraMode = { FIXED: 'FIXED', FOLLOW: 'FOLLOW', FPS: 'FPS', RELATIVE: 'RELATIVE' }; " + + " FLY: 'FLY', ACTION0: 'ACTION0', ACTION1: 'ACTION1' }; " + + "GameCameraMode = { FOLLOW: 'FOLLOW', FPS: 'FPS' }; " + "GamePlayerMoveState = { FLYING: 'FLYING', GROUND: 'GROUND', SWIM: 'SWIM', FALL: 'FALL', " + - " JUMP: 'JUMP', DOUBLE_JUMP: 'DOUBLE_JUMP' }; " + + " JUMP: 'JUMP' }; " + "GamePlayerWalkState = { NONE: 'NONE', CROUCH: 'CROUCH', WALK: 'WALK', RUN: 'RUN' };", "enums", 1, null); } finally { @@ -665,8 +781,6 @@ public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] ar } } - public MinecraftServer getServer() { return server; } - public Box3JSWorld getWorldBinding() { return worldBinding; } public Box3JSVoxels getVoxelsBinding() { return voxelsBinding; } public class Box3JSConsole { diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java index 960e50e..f6045c5 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameBounds3.java @@ -1,5 +1,7 @@ package com.box3lab.box3js.script; +import org.mozilla.javascript.NativeArray; + public class GameBounds3 { public GameVector3 lo, hi; @@ -9,18 +11,68 @@ public GameBounds3(GameVector3 lo, GameVector3 hi) { this.hi = hi; } + public GameBounds3 set(double lox, double loy, double loz, double hix, double hiy, double hiz) { + lo.x = lox; lo.y = loy; lo.z = loz; + hi.x = hix; hi.y = hiy; hi.z = hiz; + return this; + } + + public GameBounds3 copy(GameBounds3 b) { + lo.x = b.lo.x; lo.y = b.lo.y; lo.z = b.lo.z; + hi.x = b.hi.x; hi.y = b.hi.y; hi.z = b.hi.z; + return this; + } + public boolean intersects(GameBounds3 other) { return !(hi.x < other.lo.x || lo.x > other.hi.x || hi.y < other.lo.y || lo.y > other.hi.y || hi.z < other.lo.z || lo.z > other.hi.z); } + public GameBounds3 intersect(GameBounds3 other) { + double lx = Math.max(lo.x, other.lo.x); + double ly = Math.max(lo.y, other.lo.y); + double lz = Math.max(lo.z, other.lo.z); + double hx = Math.min(hi.x, other.hi.x); + double hy = Math.min(hi.y, other.hi.y); + double hz = Math.min(hi.z, other.hi.z); + if (lx > hx || ly > hy || lz > hz) return null; + return new GameBounds3(new GameVector3(lx, ly, lz), new GameVector3(hx, hy, hz)); + } + public boolean contains(GameVector3 v) { return v.x >= lo.x && v.x <= hi.x && v.y >= lo.y && v.y <= hi.y && v.z >= lo.z && v.z <= hi.z; } + public boolean containsBounds(GameBounds3 b) { + return contains(b.lo) && contains(b.hi); + } + + public static GameBounds3 fromPoints(Object points) { + if (!(points instanceof NativeArray arr)) return null; + long len = arr.getLength(); + if (len == 0) return null; + + double minX = Double.POSITIVE_INFINITY, minY = Double.POSITIVE_INFINITY, minZ = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY, maxZ = Double.NEGATIVE_INFINITY; + + for (int i = 0; i < len; i++) { + Object elem = arr.get(i); + if (elem instanceof GameVector3 v) { + if (v.x < minX) minX = v.x; + if (v.y < minY) minY = v.y; + if (v.z < minZ) minZ = v.z; + if (v.x > maxX) maxX = v.x; + if (v.y > maxY) maxY = v.y; + if (v.z > maxZ) maxZ = v.z; + } + } + + return new GameBounds3(new GameVector3(minX, minY, minZ), new GameVector3(maxX, maxY, maxZ)); + } + @Override public String toString() { return "GameBounds3(" + lo + ", " + hi + ")"; diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameEventHandlerToken.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameEventHandlerToken.java new file mode 100644 index 0000000..cac0802 --- /dev/null +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameEventHandlerToken.java @@ -0,0 +1,26 @@ +package com.box3lab.box3js.script; + +public class GameEventHandlerToken { + + private boolean cancelled; + private final Runnable onCancel; + + public GameEventHandlerToken(Runnable onCancel) { + this.onCancel = onCancel; + } + + public void cancel() { + if (!cancelled) { + cancelled = true; + onCancel.run(); + } + } + + public void resume() { + throw new UnsupportedOperationException("Resume is not supported — re-register the handler instead"); + } + + public boolean active() { + return !cancelled; + } +} diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java index bca71a1..29174bd 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameRGBColor.java @@ -8,10 +8,72 @@ public GameRGBColor(double r, double g, double b) { this.r = r; this.g = g; this.b = b; } + public GameRGBColor set(double r, double g, double b) { + this.r = r; this.g = g; this.b = b; return this; + } + + public GameRGBColor copy(GameRGBColor o) { + this.r = o.r; this.g = o.g; this.b = o.b; return this; + } + + public GameRGBColor clone() { + return new GameRGBColor(r, g, b); + } + + public GameRGBColor add(GameRGBColor o) { + return new GameRGBColor(r + o.r, g + o.g, b + o.b); + } + + public GameRGBColor sub(GameRGBColor o) { + return new GameRGBColor(r - o.r, g - o.g, b - o.b); + } + + public GameRGBColor mul(GameRGBColor o) { + return new GameRGBColor(r * o.r, g * o.g, b * o.b); + } + + public GameRGBColor div(GameRGBColor o) { + return new GameRGBColor( + o.r == 0 ? 0 : r / o.r, + o.g == 0 ? 0 : g / o.g, + o.b == 0 ? 0 : b / o.b); + } + + public GameRGBColor addEq(GameRGBColor o) { + r += o.r; g += o.g; b += o.b; return this; + } + + public GameRGBColor subEq(GameRGBColor o) { + r -= o.r; g -= o.g; b -= o.b; return this; + } + + public GameRGBColor mulEq(GameRGBColor o) { + r *= o.r; g *= o.g; b *= o.b; return this; + } + + public GameRGBColor divEq(GameRGBColor o) { + r = o.r == 0 ? 0 : r / o.r; + g = o.g == 0 ? 0 : g / o.g; + b = o.b == 0 ? 0 : b / o.b; + return this; + } + public GameRGBColor lerp(GameRGBColor o, double n) { return new GameRGBColor(r + (o.r - r) * n, g + (o.g - g) * n, b + (o.b - b) * n); } + public boolean equals(GameRGBColor o) { + if (o == null) return false; + return Math.abs(r - o.r) < 1e-6 && Math.abs(g - o.g) < 1e-6 && Math.abs(b - o.b) < 1e-6; + } + + public String toRGBA() { + int ri = Math.max(0, Math.min(255, (int) (r * 255))); + int gi = Math.max(0, Math.min(255, (int) (g * 255))); + int bi = Math.max(0, Math.min(255, (int) (b * 255))); + return String.format("rgba(%d,%d,%d,1.0)", ri, gi, bi); + } + public static GameRGBColor random() { return new GameRGBColor(Math.random(), Math.random(), Math.random()); } diff --git a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java index 2654a7c..cda31f8 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java +++ b/Box3JS-NeoForge-1.21.1/src/main/java/com/box3lab/box3js/script/GameVector3.java @@ -14,6 +14,14 @@ public GameVector3 set(double x, double y, double z) { this.x = x; this.y = y; this.z = z; return this; } + public GameVector3 copy(GameVector3 v) { + this.x = v.x; this.y = v.y; this.z = v.z; return this; + } + + public GameVector3 clone() { + return new GameVector3(x, y, z); + } + public GameVector3 add(GameVector3 v) { return new GameVector3(x + v.x, y + v.y, z + v.z); } @@ -22,14 +30,51 @@ public GameVector3 sub(GameVector3 v) { return new GameVector3(x - v.x, y - v.y, z - v.z); } + public GameVector3 mul(GameVector3 v) { + return new GameVector3(x * v.x, y * v.y, z * v.z); + } + + public GameVector3 div(GameVector3 v) { + return new GameVector3( + v.x == 0 ? 0 : x / v.x, + v.y == 0 ? 0 : y / v.y, + v.z == 0 ? 0 : z / v.z); + } + public GameVector3 scale(double n) { return new GameVector3(x * n, y * n, z * n); } + public GameVector3 addEq(GameVector3 v) { + x += v.x; y += v.y; z += v.z; return this; + } + + public GameVector3 subEq(GameVector3 v) { + x -= v.x; y -= v.y; z -= v.z; return this; + } + + public GameVector3 mulEq(GameVector3 v) { + x *= v.x; y *= v.y; z *= v.z; return this; + } + + public GameVector3 divEq(GameVector3 v) { + x = v.x == 0 ? 0 : x / v.x; + y = v.y == 0 ? 0 : y / v.y; + z = v.z == 0 ? 0 : z / v.z; + return this; + } + public double dot(GameVector3 v) { return x * v.x + y * v.y + z * v.z; } + public GameVector3 cross(GameVector3 v) { + return new GameVector3( + y * v.z - z * v.y, + z * v.x - x * v.z, + x * v.y - y * v.x); + } + public double mag() { return Math.sqrt(x * x + y * y + z * z); } @@ -50,10 +95,35 @@ public GameVector3 lerp(GameVector3 v, double n) { return new GameVector3(x + (v.x - x) * n, y + (v.y - y) * n, z + (v.z - z) * n); } + public GameVector3 towards(GameVector3 v) { + return sub(v).normalize(); + } + + public double angle(GameVector3 v) { + double m = mag() * v.mag(); + if (m == 0) return 0; + double c = dot(v) / m; + return Math.acos(Math.max(-1, Math.min(1, c))); + } + public boolean equals(GameVector3 v) { + if (v == null) return false; + return Math.abs(x - v.x) < 1e-6 && Math.abs(y - v.y) < 1e-6 && Math.abs(z - v.z) < 1e-6; + } + + public boolean exactEquals(GameVector3 v) { + if (v == null) return false; return x == v.x && y == v.y && z == v.z; } + public GameVector3 max(GameVector3 v) { + return new GameVector3(Math.max(x, v.x), Math.max(y, v.y), Math.max(z, v.z)); + } + + public GameVector3 min(GameVector3 v) { + return new GameVector3(Math.min(x, v.x), Math.min(y, v.y), Math.min(z, v.z)); + } + public static GameVector3 fromPolar(double mag, double phi, double theta) { return new GameVector3( mag * Math.cos(phi) * Math.cos(theta), diff --git a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts index ca416e0..8d94d73 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts +++ b/Box3JS-NeoForge-1.21.1/src/main/resources/assets/box3js/template/types/globals.d.ts @@ -40,6 +40,12 @@ declare class GameVector3 { */ set(x: number, y: number, z: number): GameVector3; + /** 原地复制 v 的值。Copies values from v in‑place. */ + copy(v: GameVector3): GameVector3; + + /** 深拷贝。Returns a new independent copy. */ + clone(): GameVector3; + /** * 向量加法: this + v。 * Vector addition: this + v. @@ -54,6 +60,12 @@ declare class GameVector3 { */ sub(v: GameVector3): GameVector3; + /** 逐分量乘法 (返回新对象)。Component‑wise multiplication (returns new vector). */ + mul(v: GameVector3): GameVector3; + + /** 逐分量除法 (返回新对象, 除以 0 得 0)。Component‑wise division (divide‑by‑zero → 0). */ + div(v: GameVector3): GameVector3; + /** * 标量乘法: 每个分量乘以 n。 * Scalar multiplication: each component multiplied by n. @@ -61,12 +73,27 @@ declare class GameVector3 { */ scale(n: number): GameVector3; + /** 原地加法。Addition in‑place. */ + addEq(v: GameVector3): GameVector3; + + /** 原地减法。Subtraction in‑place. */ + subEq(v: GameVector3): GameVector3; + + /** 原地乘法。Multiplication in‑place. */ + mulEq(v: GameVector3): GameVector3; + + /** 原地除法 (除以 0 跳过该分量)。Division in‑place (divide‑by‑zero skips that component). */ + divEq(v: GameVector3): GameVector3; + /** * 点积 (内积): this · v。 * Dot (inner) product: this · v. */ dot(v: GameVector3): number; + /** 叉积: this × v。Cross product. */ + cross(v: GameVector3): GameVector3; + /** * 向量长度 (模)。 * Magnitude (length) of this vector. @@ -100,11 +127,32 @@ declare class GameVector3 { lerp(v: GameVector3, n: number): GameVector3; /** - * 检查两个向量的所有分量是否完全相等。 - * Returns true if all components are exactly equal. + * 指向 v 的方向向量 (已单位化)。 + * Direction vector pointing toward v (normalized). + */ + towards(v: GameVector3): GameVector3; + + /** + * this 与 v 之间的夹角 (弧度)。 + * Angle between this and v in radians. + */ + angle(v: GameVector3): number; + + /** + * 近似相等检查 (容差 1e‑6)。 + * Approximate equality within 1e‑6 tolerance. */ equals(v: GameVector3): boolean; + /** 精确相等检查 (分量完全相等)。Exact component‑wise equality. */ + exactEquals(v: GameVector3): boolean; + + /** 逐分量取较大值 (返回新对象)。Component‑wise max. */ + max(v: GameVector3): GameVector3; + + /** 逐分量取较小值 (返回新对象)。Component‑wise min. */ + min(v: GameVector3): GameVector3; + /** * 从球坐标创建向量。 * Creates a vector from spherical coordinates. @@ -140,18 +188,43 @@ declare class GameBounds3 { */ constructor(lo: GameVector3, hi: GameVector3); + /** 原地设置所有边界。Sets all boundaries in‑place. */ + set( + lox: number, + loy: number, + loz: number, + hix: number, + hiy: number, + hiz: number, + ): GameBounds3; + + /** 原地复制 b 的值。Copies values from b in‑place. */ + copy(b: GameBounds3): GameBounds3; + /** * 判断当前包围盒是否与 other 相交。 * Returns true if this bounds intersects with other. */ intersects(other: GameBounds3): boolean; + /** + * 计算交集包围盒 (无交集返回 null)。 + * Returns the intersection bounds, or null if they don't overlap. + */ + intersect(other: GameBounds3): GameBounds3 | null; + /** * 判断点 v 是否位于包围盒内部 (含边界)。 * Returns true if point v is inside (or on the boundary of) this bounds. */ contains(v: GameVector3): boolean; + /** 判断是否完全包含另一个包围盒。Whether this bounds fully contains b. */ + containsBounds(b: GameBounds3): boolean; + + /** 从 GameVector3 数组创建最小包围盒。Creates bounds from an array of GameVector3. */ + static fromPoints(points: GameVector3[]): GameBounds3 | null; + toString(): string; } @@ -175,12 +248,51 @@ declare class GameRGBColor { */ constructor(r: number, g: number, b: number); + /** 原地设置所有通道。Sets all three channels in‑place. */ + set(r: number, g: number, b: number): GameRGBColor; + + /** 原地复制另一个颜色的值。Copies values from another color in‑place. */ + copy(o: GameRGBColor): GameRGBColor; + + /** 深拷贝。Returns a new independent copy. */ + clone(): GameRGBColor; + + /** 逐通道加法 (返回新对象)。Channel‑wise addition (returns new object). */ + add(o: GameRGBColor): GameRGBColor; + + /** 逐通道减法 (返回新对象)。Channel‑wise subtraction (returns new object). */ + sub(o: GameRGBColor): GameRGBColor; + + /** 逐通道乘法 (返回新对象)。Channel‑wise multiplication (returns new object). */ + mul(o: GameRGBColor): GameRGBColor; + + /** 逐通道除法 (返回新对象, 除以 0 得 0)。Channel‑wise division (divide‑by‑zero → 0). */ + div(o: GameRGBColor): GameRGBColor; + + /** 原地加法。Addition in‑place. */ + addEq(o: GameRGBColor): GameRGBColor; + + /** 原地减法。Subtraction in‑place. */ + subEq(o: GameRGBColor): GameRGBColor; + + /** 原地乘法。Multiplication in‑place. */ + mulEq(o: GameRGBColor): GameRGBColor; + + /** 原地除法 (除以 0 跳过该通道)。Division in‑place (divide‑by‑zero skips that channel). */ + divEq(o: GameRGBColor): GameRGBColor; + /** * 在 this 和 o 之间线性插值。 * Linear interpolation between this and o by ratio n. */ lerp(o: GameRGBColor, n: number): GameRGBColor; + /** 近似相等检查 (容差 1e‑6)。Approximate equality within 1e‑6 tolerance. */ + equals(o: GameRGBColor): boolean; + + /** 转为 "rgba(r,g,b,1.0)" 格式字符串。Converts to an rgba CSS string. */ + toRGBA(): string; + /** * 生成一个随机 RGB 颜色 (每个通道 0‑1)。 * Generates a random RGB color (each channel 0–1). @@ -391,6 +503,44 @@ interface AxisAngle { axis: GameVector3; } +/** + * 事件处理器令牌 — 由 world.onXxx() 返回。 + * Event handler token — returned by world.onXxx(). + * + * @remarks + * 调用 cancel() 取消监听后不可恢复, 需重新注册。 + * Once cancelled via cancel(), it cannot be resumed — re-register instead. + */ +declare class GameEventHandlerToken { + /** 取消事件监听 (不可逆)。Cancels the event listener (irreversible). */ + cancel(): void; + + /** + * 尝试恢复已取消的监听 (会抛出 UnsupportedOperationException)。 + * Attempts to resume a cancelled listener — always throws UnsupportedOperationException. + * @throws UnsupportedOperationException 始终抛出 / always thrown + */ + resume(): void; + + /** 返回 true 表示监听仍处于活跃状态。Returns true if the listener is still active. */ + active(): boolean; +} + +/** + * onTick 回调的参数类型。 + * The info object passed to onTick handlers. + */ +interface TickInfo { + /** 当前 tick 数。Current tick count. */ + tick: number; + /** 上一 tick 数。Previous tick count. */ + prevTick: number; + /** 自启动以来的毫秒数。Milliseconds elapsed since server start. */ + elapsedTimeMS: number; + /** 跳过的 tick 数 (MC 下始终为 0)。Number of skipped ticks (always 0 in MC). */ + skip: number; +} + // ================================================================ // §2 Storage Types — 持久化存储 // ================================================================ @@ -536,8 +686,8 @@ interface ReturnValue { * Cross‑project sharing: `getGroupStorage` uses a `__shared__/` namespace visible to all projects. */ interface GameStorage { - /** 始终返回空字符串 (MC 本地存储无 key)。Always returns "" for MC local storage. */ - key: string; + /** 始终返回空字符串 (MC 本地存储无 key, 只读)。Always returns "" for MC local storage, readonly. */ + readonly key: string; /** * 打开或创建指定名称的数据存储空间 (项目隔离)。 @@ -574,54 +724,54 @@ interface GameEntity { // ── 身份 / Identity ── /** - * 实体 UUID (字符串格式)。 - * Entity UUID as a string (e.g. "550e8400-e29b-41d4-a716-446655440000"). + * 实体 UUID (字符串格式, 只读)。 + * Entity UUID as a string (e.g. "550e8400-e29b-41d4-a716-446655440000"), readonly. */ - id: string; + readonly id: string; /** * 是否为玩家实体。返回 true 后 player 属性自动收窄为非 null。 * True if this entity is a player. After a truthy check, `player` is narrowed to non-null. */ - isPlayer(): this is GameEntity & { player: GamePlayer }; + isPlayer(): this is GamePlayerEntity; /** - * 实体类型标识符 (如 "minecraft:zombie")。 - * Entity type identifier (e.g. "minecraft:zombie"). + * 实体类型标识符 (如 "minecraft:zombie", 只读)。 + * Entity type identifier (e.g. "minecraft:zombie"), readonly. */ - entityType: string; + readonly entityType: string; // ── 位置 & 运动 / Position & Movement ── /** - * 当前坐标 (世界坐标)。 - * Current world‑space position. + * 当前坐标 (世界坐标, 只读, 可通过 .set() 修改)。 + * Current world‑space position. Readonly ref — mutate via .set(), cannot reassign. */ - position: GameVector3; + readonly position: GameVector3; /** - * 当前速度 (运动向量)。 - * Current velocity (motion vector). + * 当前速度 (运动向量, 只读, 可通过 .set() 修改)。 + * Current velocity (motion vector). Readonly ref — mutate via .set(), cannot reassign. */ - velocity: GameVector3; + readonly velocity: GameVector3; /** - * 包围盒半尺寸 (x=宽/2, y=高/2, z=宽/2)。 - * Bounding‑box half‑extents (x=width/2, y=height/2, z=width/2). + * 包围盒半尺寸 (x=宽/2, y=高/2, z=宽/2, 只读)。 + * Bounding‑box half‑extents (x=width/2, y=height/2, z=width/2), readonly. */ - bounds: GameVector3; + readonly bounds: GameVector3; /** - * 是否在地面上。 - * True if the entity is standing on a block. + * 是否在地面上 (只读)。 + * True if the entity is standing on a block, readonly. */ - onGround: boolean; + readonly onGround: boolean; /** - * 视线起始点 (眼部位置)。 - * Eye position (raycast origin for the entity's view). + * 视线起始点 (眼部位置, 只读)。 + * Eye position (raycast origin for the entity's view), readonly. */ - eyePosition: GameVector3; + readonly eyePosition: GameVector3; // ── 生命状态 / Lifecycle ── @@ -638,10 +788,10 @@ interface GameEntity { maxHp: number; /** - * 实体是否已被移除/销毁 (true = 已移除)。 - * Whether the entity has been removed / destroyed (true = removed). + * 实体是否已被移除/销毁 (true = 已移除, 只读)。 + * Whether the entity has been removed / destroyed (true = removed), readonly. */ - destroyed: boolean; + readonly destroyed: boolean; /** * 设置实体着火 tick 数 (0 = 灭火)。 @@ -686,6 +836,35 @@ interface GameEntity { nameTag: string; setNameTag(name: string): void; + // ── 物理 / Physics ── + + /** + * 是否参与碰撞 (默认 true)。 + * Whether the entity participates in collisions (default true). + */ + collides: boolean; + + /** + * 是否固定 (默认 false, true 时禁用重力并每 tick 清零速度)。 + * Whether the entity is fixed in place (default false; disables gravity + zeros velocity each tick). + */ + fixed: boolean; + + /** + * 是否受重力影响 (默认 true)。 + * Whether gravity affects the entity (default true). + */ + gravity: boolean; + + /** 摩擦系数 (默认 0.0)。Friction coefficient. */ + friction: number; + + /** 质量 (默认 1.0)。Mass. */ + mass: number; + + /** 弹性系数 (默认 0.0)。Restitution (bounciness). */ + restitution: number; + // ── 无敌 & 持久化 / Invulnerability & Persistence ── /** 是否无敌。Whether the entity is invulnerable to damage. */ @@ -709,6 +888,9 @@ interface GameEntity { /** 检查是否拥有指定标签。Checks whether the entity has the given tag. */ hasTag(tag: string): boolean; + /** 获取所有标签。Returns all tags as a string array. */ + tags(): string[]; + // ── 效果 / Effects ── /** @@ -816,16 +998,6 @@ interface GameEntity { */ destroy(): void; - /** - * 移除实体 (不触发 onDestroy 回调)。 - * Removes the entity WITHOUT triggering onDestroy callback. - */ - remove(): void; - - /** - * 注册实体被销毁时的回调。 - * Registers a callback to be called when this entity is destroyed. - */ setOnDestroy(handler: (entity: GameEntity) => void): void; // ── 玩家代理 / Player proxy ── @@ -837,6 +1009,12 @@ interface GameEntity { player: GamePlayer | null; } +/** + * 玩家实体 — GameEntity 的子类型, 保证 player 属性非 null。 + * A player entity — subtype of GameEntity with a guaranteed non‑null `player`. + */ +type GamePlayerEntity = GameEntity & { player: GamePlayer }; + // ================================================================ // §4 Player — 玩家 // ================================================================ @@ -848,10 +1026,36 @@ interface GameEntity { interface GamePlayer { // ── 身份 / Identity ── - /** 玩家名。Player display name. */ - name: string; - /** 玩家 UUID (与 entity.id 相同)。Player UUID (same as entity.id). */ - userId: string; + /** 玩家名 (只读)。Player display name, readonly. */ + readonly name: string; + /** 玩家 UUID (与 entity.id 相同, 只读)。Player UUID (same as entity.id), readonly. */ + readonly userId: string; + + // ── 位置 & 运动 / Position & Movement ── + + /** + * 当前坐标 (世界坐标, 只读, 可通过 .set() 修改)。 + * Current world‑space position. Readonly ref — mutate via .set(), cannot reassign. + */ + readonly position: GameVector3; + + /** + * 当前速度 (运动向量, 只读, 可通过 .set() 修改)。 + * Current velocity (motion vector). Readonly ref — mutate via .set(), cannot reassign. + */ + readonly velocity: GameVector3; + + /** + * 包围盒半尺寸 (只读)。 + * Bounding‑box half‑extents, readonly. + */ + readonly bounds: GameVector3; + + /** + * 是否在地面上 (只读)。 + * True if the player is standing on a block, readonly. + */ + readonly onGround: boolean; // ── 外观 / Appearance ── @@ -898,6 +1102,20 @@ interface GamePlayer { */ readonly walkState: string; + // ── 跳跃 / 潜行 / 游泳 / Jump / Sneak / Swim ── + + /** + * 是否允许跳跃 (默认 true, false 时清除跳跃力)。 + * Whether jumping is enabled (default true; when false, jump strength is zeroed). + */ + enableJump: boolean; + + /** 潜行速度 (默认 0.0, MC 下无独立潜行速度)。Crouch speed (stored as custom prop). */ + crouchSpeed: number; + + /** 游泳速度 (映射到 WATER_MOVEMENT_EFFICIENCY 属性)。Swim speed (maps to WATER_MOVEMENT_EFFICIENCY attribute). */ + swimSpeed: number; + // ── 飞行 & 碰撞 / Flying & Collision ── /** 是否允许飞行。Whether flight is enabled. */ @@ -1000,6 +1218,18 @@ interface GamePlayer { // ── 重生 / Respawn ── + /** + * 是否已死亡。 + * Whether the player is dead or dying. + */ + readonly dead: boolean; + + /** + * 重生点坐标 (可读写)。 + * Spawn point coordinates (readable & writable). + */ + spawnPoint: GameVector3; + /** * 设置重生点。 * Sets the player's respawn point. @@ -1105,6 +1335,15 @@ interface GamePlayer { */ giveItem(itemId: string, count: number): void; + /** + * 给予玩家自定义物品 (基于 resourcepacks/box3js-items/items.json 配置)。 + * Gives a custom item defined in the resource pack's items.json. + * Items are vanilla paper with custom_model_data + name/lore/food components. + * @param id - 自定义物品 ID (如 "arena_trophy") + * @param count - 数量 (1‑64) + */ + giveCustomItem(id: string, count: number): void; + /** * 给予玩家附魔物品。 * Gives an enchanted item to the player. @@ -1146,9 +1385,6 @@ interface GamePlayer { /** 管理员权限等级 (0-4)。0=普通玩家, 4=最高权限。Server operator permission level (0–4). */ opLevel: number; - /** 管理员权限等级 (0-4)。0=普通玩家, 4=最高权限。Server operator permission level (0–4). */ - getOpLevel(): number; - // ── 效果 / Effects ── /** @@ -1185,10 +1421,29 @@ interface GamePlayer { /** * 为该玩家注册聊天处理器 (覆盖全局 onChat)。 * Registers a per‑player chat handler (overrides global onChat for this player). + * @returns GameEventHandlerToken */ onChat( - handler: (entity: GameEntity, message: string, tick: number) => void, - ): void; + handler: ( + entity: GamePlayerEntity, + message: string, + tick: number, + ) => boolean | void, + ): GameEventHandlerToken; + + // ── 成就 / Advancements ── + + /** + * 授予该玩家一个成就/进度。 + * Grants an advancement to this player by resource location (e.g. "minecraft:story/mine_stone"). + */ + grantAdvancement(advancementId: string): void; + + /** + * 撤销该玩家的一个成就/进度。 + * Revokes an advancement from this player. + */ + revokeAdvancement(advancementId: string): void; } // ================================================================ @@ -1205,6 +1460,9 @@ interface GameWorld { /** 服务器 MOTD。Server MOTD string. */ projectName(): string; + /** 服务器 MOTD (可读写, 同 projectName)。Server MOTD (read/write, alias of projectName). */ + serverId: string; + /** 当前服务端 tick 计数。Current server tick count. */ currentTick(): number; @@ -1284,6 +1542,55 @@ interface GameWorld { */ setGameRule(name: string, value: boolean | string): void; + // ── 音效属性 / Sound Properties ── + + /** 环境音效路径 (每 200 tick 在世界出生点自动播放, 0.3 音量)。Ambient sound — auto-plays at world spawn every 200 ticks at 0.3 volume. */ + ambientSound: string; + + /** 玩家加入音效路径 (玩家加入时自动播放)。Player join sound — auto-plays when a player joins. */ + playerJoinSound: string; + + /** 玩家离开音效路径 (玩家离开时自动播放)。Player leave sound — auto-plays when a player leaves. */ + playerLeaveSound: string; + + /** 方块放置音效路径 (放置方块时自动播放)。Block place sound — auto-plays when a block is placed. */ + placeVoxelSound: string; + + /** 方块破坏音效路径 (破坏方块时自动播放)。Block break sound — auto-plays when a block is broken. */ + breakVoxelSound: string; + + // ── 实体生成 / Entity Spawning ── + + /** + * 在指定位置生成实体。 + * Spawns an entity at the given position. + * @param type - 实体类型 ID (如 "minecraft:zombie") + * @param pos - 生成坐标 + * @returns 生成的实体包装, 失败返回 null + */ + spawnEntity(type: string, pos: GameVector3): GameEntity | null; + + /** + * 使用完整配置对象生成实体。 + * Spawns an entity with a full configuration object. + * @param config - { type, position, velocity, fixed, gravity, friction, mass, restitution, collides, meshInvisible, hp, maxHp, tags } + */ + createEntity(config: { + type?: string; + position?: GameVector3; + velocity?: GameVector3; + fixed?: boolean; + gravity?: boolean; + friction?: number; + mass?: number; + restitution?: number; + collides?: boolean; + meshInvisible?: boolean; + hp?: number; + maxHp?: number; + tags?: string[]; + }): GameEntity | null; + // ── 消息 & 声音 / Broadcasting ── /** @@ -1292,6 +1599,60 @@ interface GameWorld { */ say(message: string): void; + // ── 自定义物品 / Custom Items ── + + /** + * 从资源包加载自定义物品配置 (基于数据组件, 无需 DeferredRegister, 无注册表同步问题)。 + * Loads custom item definitions from a resource pack's items.json. + * Items use minecraft:paper as base with custom_model_data for model switching. + * Models & textures must be provided via the resource pack (resourcepacks//). + * + * JSON 格式使用 Minecraft 原版组件 ID 作为 key: + * "minecraft:custom_model_data", "minecraft:custom_name", "minecraft:lore", + * "minecraft:max_stack_size", "minecraft:enchantment_glint_override", + * "minecraft:rarity", "minecraft:food": { nutrition, saturation, can_always_eat, eat_seconds } + * + * @param packName - 资源包目录名 (如 "box3js-items"), 会在 resourcepacks//items.json 查找 + */ + loadCustomItems(packName: string): void; + + // ── 结构 & 成就 / Structure & Advancement ── + + /** + * 在指定位置放置数据包中的 .nbt 结构。 + * Places an .nbt structure from current datapacks at the given position. + * Structure must exist under data//structure/.nbt + */ + placeStructure(x: number, y: number, z: number, structureId: string): void; + placeStructure(pos: GameVector3, structureId: string): void; + + /** + * 为指定玩家授予成就/进度。 + * Grants a datapack advancement to a player by name. + */ + grantAdvancement(playerName: string, advancementId: string): void; + + /** + * 按物品名搜索配方 ID 列表。 + * Searches recipe IDs matching a filter string. + * @param filter - 搜索关键词 (匹配配方 ID) + */ + listRecipes(filter: string): string[]; + + /** + * 移除指定 ID 的配方 (黑名单机制, 服务器重载后需重新移除)。 + * Removes a recipe by ID (blacklisted; re‑apply after server reload). + * @param recipeId - 配方 ID, 例如 "minecraft:iron_pickaxe" + * @returns 是否成功加入黑名单 + */ + removeRecipe(recipeId: string): boolean; + + /** + * 清除所有配方黑名单, 恢复全部原始配方。 + * Clears the recipe blacklist and restores all original recipes. + */ + clearRecipes(): void; + /** * 在指定位置向全服播放声音。 * Plays a sound for all players at a location. @@ -1348,19 +1709,37 @@ interface GameWorld { * 查询指定半径内的所有实体。 * Returns all entities within a radius around a point. */ - entitiesInRadius(x: number, y: number, z: number, radius: number): GameEntity[]; + entitiesInRadius( + x: number, + y: number, + z: number, + radius: number, + ): GameEntity[]; entitiesInRadius(pos: GameVector3, radius: number): GameEntity[]; - // ── 实体生成 / Entity Spawning ── + // ── 搜索与音效 / Search & Sound ── /** - * 在指定位置生成实体。 - * Spawns an entity at the given position. - * @param type - 实体类型 ID (如 "minecraft:zombie") - * @param pos - 生成坐标 - * @returns 生成的实体包装, 失败返回 null + * 播放音效 (简写或完整配置)。 + * Plays a sound (string shorthand or full config object). + * @param config - 音效路径字符串 或 { path, position, volume, pitch } */ - spawnEntity(type: string, pos: GameVector3): GameEntity | null; + sound( + config: + | string + | { + path: string; + position?: GameVector3; + volume?: number; + pitch?: number; + }, + ): void; + + /** + * 查询包围盒内的所有实体。 + * Returns all entities inside a GameBounds3. + */ + searchBox(bounds: GameBounds3): GameEntity[]; // ── 射线检测 / Raycast ── @@ -1705,78 +2084,94 @@ interface GameWorld { // ═══════════════════════════════════════════════════ // 事件注册 / Event Registration + // 所有 onXxx() 返回 GameEventHandlerToken, 调用 .cancel() 取消监听。 + // All onXxx() return GameEventHandlerToken; call .cancel() to unregister. // ═══════════════════════════════════════════════════ /** * 注册每 tick 回调 (每秒 20 次)。 * Registers a callback invoked every tick (20 times/sec). + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ - onTick(handler: () => void): void; + onTick(handler: (info: TickInfo) => void): GameEventHandlerToken; /** * 注册玩家加入回调。 * Registers a callback invoked when a player joins the server. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ - onPlayerJoin(handler: (entity: GameEntity) => void): void; + onPlayerJoin( + handler: (entity: GamePlayerEntity, tick: number) => void, + ): GameEventHandlerToken; /** * 注册玩家离开回调。 * Registers a callback invoked when a player leaves the server. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ - onPlayerLeave(handler: (entity: GameEntity) => void): void; + onPlayerLeave( + handler: (entity: GamePlayerEntity, tick: number) => void, + ): GameEventHandlerToken; /** * 注册聊天消息回调 (包括 /me 消息)。 * Registers a callback for chat messages (including /me). * @param handler - (entity, message, tick) => void + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onChat( - handler: (entity: GameEntity, message: string, tick: number) => void, - ): void; + handler: (entity: GamePlayerEntity, message: string, tick: number) => void, + ): GameEventHandlerToken; /** * 注册玩家重生回调。 * Registers a callback invoked when a player respawns. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ - onPlayerRespawn(handler: (entity: GameEntity) => void): void; + onPlayerRespawn( + handler: (entity: GamePlayerEntity, tick: number) => void, + ): GameEventHandlerToken; /** * 注册方块右键激活回调。 * Registers a callback invoked when a player right‑clicks a block. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onBlockActivate( handler: ( - entity: GameEntity, + entity: GamePlayerEntity, x: number, y: number, z: number, voxel: string, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册方块破坏回调。 * Registers a callback invoked when a player breaks a block. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onVoxelDestroy( handler: ( - entity: GameEntity, + entity: GamePlayerEntity, x: number, y: number, z: number, voxel: string, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册方块放置回调。 * Registers a callback invoked when a player places a block. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onBlockPlace( handler: ( - entity: GameEntity, + entity: GamePlayerEntity, x: number, y: number, z: number, @@ -1784,15 +2179,16 @@ interface GameWorld { voxelId: number, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册方块接触回调 (玩家移动到新方块时触发)。 * Registers a callback invoked when a player's block position changes. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onVoxelContact( handler: ( - entity: GameEntity, + entity: GamePlayerEntity, voxelId: number, x: number, y: number, @@ -1801,27 +2197,38 @@ interface GameWorld { force: number, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册实体交互回调 (玩家右键实体)。 * Registers a callback invoked when a player right‑clicks an entity. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onInteract( - handler: (entity: GameEntity, target: GameEntity, tick: number) => void, - ): void; + handler: ( + entity: GamePlayerEntity, + target: GameEntity, + tick: number, + ) => void, + ): GameEventHandlerToken; /** * 注册实体死亡回调。 * Registers a callback invoked when an entity dies. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onEntityDeath( - handler: (entity: GameEntity, killer: GameEntity | null, tick: number) => void, - ): void; + handler: ( + entity: GameEntity, + killer: GameEntity | null, + tick: number, + ) => void, + ): GameEventHandlerToken; /** * 注册实体受伤回调。 * Registers a callback invoked when an entity takes damage. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onEntityDamage( handler: ( @@ -1831,59 +2238,79 @@ interface GameWorld { attacker: GameEntity | null, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册流体进入回调 (玩家进入水/熔岩)。 * Registers a callback invoked when a player enters a fluid. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onFluidEnter( handler: ( - entity: GameEntity, + entity: GamePlayerEntity, fluid: string, x: number, y: number, z: number, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册流体离开回调 (玩家离开水/熔岩)。 * Registers a callback invoked when a player leaves a fluid. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onFluidLeave( handler: ( - entity: GameEntity, + entity: GamePlayerEntity, fluid: string, x: number, y: number, z: number, tick: number, ) => void, - ): void; + ): GameEventHandlerToken; /** * 注册实体接触回调 (两个实体碰撞)。 * Registers a callback invoked when two entities come into contact. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onEntityContact( handler: (entityA: GameEntity, entityB: GameEntity, tick: number) => void, - ): void; + ): GameEventHandlerToken; /** * 注册实体分离回调 (两个实体不再碰撞)。 * Registers a callback invoked when two entities separate after contact. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ onEntitySeparate( handler: (entityA: GameEntity, entityB: GameEntity, tick: number) => void, - ): void; + ): GameEventHandlerToken; + + /** + * 注册按钮按下回调 — 当玩家按下指定按钮时触发。 + * Registers a callback for button presses from any player. + * @param handler — `(entity, button, tick) => void` + * + * `button` 参数值是 {@link GameButtonType} 中的字符串常量之一: + * WALK / RUN / CROUCH / JUMP / FLY / ACTION0 / ACTION1 + * @returns GameEventHandlerToken — 调用 .cancel() 取消 + */ + onButtonPressed( + handler: (entity: GamePlayerEntity, button: string, tick: number) => void, + ): GameEventHandlerToken; /** * 注册跨项目消息回调。 * Registers a callback for messages from other script projects. + * @returns GameEventHandlerToken — 调用 .cancel() 取消 */ - onMessage(handler: (sender: string, data: unknown) => void): void; + onMessage( + handler: (sender: string, data: unknown) => void, + ): GameEventHandlerToken; } /** @@ -1974,8 +2401,8 @@ interface GameVoxels { getVoxelId(pos: GameVector3): number; /** - * 获取方块名称 (与 getVoxel 相同, 兼容旧 API)。 - * Alias for getVoxel — kept for Box3 compatibility. + * 获取方块名称 (如 "minecraft:stone")。 + * Returns the block name at the given position (e.g. "minecraft:stone"). */ getVoxelName(x: number, y: number, z: number): string; getVoxelName(pos: GameVector3): string; @@ -2125,50 +2552,26 @@ interface GameConsole { // ================================================================ /** - * 对话框类型 — 用于 player.dialog()。 - * Dialog type constants for player.dialog(). - */ -declare const GameDialogType: { - readonly TEXT: "TEXT"; - readonly INPUT: "INPUT"; - readonly SELECT: "SELECT"; -}; - -/** - * 按钮类型 — 用于输入绑定。 - * Button type constants for input bindings. + * 按钮类型 — 用于 world.onButtonPressed() 的 button 参数。 + * Button type constants for the button parameter of world.onButtonPressed(). */ declare const GameButtonType: { readonly WALK: "WALK"; readonly RUN: "RUN"; readonly CROUCH: "CROUCH"; readonly JUMP: "JUMP"; - readonly DOUBLE_JUMP: "DOUBLE_JUMP"; readonly FLY: "FLY"; readonly ACTION0: "ACTION0"; readonly ACTION1: "ACTION1"; }; /** - * 输入方向 — 用于输入绑定。 - * Input direction constants for input bindings. - */ -declare const GameInputDirection: { - readonly NONE: 0; - readonly VERTICAL: 1; - readonly HORIZONTAL: 2; - readonly BOTH: 3; -}; - -/** - * 相机模式 — 用于 player.cameraMode 属性。 + * 相机模式 — player.cameraMode 的取值。 * Camera mode constants for the player.cameraMode property. */ declare const GameCameraMode: { - readonly FIXED: "FIXED"; readonly FOLLOW: "FOLLOW"; readonly FPS: "FPS"; - readonly RELATIVE: "RELATIVE"; }; /** @@ -2181,7 +2584,6 @@ declare const GamePlayerMoveState: { readonly SWIM: "SWIM"; readonly FALL: "FALL"; readonly JUMP: "JUMP"; - readonly DOUBLE_JUMP: "DOUBLE_JUMP"; }; /** @@ -2211,21 +2613,6 @@ declare const storage: GameStorage; /** 服务端控制台输出 / Server console output */ declare const console: GameConsole; -/** - * CommonJS 模块导入。 - * CommonJS module import. - * - * @remarks - * 从当前项目目录加载 .js 文件 (自动追加 .js 后缀)。 - * Loads a .js file from the current project directory (auto‑appends .js extension). - * 模块通过 Rhino 的 ModuleScope 加载,支持相对路径和嵌套导入。 - * Modules are loaded via Rhino's ModuleScope; relative paths and nested requires are supported. - * - * @param id - 模块标识符 (如 "./state" 或 "./state.js") - * @returns 模块的 exports 对象 - */ -declare function require(id: string): any; - /** * 阻塞当前执行线程 (毫秒级)。 * Blocks the current execution thread for the specified duration. diff --git a/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml b/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml index 526db5a..7d93cca 100644 --- a/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml +++ b/Box3JS-NeoForge-1.21.1/src/main/templates/META-INF/neoforge.mods.toml @@ -46,7 +46,7 @@ authors="神岛实验室" # The description text for the mod (multi line!) (#mandatory) description=''' -Box3JS 为 Minecraft 服务端引入 JavaScript 脚本运行时,支持约 100 项 Box3 API 与 90 余项 Minecraft 扩展 API。无需 Java 基础即可编写服务端脚本。 +Box3JS 为 Minecraft 引入神奇代码岛风格的 JavaScript 脚本运行时,支持约 200 项 Box3 API 与 90 余项 Minecraft 扩展 API。无需 Java 基础即可编写服务端脚本。 ''' # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. diff --git a/README.md b/README.md index 36cf3a0..656cf49 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ npm install && npm run build # 安装依赖并编译 - `storage` — JSON 数据持久化 - `console` / `require()` / `sleep()` / `GameVector3` / `GameBounds3` / `GameRGBColor` 等 -完整 API 文档见 `docs/api/`。 +完整 API 文档见 `docs/api/`,开发教程见 `docs/tutorial/`([从零开始 →](Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md))。 ### 🔒 命令权限管理 diff --git a/README_en.md b/README_en.md index de4dea0..122405b 100644 --- a/README_en.md +++ b/README_en.md @@ -80,6 +80,45 @@ You can also migrate structures from Box3 directly into your Minecraft world, pr Quickly toggle barrier visibility. The state is saved to a local config file and will be applied automatically next time you enter the world. +### 🧪 Box3JS Script Engine (Beta) + +Box3JS is a built-in JavaScript scripting engine (Rhino engine) that lets server owners write server-side scripts for custom gameplay, mini-games, and world interactions. Scripts live in `config/box3/script//`. + +**Quick start:** + +```bash +/box3script create mygame # Create TypeScript scaffold +cd config/box3/script/mygame +npm install && npm run build # Install and compile +/box3script sandbox mygame # (Recommended) Enable sandbox mode +/box3script on mygame # Enable and run +``` + +**Commands:** + +| Command | Description | +|---|---| +| `/box3script` | List all projects and their on/sandbox status | +| `/box3script create ` | Create a new script project (TypeScript scaffold) | +| `/box3script on ` | Enable and load a project | +| `/box3script on all` | Enable all projects | +| `/box3script off ` | Disable a project | +| `/box3script off all` | Disable all projects | +| `/box3script stop` | Stop all scripts (sandbox projects keep tracking state) | +| `/box3script stop ` | Stop a project (sandbox projects keep tracking state) | +| `/box3script reload` | Reload all enabled projects | +| `/box3script reload ` | Reload a project (for development) | +| `/box3script watch` | Toggle file watching (auto-reload `.js` on changes) | +| `/box3script sandbox ` | Toggle sandbox mode (on = track, off = rollback) | + +> All `` parameters support **Tab completion**. + +**Sandbox system:** tracks all world modifications and auto-rolls back when disabled, covering: block changes, entity/player states, world state. + +**Available APIs:** `world`, `entity`, `player`, `voxels`, `storage`, `console` / `require()` / `GameVector3` / `GameBounds3` and more. + +Full API docs: `docs/api/`. Step-by-step tutorials: `docs/tutorial/` ([Getting Started →](Box3JS-NeoForge-1.21.1/docs/tutorial/01-basics.md)). + ### 🔒 Command Permission Management - `/box3import`, `/box3barrier`, and `/box3export` require different permission levels depending on configuration (default level `0`).