sapdon 3.2.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -121
- package/doc/dev/architecture.md +418 -0
- package/doc/dev/cli.md +467 -0
- package/doc/dev/core.md +751 -0
- package/doc/dev/lr-paradigm.md +85 -0
- package/doc/dev/oc.md +582 -0
- package/doc/dev/workflow.md +257 -0
- package/doc/hello_sapdon/hello_sapdon.md +3 -3
- package/doc/user/api/biome.md +558 -0
- package/doc/user/api/block.md +1530 -0
- package/doc/user/api/entity.md +685 -0
- package/doc/user/api/extra.md +231 -0
- package/doc/user/api/item.md +1125 -0
- package/doc/user/api/neo-guidebook.md +409 -0
- package/doc/user/api/recipe.md +427 -0
- package/doc/user/api/sapdon-ui.md +185 -0
- package/doc/user/api/texture.md +181 -0
- package/doc/user/config/build-config.md +83 -0
- package/doc/user/config/mod-info.md +33 -0
- package/doc/user/faq.md +160 -0
- package/doc/user/quick-start.md +122 -0
- package/doc/user/tutorials/block.md +474 -0
- package/doc/user/tutorials/entity.md +356 -0
- package/doc/user/tutorials/item.md +449 -0
- package/doc/user/tutorials/neo-guidebook-experience.md +381 -0
- package/doc/user/tutorials/neo-guidebook.md +640 -0
- package/doc/user/tutorials/recipe.md +278 -0
- package/doc/user/tutorials/sapdon-ui.md +207 -0
- package/package.json +8 -3
- package/prod/cli/index.js +1 -1
- package/prod/cli/start.js +1 -1458
- package/prod/core/index.d.ts +4564 -2166
- package/prod/core/index.js +1 -59015
- package/prod/core/package.json +7 -0
- package/prod/oc/index.d.ts +350 -108
- package/prod/oc/index.js +1 -1
- package/prod/oc/package.json +7 -0
- package/prod/utils/index.d.ts +20 -9
- package/prod/utils/index.js +1 -1
- package/prod/utils/package.json +7 -0
- package/doc/BlockAPI.md +0 -145
- package/doc/api.md +0 -128
- package/doc/oc/index.md +0 -0
- package/doc/sapdon-ts.md +0 -64
- package/src/templates/js_sapdon/build.config +0 -23
- package/src/templates/js_sapdon/main.mjs +0 -4
- package/src/templates/js_sapdon/mod.info +0 -7
- package/src/templates/js_sapdon/pack_icon.png +0 -0
- package/src/templates/js_sapdon/package.json +0 -20
- package/src/templates/js_sapdon/res/animations/animation_item.animation.json +0 -34
- package/src/templates/js_sapdon/res/animations/large_item.animation.json +0 -27
- package/src/templates/js_sapdon/res/models/blocks/crop.geo.json +0 -48
- package/src/templates/js_sapdon/res/models/entity/animation/animation_item.geo.json +0 -26
- package/src/templates/js_sapdon/res/models/entity/animation/large_item.geo.json +0 -28
- package/src/templates/js_sapdon/res/textures/blocks/none.png +0 -0
- package/src/templates/js_sapdon/res/textures/blocks/test_log_oak.png +0 -0
- package/src/templates/js_sapdon/res/textures/blocks/test_log_top.png +0 -0
- package/src/templates/js_sapdon/res/textures/items/masterball.png +0 -0
- package/src/templates/js_sapdon/scripts/custom_components/cropComponent.js +0 -50
- package/src/templates/js_sapdon/scripts/custom_components/items/gui_book.js +0 -37
- package/src/templates/js_sapdon/scripts/custom_components/registry.js +0 -25
- package/src/templates/js_sapdon/scripts/index.js +0 -0
- package/src/templates/ts_sapdon/build.config +0 -23
- package/src/templates/ts_sapdon/main.ts +0 -11
- package/src/templates/ts_sapdon/mod.info +0 -7
- package/src/templates/ts_sapdon/pack_icon.png +0 -0
- package/src/templates/ts_sapdon/package.json +0 -20
- package/src/templates/ts_sapdon/res/models/blocks/crop.geo.json +0 -48
- package/src/templates/ts_sapdon/res/textures/blocks/test_log_oak.png +0 -0
- package/src/templates/ts_sapdon/res/textures/blocks/test_log_top.png +0 -0
- package/src/templates/ts_sapdon/res/textures/items/masterball.png +0 -0
- package/src/templates/ts_sapdon/scripts/components/cropComponent.ts +0 -44
- package/src/templates/ts_sapdon/scripts/components/items/guiBook.ts +0 -36
- package/src/templates/ts_sapdon/scripts/components/registry.ts +0 -24
- package/src/templates/ts_sapdon/scripts/index.ts +0 -7
- package/src/templates/ts_sapdon/tsconfig.json +0 -117
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# NeoGuidebook 指南书教程
|
|
2
|
+
|
|
3
|
+
本教程将带你一步步创建一个带自定义 UI 的技术指南手册。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 目录
|
|
8
|
+
|
|
9
|
+
1. [准备工作](#1-准备工作)
|
|
10
|
+
2. [创建构建入口](#2-创建构建入口)
|
|
11
|
+
3. [定义书页内容](#3-定义书页内容)
|
|
12
|
+
4. [编写运行时脚本](#4-编写运行时脚本)
|
|
13
|
+
5. [注册物品组件](#5-注册物品组件)
|
|
14
|
+
6. [构建与部署](#6-构建与部署)
|
|
15
|
+
7. [完整示例](#7-完整示例)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. 准备工作
|
|
20
|
+
|
|
21
|
+
确保你的项目已经初始化:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm init
|
|
25
|
+
npm install @sapdon/core @sapdon/cli
|
|
26
|
+
npm install @minecraft/server@2.8.0 @minecraft/server-ui@2.1.0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
项目结构:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
my_guidebook/
|
|
33
|
+
├── main.ts # 构建入口(定义 UI 和物品)
|
|
34
|
+
├── tsconfig.json # TypeScript 配置
|
|
35
|
+
├── build.config # 构建配置
|
|
36
|
+
├── mod.info # 模块信息
|
|
37
|
+
├── package.json
|
|
38
|
+
├── scripts/
|
|
39
|
+
│ ├── index.ts # 运行时入口
|
|
40
|
+
│ ├── lib/
|
|
41
|
+
│ │ └── page_bridge.ts # Bridge 导航库
|
|
42
|
+
│ ├── custom_components/
|
|
43
|
+
│ │ ├── registry.ts # 组件注册
|
|
44
|
+
│ │ └── items/
|
|
45
|
+
│ │ └── gui_book.ts # 物品交互
|
|
46
|
+
│ └── page_ids.json # 由构建自动生成
|
|
47
|
+
├── dev/ # 构建输出目录
|
|
48
|
+
│ ├── guidebook_BP/
|
|
49
|
+
│ └── guidebook_RP/
|
|
50
|
+
└── res/ # 资源文件
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### build.config
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"formatVersion": 2,
|
|
58
|
+
"buildOptions": {
|
|
59
|
+
"useHMR": true,
|
|
60
|
+
"buildMode": "dev",
|
|
61
|
+
"buildEntry": "main.ts",
|
|
62
|
+
"scriptEntry": "scripts/index.ts",
|
|
63
|
+
"scriptOutput": "scripts/index.js",
|
|
64
|
+
"useJs": false,
|
|
65
|
+
"buildDir": "dev/",
|
|
66
|
+
"dependencies": [
|
|
67
|
+
{ "module_name": "@minecraft/server-ui", "version": "2.1.0" },
|
|
68
|
+
{ "module_name": "@minecraft/server", "version": "2.8.0" }
|
|
69
|
+
],
|
|
70
|
+
"resource": { "path": "res/", "resourceHints": true }
|
|
71
|
+
},
|
|
72
|
+
"versionType": "release"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### mod.info
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"name": "guidebook",
|
|
81
|
+
"description": "我的技术手册",
|
|
82
|
+
"author": "you",
|
|
83
|
+
"version": "1.0.0",
|
|
84
|
+
"min_engine_version": "1.26.30"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### tsconfig.json
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"compilerOptions": {
|
|
93
|
+
"module": "ESNext",
|
|
94
|
+
"moduleResolution": "bundler",
|
|
95
|
+
"target": "ESNext",
|
|
96
|
+
"strict": true,
|
|
97
|
+
"outDir": "./dev"
|
|
98
|
+
},
|
|
99
|
+
"include": ["scripts/**/*.ts", "main.ts"]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 2. 创建构建入口
|
|
106
|
+
|
|
107
|
+
`main.ts` 在构建时运行,负责定义物品和 UI 布局。
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import fs from "fs"
|
|
111
|
+
import path from "path"
|
|
112
|
+
import {
|
|
113
|
+
ItemAPI, ItemComponent, ItemCategory, registry,
|
|
114
|
+
NeoGuidebook, NeoGuidebookPage
|
|
115
|
+
} from '@sapdon/core'
|
|
116
|
+
|
|
117
|
+
// 2.1 创建物品
|
|
118
|
+
const item = ItemAPI.createItem("my_mod:guidebook", ItemCategory.Items, "book_icon")
|
|
119
|
+
item.format_version = "1.21.90"
|
|
120
|
+
item.addComponent(ItemComponent.setCustomComponentV2("my_mod:book_component", {}))
|
|
121
|
+
item.addComponent(ItemComponent.setMaxStackSize(1))
|
|
122
|
+
item.addComponent(ItemComponent.setDisplayName("技术手册"))
|
|
123
|
+
item.addComponent(ItemComponent.setInteractButton("打开手册"))
|
|
124
|
+
|
|
125
|
+
// 2.2 创建指南书 UI
|
|
126
|
+
const book = new NeoGuidebook("my_mod:guidebook", "ui/", [320, 207], {
|
|
127
|
+
debug: true,
|
|
128
|
+
buttons: { close: { visible: false } },
|
|
129
|
+
textures: {
|
|
130
|
+
homeDefault: "textures/ui/book_shiftleft_default",
|
|
131
|
+
homeHover: "textures/ui/book_shiftleft_hover",
|
|
132
|
+
homePressed: "textures/ui/book_shiftleft_pressed",
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// 2.3 注册页面(见下一节)
|
|
137
|
+
// ...
|
|
138
|
+
|
|
139
|
+
// 2.4 生成 page_ids.json
|
|
140
|
+
const pageIds: string[] = book.getPageIds()
|
|
141
|
+
fs.writeFileSync(
|
|
142
|
+
path.join(process.cwd(), "scripts", "page_ids.json"),
|
|
143
|
+
JSON.stringify(pageIds, null, 2)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// 2.5 提交注册
|
|
147
|
+
registry.submit()
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 3. 定义书页内容
|
|
153
|
+
|
|
154
|
+
### 3.1 创建单页
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const cover = new NeoGuidebookPage("cover")
|
|
158
|
+
.addEmptySpace(["100%", "5%"])
|
|
159
|
+
.addBookTitleBar("技术手册\n by You", ["100%", "15%"])
|
|
160
|
+
.addEmptySpace(["100%", "3%"])
|
|
161
|
+
.addBookText(
|
|
162
|
+
"这里是手册的介绍内容。\n可以跨越多行。",
|
|
163
|
+
["100%", "60%"]
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 3.2 双页布局
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
// 左页
|
|
171
|
+
const ch1_left = new NeoGuidebookPage("ch1_left")
|
|
172
|
+
.addEmptySpace(["100%", "5%"])
|
|
173
|
+
.addCategoryTitle("第一章", ["100%", "15%"])
|
|
174
|
+
.addDivider(["100%", "3%"])
|
|
175
|
+
.addBookText("第一章的内容...", ["100%", "60%"])
|
|
176
|
+
.addEmptySpace(["100%", "12%"])
|
|
177
|
+
|
|
178
|
+
// 右页
|
|
179
|
+
const ch1_right = new NeoGuidebookPage("ch1_right")
|
|
180
|
+
.addEmptySpace(["100%", "5%"])
|
|
181
|
+
.addCategoryTitle("补充说明", ["100%", "15%"])
|
|
182
|
+
.addDivider(["100%", "3%"])
|
|
183
|
+
.addBookText("补充内容...", ["100%", "65%"])
|
|
184
|
+
.addEmptySpace(["100%", "7%"])
|
|
185
|
+
|
|
186
|
+
// 注册为双页
|
|
187
|
+
book.addDoublePageStack("page_index1", ch1_left.getPanel(), ch1_right.getPanel())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 3.3 章节目录
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const chapter_list = new NeoGuidebookPage("chapter_list")
|
|
194
|
+
.addChapters([
|
|
195
|
+
{ chapter_name: "第一章", chapter_texture: "textures/items/map" },
|
|
196
|
+
{ chapter_name: "第二章", chapter_texture: "textures/items/iron_ingot" },
|
|
197
|
+
{ chapter_name: "第三章", chapter_texture: "textures/items/book_writable" },
|
|
198
|
+
])
|
|
199
|
+
.buildChapterList()
|
|
200
|
+
|
|
201
|
+
// 封面 + 目录作为首页
|
|
202
|
+
book.addDoublePageStack("page_index0", cover.getPanel(), chapter_list.getPanel())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 3.4 合成表示例
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
const recipe_page = new NeoGuidebookPage("recipe")
|
|
209
|
+
.addEmptySpace(["100%", "5%"])
|
|
210
|
+
.addCategoryTitle("合成配方", ["100%", "15%"])
|
|
211
|
+
.addDivider(["100%", "3%"])
|
|
212
|
+
.addRecipeGrid(2, 3, [
|
|
213
|
+
"textures/items/iron_ingot",
|
|
214
|
+
"textures/items/iron_ingot",
|
|
215
|
+
"textures/items/iron_ingot",
|
|
216
|
+
"textures/items/stick",
|
|
217
|
+
"textures/items/stick",
|
|
218
|
+
"textures/items/stick",
|
|
219
|
+
], ["100%", "40%"])
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 3.5 注册所有页面
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// 按 page_index0 ~ page_indexN 顺序注册
|
|
226
|
+
book.addDoublePageStack("page_index0", cover.getPanel(), chapter_list.getPanel())
|
|
227
|
+
book.addDoublePageStack("page_index1", ch1_left.getPanel(), ch1_right.getPanel())
|
|
228
|
+
book.addSinglePageStack("page_index2", recipe_page.getPanel())
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 4. 编写运行时脚本
|
|
234
|
+
|
|
235
|
+
### 4.1 page_bridge.ts(导航管理器)
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { ActionFormData, ActionFormResponse } from "@minecraft/server-ui"
|
|
239
|
+
import { world, Player } from "@minecraft/server"
|
|
240
|
+
|
|
241
|
+
interface PageCallbacks {
|
|
242
|
+
onEnter?: (pageId: string, index: number) => void
|
|
243
|
+
onLeave?: (pageId: string, index: number) => void
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface BridgeOptions {
|
|
247
|
+
debug?: boolean
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export class NeoGuidebookBridge {
|
|
251
|
+
private uiName: string
|
|
252
|
+
private pageIds: string[]
|
|
253
|
+
private totalPages: number
|
|
254
|
+
private hooks: Map<string, PageCallbacks>
|
|
255
|
+
private currentIndex: number
|
|
256
|
+
private debug: boolean
|
|
257
|
+
|
|
258
|
+
constructor(uiName: string, pageIds: string[], options: BridgeOptions = {}) {
|
|
259
|
+
this.uiName = uiName
|
|
260
|
+
this.pageIds = pageIds
|
|
261
|
+
this.totalPages = pageIds.length
|
|
262
|
+
this.hooks = new Map()
|
|
263
|
+
this.currentIndex = 0
|
|
264
|
+
this.debug = options.debug === true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private log(...args: string[]): void {
|
|
268
|
+
if (this.debug) world.sendMessage("[Guidebook] " + args.join(" "))
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
onPage(pageId: string, callbacks: PageCallbacks): this {
|
|
272
|
+
this.hooks.set(pageId, callbacks)
|
|
273
|
+
return this
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
show(player: Player, startIndex: number = 0): void {
|
|
277
|
+
this.currentIndex = Math.max(0, Math.min(startIndex, this.totalPages - 1))
|
|
278
|
+
this.open(player)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
open(player: Player): void {
|
|
282
|
+
const pageId = this.pageIds[this.currentIndex]
|
|
283
|
+
const hook = this.hooks.get(pageId)
|
|
284
|
+
if (hook?.onEnter) hook.onEnter(pageId, this.currentIndex)
|
|
285
|
+
|
|
286
|
+
const form = new ActionFormData()
|
|
287
|
+
.title(this.uiName)
|
|
288
|
+
.body(pageId)
|
|
289
|
+
|
|
290
|
+
const actions: string[] = []
|
|
291
|
+
const hasPrev = this.currentIndex > 0
|
|
292
|
+
const hasNext = this.currentIndex < this.totalPages - 1
|
|
293
|
+
const hasHome = this.currentIndex !== 0
|
|
294
|
+
const isChapterList = this.currentIndex === 0
|
|
295
|
+
|
|
296
|
+
// 按钮文字必须匹配 JSON UI 的 $binding_button_text
|
|
297
|
+
if (hasPrev) { form.button("prev_button"); actions.push("prev") }
|
|
298
|
+
if (hasNext) { form.button("next_button"); actions.push("next") }
|
|
299
|
+
if (hasHome) { form.button("home_button"); actions.push("home") }
|
|
300
|
+
|
|
301
|
+
// 首页显示章节选择按钮
|
|
302
|
+
if (isChapterList) {
|
|
303
|
+
for (let i = 1; i < this.totalPages; i++) {
|
|
304
|
+
form.button(`item_${i - 1}_button`)
|
|
305
|
+
actions.push(`goto:${i}`)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
form.show(player).then((response: ActionFormResponse) => {
|
|
310
|
+
if (response.canceled) {
|
|
311
|
+
if (hook?.onLeave) hook.onLeave(pageId, this.currentIndex)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const action = actions[response.selection!]
|
|
316
|
+
if (!action) return
|
|
317
|
+
|
|
318
|
+
const prev = this.currentIndex
|
|
319
|
+
if (action === "prev") this.currentIndex--
|
|
320
|
+
else if (action === "next") this.currentIndex++
|
|
321
|
+
else if (action === "home") this.currentIndex = 0
|
|
322
|
+
else if (action.startsWith("goto:"))
|
|
323
|
+
this.currentIndex = parseInt(action.split(":")[1], 10)
|
|
324
|
+
|
|
325
|
+
if (hook?.onLeave) hook.onLeave(pageId, prev)
|
|
326
|
+
if (this.currentIndex !== prev) this.open(player)
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
> **重要**:`form.button()` 的参数**不是显示文字**,而是 JSON UI 绑定的键名。必须与 `neo_guidebook.json` 中对应按钮的 `$binding_button_text` 一致。
|
|
333
|
+
|
|
334
|
+
### 4.2 gui_book.ts(物品交互)
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { world, ItemCustomComponent, ItemComponentUseEvent } from "@minecraft/server"
|
|
338
|
+
import { NeoGuidebookBridge } from "../lib/page_bridge"
|
|
339
|
+
import pageIds from "../page_ids.json"
|
|
340
|
+
|
|
341
|
+
const DEBUG: boolean = true
|
|
342
|
+
|
|
343
|
+
const bridge = new NeoGuidebookBridge("sapdon_ui:guidebook", pageIds, { debug: DEBUG })
|
|
344
|
+
|
|
345
|
+
export const GuiBookItemComponent: ItemCustomComponent = {
|
|
346
|
+
onUse(event: ItemComponentUseEvent, params: any): void {
|
|
347
|
+
const player = event.source
|
|
348
|
+
if (player.typeId != "minecraft:player") return
|
|
349
|
+
bridge.show(player, 0)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 5. 注册物品组件
|
|
357
|
+
|
|
358
|
+
### 5.1 registry.ts
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { GuiBookItemComponent } from "./items/gui_book"
|
|
362
|
+
import { system, world, StartupEvent } from "@minecraft/server"
|
|
363
|
+
|
|
364
|
+
const DEBUG: boolean = false
|
|
365
|
+
let registered: boolean = false
|
|
366
|
+
|
|
367
|
+
export const registerCustomItemComponent = (): void => {
|
|
368
|
+
system.beforeEvents.startup.subscribe((initEvent: StartupEvent) => {
|
|
369
|
+
if (registered) return
|
|
370
|
+
registered = true
|
|
371
|
+
if (DEBUG) world.sendMessage("[Registry] registering my_mod:book_component")
|
|
372
|
+
initEvent.itemComponentRegistry.registerCustomComponent(
|
|
373
|
+
"my_mod:book_component",
|
|
374
|
+
GuiBookItemComponent
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
> `registerCustomComponent` 的第一个参数**必须**与 `main.ts` 中 `setCustomComponentV2()` 的第一个参数一致。
|
|
381
|
+
|
|
382
|
+
### 5.2 scripts/index.ts(运行时入口)
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { registerCustomItemComponent } from "./custom_components/registry"
|
|
386
|
+
registerCustomItemComponent()
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 6. 构建与部署
|
|
392
|
+
|
|
393
|
+
### 6.1 构建
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
npm run build
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
构建完成后:
|
|
400
|
+
- `dev/guidebook_BP/` — 行为包(manifest.json + scripts/ + items/)
|
|
401
|
+
- `dev/guidebook_RP/` — 资源包(manifest.json + ui/ + textures/)
|
|
402
|
+
|
|
403
|
+
### 6.2 部署
|
|
404
|
+
|
|
405
|
+
将 `dev/guidebook_BP` 和 `dev/guidebook_RP` 复制到 Minecraft 的 `development_behavior_packs` / `development_resource_packs` 目录:
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
C:/Users/你的用户名/AppData/Local/Packages/
|
|
409
|
+
Microsoft.MinecraftUWP_8wekyb3d8bbwe/LocalState/games/
|
|
410
|
+
com.mojang/development_behavior_packs/guidebook_BP/
|
|
411
|
+
com.mojang/development_resource_packs/guidebook_RP/
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### 6.3 在游戏中启用
|
|
415
|
+
|
|
416
|
+
1. 创建一个新世界或打开现有世界
|
|
417
|
+
2. 设置 → 行为包 → 添加 `guidebook_BP`
|
|
418
|
+
3. 设置 → 资源包 → 添加 `guidebook_RP`
|
|
419
|
+
4. 进入游戏,获取物品:`/give @s sapdon:neo_guidebook`
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## 7. 完整示例
|
|
424
|
+
|
|
425
|
+
以下是一个完整的最小化指南书示例:
|
|
426
|
+
|
|
427
|
+
### main.ts
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
import fs from "fs"
|
|
431
|
+
import path from "path"
|
|
432
|
+
import {
|
|
433
|
+
ItemAPI, ItemComponent, ItemCategory, registry,
|
|
434
|
+
NeoGuidebook, NeoGuidebookPage
|
|
435
|
+
} from '@sapdon/core'
|
|
436
|
+
|
|
437
|
+
// 物品
|
|
438
|
+
const item = ItemAPI.createItem("my_mod:guidebook", ItemCategory.Items, "book_icon")
|
|
439
|
+
item.format_version = "1.21.90"
|
|
440
|
+
item.addComponent(ItemComponent.setCustomComponentV2("my_mod:book_component", {}))
|
|
441
|
+
item.addComponent(ItemComponent.setMaxStackSize(1))
|
|
442
|
+
item.addComponent(ItemComponent.setDisplayName("快速入门手册"))
|
|
443
|
+
item.addComponent(ItemComponent.setInteractButton("打开"))
|
|
444
|
+
|
|
445
|
+
// 指南书
|
|
446
|
+
const book = new NeoGuidebook("my_mod:guidebook", "ui/", [320, 207])
|
|
447
|
+
|
|
448
|
+
// 封面
|
|
449
|
+
const cover = new NeoGuidebookPage("cover")
|
|
450
|
+
.addEmptySpace(["100%", "5%"])
|
|
451
|
+
.addBookTitleBar("快速入门手册", ["100%", "15%"])
|
|
452
|
+
.addEmptySpace(["100%", "3%"])
|
|
453
|
+
.addBookText("欢迎使用快速入门手册!", ["100%", "60%"])
|
|
454
|
+
|
|
455
|
+
// 第一章
|
|
456
|
+
const ch1 = new NeoGuidebookPage("ch1")
|
|
457
|
+
.addEmptySpace(["100%", "5%"])
|
|
458
|
+
.addCategoryTitle("第一章:基础", ["100%", "15%"])
|
|
459
|
+
.addDivider(["100%", "3%"])
|
|
460
|
+
.addBookText("这是第一章的内容。", ["100%", "72%"])
|
|
461
|
+
|
|
462
|
+
// 第二章
|
|
463
|
+
const ch2 = new NeoGuidebookPage("ch2")
|
|
464
|
+
.addEmptySpace(["100%", "5%"])
|
|
465
|
+
.addCategoryTitle("第二章:进阶", ["100%", "15%"])
|
|
466
|
+
.addDivider(["100%", "3%"])
|
|
467
|
+
.addBookText("这是第二章的内容。", ["100%", "72%"])
|
|
468
|
+
|
|
469
|
+
// 注册页面
|
|
470
|
+
book.addDoublePageStack("page_index0", cover.getPanel(), ch1.getPanel())
|
|
471
|
+
book.addDoublePageStack("page_index1", ch2.getPanel(), ch2.getPanel())
|
|
472
|
+
|
|
473
|
+
// 输出 page_ids
|
|
474
|
+
fs.writeFileSync(
|
|
475
|
+
path.join(process.cwd(), "scripts", "page_ids.json"),
|
|
476
|
+
JSON.stringify(book.getPageIds(), null, 2)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
registry.submit()
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### 运行时文件
|
|
483
|
+
|
|
484
|
+
`scripts/index.ts`:
|
|
485
|
+
```typescript
|
|
486
|
+
import { registerCustomItemComponent } from "./custom_components/registry"
|
|
487
|
+
registerCustomItemComponent()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
`scripts/custom_components/registry.ts`:
|
|
491
|
+
```typescript
|
|
492
|
+
import { GuiBookItemComponent } from "./items/gui_book"
|
|
493
|
+
import { system, StartupEvent } from "@minecraft/server"
|
|
494
|
+
|
|
495
|
+
let registered: boolean = false
|
|
496
|
+
|
|
497
|
+
export const registerCustomItemComponent = (): void => {
|
|
498
|
+
system.beforeEvents.startup.subscribe((initEvent: StartupEvent) => {
|
|
499
|
+
if (registered) return
|
|
500
|
+
registered = true
|
|
501
|
+
initEvent.itemComponentRegistry.registerCustomComponent(
|
|
502
|
+
"my_mod:book_component",
|
|
503
|
+
GuiBookItemComponent
|
|
504
|
+
)
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
`scripts/custom_components/items/gui_book.ts`:
|
|
510
|
+
```typescript
|
|
511
|
+
import { world, ItemCustomComponent, ItemComponentUseEvent } from "@minecraft/server"
|
|
512
|
+
import { NeoGuidebookBridge } from "../lib/page_bridge"
|
|
513
|
+
import pageIds from "../../page_ids.json"
|
|
514
|
+
|
|
515
|
+
const bridge = new NeoGuidebookBridge("sapdon_ui:guidebook", pageIds)
|
|
516
|
+
|
|
517
|
+
export const GuiBookItemComponent: ItemCustomComponent = {
|
|
518
|
+
onUse(event: ItemComponentUseEvent, params: any): void {
|
|
519
|
+
const player = event.source
|
|
520
|
+
if (player.typeId != "minecraft:player") return
|
|
521
|
+
bridge.show(player, 0)
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`scripts/lib/page_bridge.ts`:
|
|
527
|
+
```typescript
|
|
528
|
+
import { ActionFormData, ActionFormResponse } from "@minecraft/server-ui"
|
|
529
|
+
import { world, Player } from "@minecraft/server"
|
|
530
|
+
|
|
531
|
+
interface PageCallbacks {
|
|
532
|
+
onEnter?: (pageId: string, index: number) => void
|
|
533
|
+
onLeave?: (pageId: string, index: number) => void
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
interface BridgeOptions {
|
|
537
|
+
debug?: boolean
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export class NeoGuidebookBridge {
|
|
541
|
+
private uiName: string
|
|
542
|
+
private pageIds: string[]
|
|
543
|
+
private totalPages: number
|
|
544
|
+
private hooks: Map<string, PageCallbacks>
|
|
545
|
+
private currentIndex: number
|
|
546
|
+
private debug: boolean
|
|
547
|
+
|
|
548
|
+
constructor(uiName: string, pageIds: string[], options: BridgeOptions = {}) {
|
|
549
|
+
this.uiName = uiName
|
|
550
|
+
this.pageIds = pageIds
|
|
551
|
+
this.totalPages = pageIds.length
|
|
552
|
+
this.hooks = new Map()
|
|
553
|
+
this.currentIndex = 0
|
|
554
|
+
this.debug = options.debug === true
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private log(...args: string[]): void {
|
|
558
|
+
if (this.debug) world.sendMessage("[Guidebook] " + args.join(" "))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
onPage(pageId: string, callbacks: PageCallbacks): this {
|
|
562
|
+
this.hooks.set(pageId, callbacks)
|
|
563
|
+
return this
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
show(player: Player, startIndex: number = 0): void {
|
|
567
|
+
this.currentIndex = Math.max(0, Math.min(startIndex, this.totalPages - 1))
|
|
568
|
+
this.open(player)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
open(player: Player): void {
|
|
572
|
+
const pageId = this.pageIds[this.currentIndex]
|
|
573
|
+
const hook = this.hooks.get(pageId)
|
|
574
|
+
if (hook?.onEnter) hook.onEnter(pageId, this.currentIndex)
|
|
575
|
+
|
|
576
|
+
const form = new ActionFormData()
|
|
577
|
+
.title(this.uiName)
|
|
578
|
+
.body(pageId)
|
|
579
|
+
|
|
580
|
+
const actions: string[] = []
|
|
581
|
+
const hasPrev = this.currentIndex > 0
|
|
582
|
+
const hasNext = this.currentIndex < this.totalPages - 1
|
|
583
|
+
const hasHome = this.currentIndex !== 0
|
|
584
|
+
const isChapterList = this.currentIndex === 0
|
|
585
|
+
|
|
586
|
+
if (hasPrev) { form.button("prev_button"); actions.push("prev") }
|
|
587
|
+
if (hasNext) { form.button("next_button"); actions.push("next") }
|
|
588
|
+
if (hasHome) { form.button("home_button"); actions.push("home") }
|
|
589
|
+
|
|
590
|
+
if (isChapterList) {
|
|
591
|
+
for (let i = 1; i < this.totalPages; i++) {
|
|
592
|
+
form.button(`item_${i - 1}_button`)
|
|
593
|
+
actions.push(`goto:${i}`)
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
form.show(player).then((response: ActionFormResponse) => {
|
|
598
|
+
if (response.canceled) {
|
|
599
|
+
if (hook?.onLeave) hook.onLeave(pageId, this.currentIndex)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const action = actions[response.selection!]
|
|
604
|
+
if (!action) return
|
|
605
|
+
|
|
606
|
+
const prev = this.currentIndex
|
|
607
|
+
if (action === "prev") this.currentIndex--
|
|
608
|
+
else if (action === "next") this.currentIndex++
|
|
609
|
+
else if (action === "home") this.currentIndex = 0
|
|
610
|
+
else if (action.startsWith("goto:"))
|
|
611
|
+
this.currentIndex = parseInt(action.split(":")[1], 10)
|
|
612
|
+
|
|
613
|
+
if (hook?.onLeave) hook.onLeave(pageId, prev)
|
|
614
|
+
if (this.currentIndex !== prev) this.open(player)
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## 常见问题
|
|
623
|
+
|
|
624
|
+
### Q: 为什么按钮不显示?
|
|
625
|
+
|
|
626
|
+
检查 `form.button()` 的参数是否与 JSON UI 中 `$binding_button_text` 一致。按钮文字**不是**显示文本,而是绑定键名。
|
|
627
|
+
|
|
628
|
+
### Q: 为什么显示原版表单而不是自定义 UI?
|
|
629
|
+
|
|
630
|
+
检查 `title()` 的参数与 `new NeoGuidebook()` 的 identifier 是否一致。
|
|
631
|
+
|
|
632
|
+
### Q: 翻页后页面空白?
|
|
633
|
+
|
|
634
|
+
检查 `body()` 的参数是否与注册时的 `page_id` 完全一致。
|
|
635
|
+
|
|
636
|
+
### Q: 物品拿在手上右键没反应?
|
|
637
|
+
|
|
638
|
+
1. 检查 manifest.json 的依赖版本是否正确(`2.8.0` / `2.1.0`)
|
|
639
|
+
2. 检查注册的组件名是否与物品 JSON 中的一致
|
|
640
|
+
3. 检查控制台是否有版本冲突错误
|