import { Container, Player, RawMessage, world } from "@minecraft/server"; import { getModId } from "lazuli-core"; import { ActionFormData, ActionFormResponse, MessageFormData, } from "@minecraft/server-ui"; import { ItemData, EntityData, DisplayCondition } from "lazuli-common"; import { getItemAmountInContainer, giveItem } from "lazuli-utils"; /** * Namespace of Quest Complete Tag. */ let questNameSpace: string = getModId(); /** * Create a Quest. * @category Need Registry */ export class Quest { protected form = new MessageFormData(); constructor( /** * The unique id of the Quest. */ readonly id: string, /** * Title of the QuestBook. */ protected _title: string | RawMessage, /** * Body of the QuestBook. */ protected _body: string | RawMessage, /** * Options of the quest. */ public options: QuestOptions ) {} /** * Registry the quest. * @deprecated Use `Register.questRegistry()` to registry the prop */ protected register() { console.warn("[Lazuli] The register method was deprecated!"); } /** * Show form to a player. * @param player * @param backTo The screen player should return to after closing the form. */ display(player: Player, backTo?: QuestBook): void { const body = generateQuestBody(this.body, this.options); if (this.isCompleted(player)) { this.form .title(this.title) .body(body) .button1({ translate: "gui.back" }) .button2({ translate: "quest.done" }); } else { this.form .title(this.title) .body(body) .button1({ translate: "gui.back" }) .button2({ translate: "quest.check" }); } this.form.show(player).then((response) => { if ( response.canceled || response.selection === undefined || response.selection === 0 || this.isCompleted(player) ) { backTo?.display(player); } else if (response.selection === 1) { if (checkCondition(this, player)) this.complete(player); } }); } /** * Let a player complete the quest and give award. * @param player */ complete(player: Player): void { player.addTag(`${questNameSpace}:${this.id}`); player.addLevels(this.options.award.level ?? 0); player.addExperience(this.options.award.exp ?? 0); if (this.options.award?.item?.itemStack) giveItem([player], this.options.award?.item?.itemStack); player.playSound("random.levelup"); player.sendMessage({ translate: "quest.finished", }); } /** * Check if a player has completed this quest. * @param player */ isCompleted(player: Player): boolean { return player.hasTag(`${questNameSpace}:${this.id}`); } set body(content: string | RawMessage) { this._body = content; this.form.body(generateQuestBody(content, this.options)); } set title(content: string | RawMessage) { this._title = content; this.form.title(content); } get body() { return this._body; } get title() { return this._title; } } /** * Create a quest book. * @category Need Registry */ export class QuestBook { constructor( /** * The unique id of the QuestBook. */ public readonly id: string, /** * Title of the QuestBook. */ public title: string | RawMessage, /** * Body of the QuestBook. */ public body: string | RawMessage, /** * Options of the quest book. */ public options: QuestBookOptions, /** * The icon of the book. * It should be the path from the root of the resource pack. * @example texture/gui/example_pic */ public iconPath?: string ) {} /** * Registry the prop. * @deprecated Use `Register.questRegistry()` to registry the prop */ protected register() { console.warn("[Lazuli] The register method was deprecated!"); } /** * Display the book to the player. * @param player * @param backTo The screen player should return to after closing the form. */ display(player: Player, backTo?: QuestBookCategory | QuestBoard): void { const FORM = new ActionFormData().title(this.title).body(this.body); this.options.quests.forEach((quest: Quest) => { let rawTitle: RawMessage = { rawtext: [ typeof quest.title === "string" ? { text: quest.title } : quest.title, ], }; if (quest.isCompleted(player)) rawTitle.rawtext?.push({ text: " §2✔" }); FORM.button(rawTitle, quest.options.iconPath); }); FORM.show(player).then((response: ActionFormResponse) => { if (response.canceled || response.selection === undefined) { backTo?.display(player); } else this.options.quests[response.selection].display(player, this); }); } /** * Add a quest. * @param quest * @param message Optional information will be sent to world. */ addQuest(quest: Quest, message?: string | RawMessage): void { this.options.quests.push(quest); if (message) { world.sendMessage(message); } } /** * Get the quest by id. * @param id */ getQuest(id: string): Quest | undefined { return this.options.quests.find((quest: Quest) => quest.id === id); } /** * Get all quests. */ getQuests(): Quest[] { return this.options.quests; } } /** * Create a Quest Book with Chapters. * @category Need Registry */ export class ChapterQuestBook { constructor( /** * The unique id of the QuestBook. */ public readonly id: string, /** * Title of the QuestBook. */ protected title: string | RawMessage, /** * Body of the Questbook. */ protected body: string | RawMessage, /** * Chapters of the Questbook. */ public chapters: QuestChapter[], /** * The icon of the book. * It should be the path from the root of the resource pack. * @example texture/gui/example_pic */ public iconPath?: string ) {} /** * Registry the prop. * @deprecated Use `Register.questRegistry()` to registry the prop */ protected register() { console.warn("[Lazuli] The register method was deprecated!"); } /** * Display the book to the player. * @param player */ display(player: Player): void { const FORM = new ActionFormData(); FORM.title(this.title).body(this.body); this.chapters.forEach((chapter: QuestChapter) => { let FinishList: boolean[] = []; let rawTitle: RawMessage = { rawtext: [ typeof chapter.title === "string" ? { text: chapter.title } : chapter.title, ], }; chapter.quests.forEach((quest) => { FinishList.push(quest.isCompleted(player)); }); if ( FinishList.every((value) => { return value === true; }) ) { rawTitle.rawtext?.push({ text: " §2✔" }); } FORM.button(rawTitle, chapter.iconPath); }); FORM.show(player).then((response: ActionFormResponse) => { if (response.canceled || response.selection === undefined) { return; } let selection = response.selection; const CONTENT_FORM = new ActionFormData() .title(this.chapters[selection].title) .body(this.chapters[selection].body); this.chapters[selection].quests.forEach((quest) => { let rawTitle: RawMessage = { rawtext: [ typeof quest.title === "string" ? { text: quest.title } : quest.title, ], }; if (quest.isCompleted(player)) rawTitle.rawtext?.push({ text: " §2✔" }); CONTENT_FORM.button(rawTitle, quest.options.iconPath); }); CONTENT_FORM.show(player).then((response: ActionFormResponse) => { if (response.canceled || response.selection === undefined) { return; } this.chapters[selection].quests[response.selection].display(player); }); }); } } /** * Book Category for {@link QuestBoard}. */ export class QuestBookCategory { constructor( /** * Title of the category. */ public title: RawMessage | string, /** * Body of the category. */ public body: RawMessage | string, /** * The quest books that included in category. */ public books: QuestBook[], /** * The icon of the category. * It should be the path from the root of the resource pack. * @example texture/gui/example_pic */ public iconPath?: string ) {} /** * Display the category form to a player. * @param player */ display(player: Player) { const form = new ActionFormData().title(this.title).body(this.body); this.books.forEach((book) => { form.button(book.title, book.iconPath); }); form.show(player).then((response) => { if (response.canceled || response.selection === undefined) { return; } this.books[response.selection].display(player); }); } } export class QuestBoard { constructor( /** * The unique id of the QuestBook. */ public readonly id: string, /** * Title of the Board. */ protected title: string | RawMessage, /** * Body of the Board. */ protected body: string | RawMessage, /** * Books of the board. */ public books: (QuestBook | QuestBookCategory)[], /** * Control when collection is displayed. */ public displayCondition: DisplayCondition ) {} display(player: Player) { const form = new ActionFormData().title(this.title).body(this.body); this.books.forEach((book) => { form.button(book.title, book.iconPath); }); form.show(player).then((response) => { if (response.canceled || !response.selection) { return; } this.books[response.selection].display(player); }); } } /** * Set the namespace of Quest Complete Tag * @param str the namespace */ export function setQuestNameSpace(str: string) { questNameSpace = str; } /** * Generate a quest body, including condition and award. * @param description Description of the quest. * @param data Data of the quest. * @returns The generated quest body. */ function generateQuestBody( description: RawMessage | string, data: QuestOptions ): RawMessage { const [CONDITION, AWARD] = [data.condition, data.award]; let body: RawMessage = { rawtext: [ typeof description === "string" ? { text: description } : description, { text: "\n\n" }, { translate: "quest.condition" }, ], }; if (CONDITION.item) body.rawtext?.push({ translate: "quest.item", with: { rawtext: [ { text: CONDITION.item.itemStack.amount.toString(), }, CONDITION.item.name, ], }, }); if (CONDITION.playerXpPoint) body.rawtext?.push({ translate: "quest.xp", with: [CONDITION.playerXpPoint.toString()], }); if (CONDITION.playerXpLevel) body.rawtext?.push({ translate: "quest.level", with: [CONDITION.playerXpLevel.toString()], }); if (CONDITION.killEntity) body.rawtext?.push({ translate: "quest.entity", with: [CONDITION.killEntity.name], }); if (CONDITION.quests) body.rawtext?.push({ translate: "quest.quests", with: [CONDITION.quests.length.toString()], }); if ( !CONDITION.item && !CONDITION.playerXpLevel && !CONDITION.playerXpPoint && !CONDITION.killEntity && !CONDITION.quests ) { body.rawtext?.push({ translate: "quest.condition.none", }); } body.rawtext?.push({ text: "\n" }, { translate: "quest.award" }); if (AWARD.item) body.rawtext?.push({ translate: "quest.item", with: { rawtext: [ { text: AWARD.item.itemStack.amount.toString(), }, AWARD.item.name, ], }, }); if (AWARD.exp) body.rawtext?.push({ translate: "quest.xp", with: [AWARD.exp.toString()], }); if (AWARD.level) body.rawtext?.push({ translate: "quest.level", with: [AWARD.level.toString()], }); if (!AWARD.item && !AWARD.level && !AWARD.exp) { body.rawtext?.push({ translate: "quest.award.none", }); } if (data.tips) { body.rawtext?.push( { text: "\n" }, { translate: "quest.tips" }, typeof data.tips === "string" ? { text: data.tips } : data.tips ); } return body; } /** * Check if a player is up to the condition. * @param condition * @param player * @returns */ function checkCondition(quest: Quest, player: Player): boolean { let message: RawMessage = { rawtext: [] }; const CONTAINER: undefined | Container = player.getComponent( "minecraft:inventory" )?.container; const CONDITION = quest.options.condition; if (!CONTAINER) { return false; } if ( CONDITION.item && getItemAmountInContainer(CONTAINER, CONDITION.item.itemStack.typeId) < CONDITION.item.itemStack.amount ) { message.rawtext?.push({ rawtext: [ { translate: "quest.not_enough.item", with: { rawtext: [ { text: CONDITION.item.itemStack.amount.toString(), }, CONDITION.item.name, ], }, }, // Following items are missing from your inventory: { text: "\n", }, ], }); } if (CONDITION.playerXpLevel && player.level < CONDITION.playerXpLevel) { message.rawtext?.push({ rawtext: [ { translate: "quest.not_enough.level", with: [player.level.toString()], }, // You need %%1 more level(s)! { text: "\n", }, ], }); } if ( CONDITION.playerXpPoint && player.getTotalXp() < CONDITION.playerXpPoint ) { message.rawtext?.push({ rawtext: [ { translate: "quest.not_enough.xp", with: [player.getTotalXp.toString()], }, // You need %%1 more experience! { text: "\n", }, ], }); } if (CONDITION.quests) { let finishList: boolean[] = []; CONDITION.quests.forEach((quest) => { finishList.push(quest.isCompleted(player)); }); if ( !finishList.every((value) => { return value === true; }) ) { message.rawtext?.push({ rawtext: [ { translate: "quest.not_enough.quests", with: [CONDITION.quests.length.toString()], }, // You need %%1 more experience! { text: "\n", }, ], }); } } if (!quest.isCompleted(player) && CONDITION.killEntity) { message.rawtext?.push({ rawtext: [ { translate: "quest.not_enough.entity", with: [CONDITION.killEntity.name], }, ], }); } if (message.rawtext?.length > 0) player.sendMessage(message); return message.rawtext?.length === 0; } /** * Chapters of a quest book. */ export interface QuestChapter { /** * Title of the chapter. */ title: string | RawMessage; /** * Body of the chapter. */ body: string | RawMessage; /** * Quests of the chapter. */ quests: Quest[]; /** * The icon of the Chapter. * It should be the path from the root of the resource pack. * @example texture/gui/example_pic */ iconPath?: string; } /** * Conditions of a quest. */ export interface QuestCondition { /** * Match only typeId and min amount. */ item?: ItemData; /** * The specific level will be required to unlock the quest. */ playerXpLevel?: number; /** * The specific point will be required to unlock the quest. */ playerXpPoint?: number; /** * The specific entity will be required to unlock the quest. */ killEntity?: EntityData; /** * The specific quests will be required to unlock the quest. */ quests?: Quest[]; } /** * Award of a quest. */ export interface QuestAward { /** * Player will get these items when the quest is finished. */ item?: ItemData; /** * The specific level will be given to the player. */ level?: number; /** * The specific point will be given to the player. */ exp?: number; } /** * Options of a quest. */ export interface QuestOptions { /** * Condition to complete the quest. */ condition: QuestCondition; /** * It will be called when the Quest is completed by the player. */ award: QuestAward; /** * Tips of the quest. */ tips?: RawMessage | string; /** * The icon of the Quest. * It should be the path from the root of the resource pack. * @example texture/gui/example_pic */ iconPath?: string; } export interface QuestBookOptions { /** * Quests of the Questbook. */ readonly quests: Quest[]; /** * The icon of the Quest Book. * It should be the path from the root of the resource pack. * @example texture/gui/example_pic */ iconPath?: string; }