@occultus/article-api 0.23.0 → 0.24.0-beta.2
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/package.json +1 -1
- package/src/api/Article.ts +61 -31
- package/src/api/ArticleCenter.ts +17 -11
- package/src/api/Bindings/ArticleServerBindings.ts +139 -0
- package/src/api/Bindings/BindingConfig.ts +12 -0
- package/src/api/Bindings/ComponentParams.ts +10 -0
- package/src/index.ts +3 -0
- package/src/lib/utils.ts +88 -1
package/package.json
CHANGED
package/src/api/Article.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { FormLike } from "@occultus/core";
|
|
|
10
10
|
* @since 1.0.0
|
|
11
11
|
*/
|
|
12
12
|
export class Article extends FormLike {
|
|
13
|
+
static DYNAMIC_DATA_ID = "starock:unlocked_articles";
|
|
13
14
|
/**
|
|
14
15
|
* @param id 文章ID
|
|
15
16
|
* @param title 文章标题
|
|
@@ -46,15 +47,18 @@ export class Article extends FormLike {
|
|
|
46
47
|
.title(title)
|
|
47
48
|
.body(body)
|
|
48
49
|
.button({ translate: "gui.ok" });
|
|
49
|
-
mainForm
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
mainForm
|
|
51
|
+
.show(player)
|
|
52
|
+
.then((response) => {
|
|
53
|
+
if (response.canceled || response.selection === 0) {
|
|
54
|
+
this.quit(player, backTo);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
console.error(`[Article] Failed to display article: ${err}`);
|
|
60
|
+
this.quit(player, backTo as readonly FormLike[]);
|
|
61
|
+
});
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
60
64
|
* 向玩家展示文章(文章有章节时)
|
|
@@ -71,25 +75,28 @@ export class Article extends FormLike {
|
|
|
71
75
|
parseText(this.body, player)
|
|
72
76
|
];
|
|
73
77
|
const contentsForm = new ActionFormData().title(title).body(body);
|
|
74
|
-
this.chapters
|
|
78
|
+
this.chapters?.forEach((chapter) => {
|
|
75
79
|
const chapterTitle = parseText(chapter.title, player);
|
|
76
80
|
contentsForm.button(chapterTitle, chapter.iconPath);
|
|
77
81
|
});
|
|
78
|
-
contentsForm
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
contentsForm
|
|
83
|
+
.show(player)
|
|
84
|
+
.then((response) => {
|
|
85
|
+
if (response.canceled || response.selection === undefined) {
|
|
86
|
+
this.quit(player, backTo);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const [chapterTitle, chapterBody] = [
|
|
90
|
+
parseText(this.chapters![response.selection].title, player),
|
|
91
|
+
parseText(this.chapters![response.selection].body, player)
|
|
92
|
+
];
|
|
93
|
+
const chapter = new Article("temp:chapter", chapterTitle, chapterBody);
|
|
94
|
+
this.jumpTo(player, chapter, backTo);
|
|
95
|
+
})
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
console.error(`[Article] Failed to display article: ${err}`);
|
|
98
|
+
this.quit(player, backTo as readonly FormLike[]);
|
|
99
|
+
});
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* 向玩家展示文章
|
|
@@ -106,10 +113,26 @@ export class Article extends FormLike {
|
|
|
106
113
|
/**
|
|
107
114
|
* 在文章中心里解锁文章
|
|
108
115
|
* @param player
|
|
116
|
+
* @return `true`如果文章需要解锁,并且成功解锁;否则返回`false`
|
|
109
117
|
*/
|
|
110
|
-
unlock(player: Player):
|
|
111
|
-
if (this.needUnlock)
|
|
112
|
-
|
|
118
|
+
unlock(player: Player): boolean {
|
|
119
|
+
if (!this.needUnlock || this.checkUnlock(player)) return false;
|
|
120
|
+
try {
|
|
121
|
+
const raw = player.getDynamicProperty(Article.DYNAMIC_DATA_ID);
|
|
122
|
+
let unlockedIds: string[] = [];
|
|
123
|
+
if (raw && typeof raw === "string") {
|
|
124
|
+
const parsed = JSON.parse(raw);
|
|
125
|
+
unlockedIds = Array.isArray(parsed) ? parsed : [];
|
|
126
|
+
}
|
|
127
|
+
player.setDynamicProperty(
|
|
128
|
+
Article.DYNAMIC_DATA_ID,
|
|
129
|
+
JSON.stringify([...unlockedIds, this.id])
|
|
130
|
+
);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error(`Failed to unlock article ${this.id}:`, err);
|
|
134
|
+
player.setDynamicProperty(Article.DYNAMIC_DATA_ID, "[]");
|
|
135
|
+
return false;
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
/**
|
|
@@ -117,9 +140,16 @@ export class Article extends FormLike {
|
|
|
117
140
|
* @param player
|
|
118
141
|
*/
|
|
119
142
|
checkUnlock(player: Player): boolean {
|
|
120
|
-
if (this.needUnlock)
|
|
121
|
-
|
|
143
|
+
if (!this.needUnlock) return true;
|
|
144
|
+
try {
|
|
145
|
+
const raw = player.getDynamicProperty(Article.DYNAMIC_DATA_ID);
|
|
146
|
+
if (!raw || typeof raw !== "string") return false;
|
|
147
|
+
const json = JSON.parse(raw);
|
|
148
|
+
if (!Array.isArray(json)) return false;
|
|
149
|
+
return json.includes(this.id);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`Failed to check status of article ${this.id}:`, err);
|
|
122
152
|
}
|
|
123
|
-
return
|
|
153
|
+
return false;
|
|
124
154
|
}
|
|
125
155
|
}
|
package/src/api/ArticleCenter.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Player } from "@minecraft/server";
|
|
2
2
|
import { MessageFormData } from "@minecraft/server-ui";
|
|
3
3
|
import { ArticleRegistries } from "./Base/ArticleRegistries";
|
|
4
|
-
import {
|
|
5
|
-
import { generateContentForm } from "../lib/utils";
|
|
4
|
+
import { autoGetArticles, generateContentForm } from "../lib/utils";
|
|
6
5
|
import { TextProvider } from "@occultus/text-api";
|
|
7
|
-
import { FormLike } from "@occultus/core";
|
|
6
|
+
import { FormLike, OccultusSDKError } from "@occultus/core";
|
|
8
7
|
import { ReadableArticle } from "../types/ReadableArticle";
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -32,14 +31,14 @@ export class ArticleCenter extends FormLike {
|
|
|
32
31
|
* @param id 文章中心ID
|
|
33
32
|
* @param title 文章中心标题
|
|
34
33
|
* @param body 文章中心内容
|
|
35
|
-
* @param
|
|
34
|
+
* @param articleSource 文章来源,如果为`true`,则会从该脚本环境注册的所有文章中显示已经解锁的文章,反之则显示无文章提示,如果为文章列表,则只从提供的文章中显示已解锁的文章
|
|
36
35
|
*/
|
|
37
36
|
constructor(
|
|
38
37
|
public readonly id: string,
|
|
39
38
|
public title: TextProvider,
|
|
40
39
|
public body: TextProvider,
|
|
41
|
-
public
|
|
42
|
-
public readonly bindTo
|
|
40
|
+
public articleSource: ReadableArticle[] | "registry" | "auto",
|
|
41
|
+
public readonly bindTo?: ArticleRegistries,
|
|
43
42
|
public iconPath?: string
|
|
44
43
|
) {
|
|
45
44
|
super();
|
|
@@ -53,7 +52,7 @@ export class ArticleCenter extends FormLike {
|
|
|
53
52
|
player,
|
|
54
53
|
this.title,
|
|
55
54
|
this.body,
|
|
56
|
-
this.getAvailableArticles()
|
|
55
|
+
this.getAvailableArticles(player)
|
|
57
56
|
);
|
|
58
57
|
|
|
59
58
|
if (!contentForm) {
|
|
@@ -76,12 +75,19 @@ export class ArticleCenter extends FormLike {
|
|
|
76
75
|
this.quit(player, backTo as readonly FormLike[]);
|
|
77
76
|
});
|
|
78
77
|
}
|
|
79
|
-
private getAvailableArticles(): ReadableArticle[] {
|
|
80
|
-
if (
|
|
81
|
-
if (!this.
|
|
78
|
+
private getAvailableArticles(player: Player): ReadableArticle[] {
|
|
79
|
+
if (this.articleSource === "registry") {
|
|
80
|
+
if (!this.bindTo) {
|
|
81
|
+
throw new OccultusSDKError(
|
|
82
|
+
"You must bind article registries to the center when using the boolean parameter!"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
82
85
|
return this.bindTo.getAll();
|
|
83
86
|
}
|
|
84
|
-
|
|
87
|
+
if (this.articleSource === "auto") {
|
|
88
|
+
return autoGetArticles(player);
|
|
89
|
+
}
|
|
90
|
+
return this.articleSource;
|
|
85
91
|
}
|
|
86
92
|
/**
|
|
87
93
|
* 当没有可用的文章时,向玩家显示警告
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { StartupEvent, system } from "@minecraft/server";
|
|
2
|
+
import { ArticleBindingConfig } from "./BindingConfig";
|
|
3
|
+
import { OccultusSDKError } from "@occultus/core";
|
|
4
|
+
import { TextProvider } from "@occultus/text-api";
|
|
5
|
+
import { ChapterData } from "../../interface/ChapterData";
|
|
6
|
+
import { ReadableArticle } from "../../types/ReadableArticle";
|
|
7
|
+
import { ArticleCenter } from "../ArticleCenter";
|
|
8
|
+
import {
|
|
9
|
+
getArticle,
|
|
10
|
+
} from "../../lib/utils";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 文章内容类型别名
|
|
14
|
+
*/
|
|
15
|
+
export type ArticleContent =
|
|
16
|
+
| TextProvider
|
|
17
|
+
| { chapter: ChapterData[]; description: TextProvider };
|
|
18
|
+
|
|
19
|
+
export class ArticleServerBindings {
|
|
20
|
+
private static instance: ArticleServerBindings | undefined;
|
|
21
|
+
private static center: ArticleCenter | undefined;
|
|
22
|
+
private preloadedContent: Map<string, ArticleContent> = new Map();
|
|
23
|
+
private articles: Map<string, ReadableArticle> = new Map();
|
|
24
|
+
private initCenter(config: ArticleBindingConfig) {
|
|
25
|
+
const center = new ArticleCenter(
|
|
26
|
+
"starock:bindings.article_center",
|
|
27
|
+
config.centerConfig.title,
|
|
28
|
+
config.centerConfig.description,
|
|
29
|
+
"auto",
|
|
30
|
+
undefined,
|
|
31
|
+
config.centerConfig.icon_path
|
|
32
|
+
);
|
|
33
|
+
ArticleServerBindings.center = center;
|
|
34
|
+
}
|
|
35
|
+
private registryArticleComponent(
|
|
36
|
+
event: StartupEvent,
|
|
37
|
+
config: ArticleBindingConfig
|
|
38
|
+
) {
|
|
39
|
+
const self = this;
|
|
40
|
+
event.itemComponentRegistry.registerCustomComponent(config.componentName, {
|
|
41
|
+
onUse(event) {
|
|
42
|
+
const { itemStack, source } = event;
|
|
43
|
+
if (!itemStack) return;
|
|
44
|
+
if (self.articles.has(itemStack.typeId)) {
|
|
45
|
+
self.articles.get(itemStack.typeId)?.display(source, []);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const article = getArticle(itemStack);
|
|
49
|
+
self.articles.set(itemStack.typeId, article);
|
|
50
|
+
article.display(source, []);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
private registryArticleContentComponent(
|
|
55
|
+
event: StartupEvent,
|
|
56
|
+
config: ArticleBindingConfig
|
|
57
|
+
) {
|
|
58
|
+
event.itemComponentRegistry.registerCustomComponent(
|
|
59
|
+
config.contentComponentName,
|
|
60
|
+
{}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
private registryArticleCenterContent(
|
|
64
|
+
event: StartupEvent,
|
|
65
|
+
config: ArticleBindingConfig
|
|
66
|
+
) {
|
|
67
|
+
event.itemComponentRegistry.registerCustomComponent(
|
|
68
|
+
config.centerComponentName,
|
|
69
|
+
{
|
|
70
|
+
onUse(event) {
|
|
71
|
+
const { itemStack, source } = event;
|
|
72
|
+
if (!itemStack) return;
|
|
73
|
+
const center = ArticleServerBindings.center;
|
|
74
|
+
if (!center) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
center.display(source, []);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
private constructor(public config: ArticleBindingConfig) {
|
|
83
|
+
if (!config) {
|
|
84
|
+
throw new OccultusSDKError("ArticleBindingConfig is required!");
|
|
85
|
+
}
|
|
86
|
+
system.beforeEvents.startup.subscribe((event) => {
|
|
87
|
+
this.registryArticleComponent(event, config);
|
|
88
|
+
this.registryArticleContentComponent(event, config);
|
|
89
|
+
this.initCenter(config);
|
|
90
|
+
this.registryArticleCenterContent(event, config);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
static create(config: ArticleBindingConfig): void {
|
|
94
|
+
if (ArticleServerBindings.instance) {
|
|
95
|
+
throw new OccultusSDKError("ArticleServerBindings already created!");
|
|
96
|
+
}
|
|
97
|
+
ArticleServerBindings.instance = new ArticleServerBindings(config);
|
|
98
|
+
}
|
|
99
|
+
static getInstance(): ArticleServerBindings {
|
|
100
|
+
if (!ArticleServerBindings.instance) {
|
|
101
|
+
throw new OccultusSDKError("ArticleServerBindings not created!");
|
|
102
|
+
}
|
|
103
|
+
return ArticleServerBindings.instance;
|
|
104
|
+
}
|
|
105
|
+
static getCenter(): ArticleCenter {
|
|
106
|
+
if (!ArticleServerBindings.center) {
|
|
107
|
+
throw new OccultusSDKError("ArticleCenter not found!");
|
|
108
|
+
}
|
|
109
|
+
return ArticleServerBindings.center;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 预载文章内容
|
|
113
|
+
* @param id
|
|
114
|
+
* @param content
|
|
115
|
+
*/
|
|
116
|
+
preloadContent(
|
|
117
|
+
id: string,
|
|
118
|
+
content:
|
|
119
|
+
| TextProvider
|
|
120
|
+
| { chapter: ChapterData[]; description: TextProvider }
|
|
121
|
+
) {
|
|
122
|
+
if (!id || !content) {
|
|
123
|
+
console.warn(`Preload data failed with id: ${id}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.preloadedContent.set(id, content);
|
|
127
|
+
}
|
|
128
|
+
readPreloadedContent(
|
|
129
|
+
id: string
|
|
130
|
+
):
|
|
131
|
+
| TextProvider
|
|
132
|
+
| { chapter: ChapterData[]; description: TextProvider }
|
|
133
|
+
| undefined {
|
|
134
|
+
return this.preloadedContent.get(id);
|
|
135
|
+
}
|
|
136
|
+
getArticle(id: string): ReadableArticle | undefined {
|
|
137
|
+
return this.articles.get(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RawMessage } from "@minecraft/server";
|
|
2
|
+
|
|
3
|
+
export type ArticleBindingConfig = {
|
|
4
|
+
componentName: string;
|
|
5
|
+
centerComponentName: string;
|
|
6
|
+
contentComponentName: string;
|
|
7
|
+
centerConfig: {
|
|
8
|
+
title: RawMessage | string;
|
|
9
|
+
description: RawMessage | string;
|
|
10
|
+
icon_path?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RawMessage } from "@minecraft/server";
|
|
2
|
+
|
|
3
|
+
export type ArticleComponentParams = {
|
|
4
|
+
title: RawMessage | string;
|
|
5
|
+
icon_path?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ArticleContentParams = RawMessage[] | string;
|
|
9
|
+
|
|
10
|
+
export type ArticleCenterComponentParams = {};
|
package/src/index.ts
CHANGED
|
@@ -6,3 +6,6 @@ export * from "./api/Article";
|
|
|
6
6
|
export * from "./api/ArticleCenter";
|
|
7
7
|
export * from "./api/Base/ArticleRegistries";
|
|
8
8
|
export * from "./interface/ChapterData";
|
|
9
|
+
export * from "./api/Bindings/ArticleServerBindings";
|
|
10
|
+
export * from "./api/Bindings/BindingConfig";
|
|
11
|
+
export * from "./api/Bindings/ComponentParams";
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
import { Player, StartupEvent } from "@minecraft/server";
|
|
1
|
+
import { ItemStack, Player, RawMessage, StartupEvent } from "@minecraft/server";
|
|
2
2
|
import { ActionFormData } from "@minecraft/server-ui";
|
|
3
3
|
import { Article } from "../api/Article";
|
|
4
4
|
import { ReadableArticle } from "../types/ReadableArticle";
|
|
5
5
|
import { TextProvider, parseText } from "@occultus/text-api";
|
|
6
|
+
import { ArticleBindingConfig } from "../api/Bindings/BindingConfig";
|
|
7
|
+
import {
|
|
8
|
+
ArticleComponentParams,
|
|
9
|
+
ArticleContentParams
|
|
10
|
+
} from "../api/Bindings/ComponentParams";
|
|
11
|
+
import {
|
|
12
|
+
ArticleContent,
|
|
13
|
+
ArticleServerBindings
|
|
14
|
+
} from "../api/Bindings/ArticleServerBindings";
|
|
15
|
+
import { ArticleCenter } from "../api/ArticleCenter";
|
|
16
|
+
import { OccultusSDKError } from "@occultus/core";
|
|
6
17
|
|
|
7
18
|
/**
|
|
8
19
|
* 根据提供的资源生成目录表单
|
|
@@ -57,3 +68,79 @@ export function articleRegister(
|
|
|
57
68
|
}
|
|
58
69
|
});
|
|
59
70
|
}
|
|
71
|
+
|
|
72
|
+
export function parseDescription(
|
|
73
|
+
text: ArticleContent | RawMessage[]
|
|
74
|
+
): TextProvider {
|
|
75
|
+
if (typeof text === "string") return text;
|
|
76
|
+
if (Array.isArray(text)) {
|
|
77
|
+
return {
|
|
78
|
+
rawtext: text
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if ("description" in text) {
|
|
82
|
+
return text.description;
|
|
83
|
+
}
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getArticleContent(item: ItemStack) {
|
|
88
|
+
const config = ArticleServerBindings.getInstance().config;
|
|
89
|
+
const params = item.getComponent(config.contentComponentName)
|
|
90
|
+
?.customComponentParameters.params as ArticleContentParams;
|
|
91
|
+
if (!params) return;
|
|
92
|
+
if (typeof params === "string") {
|
|
93
|
+
return ArticleServerBindings.getInstance().readPreloadedContent(params);
|
|
94
|
+
}
|
|
95
|
+
return params;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function getArticle(item: ItemStack): Article {
|
|
99
|
+
const config = ArticleServerBindings.getInstance().config;
|
|
100
|
+
const mainParams = item.getComponent(config.componentName)
|
|
101
|
+
?.customComponentParameters.params as ArticleComponentParams;
|
|
102
|
+
const content = getArticleContent(item);
|
|
103
|
+
let article: Article | undefined;
|
|
104
|
+
if (!content){
|
|
105
|
+
throw new OccultusSDKError("Article content not found!");
|
|
106
|
+
}
|
|
107
|
+
if (typeof content !== "string" && "chapter" in content) {
|
|
108
|
+
article = new Article(
|
|
109
|
+
item.typeId,
|
|
110
|
+
mainParams.title,
|
|
111
|
+
content.description,
|
|
112
|
+
content.chapter,
|
|
113
|
+
mainParams.icon_path
|
|
114
|
+
);
|
|
115
|
+
return article;
|
|
116
|
+
}
|
|
117
|
+
article = new Article(
|
|
118
|
+
item.typeId,
|
|
119
|
+
mainParams.title,
|
|
120
|
+
parseDescription(content),
|
|
121
|
+
undefined,
|
|
122
|
+
mainParams.icon_path
|
|
123
|
+
);
|
|
124
|
+
return article;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 自动获得文章
|
|
129
|
+
* @param player
|
|
130
|
+
*/
|
|
131
|
+
export function autoGetArticles(player: Player): ReadableArticle[] {
|
|
132
|
+
try {
|
|
133
|
+
const raw = player.getDynamicProperty(Article.DYNAMIC_DATA_ID);
|
|
134
|
+
if (!raw || typeof raw !== "string") return [];
|
|
135
|
+
const json = JSON.parse(raw);
|
|
136
|
+
if (!Array.isArray(json)) return [];
|
|
137
|
+
return json.map(
|
|
138
|
+
(id: string) =>
|
|
139
|
+
ArticleServerBindings.getInstance().getArticle(id) ??
|
|
140
|
+
getArticle(new ItemStack(id))
|
|
141
|
+
);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error("Failed to get articles from player:" + e);
|
|
144
|
+
}
|
|
145
|
+
return [];
|
|
146
|
+
}
|