@occultus/article-api 0.24.0-beta.1 → 0.24.0-beta.3

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