import { getModId } from "lazuli-core"; import { Container, Player, RawMessage } from "@minecraft/server"; import { ItemData, EntityData, DisplayCondition } from "lazuli-common"; import { getItemAmountInContainer, giveItem, modAlert } from "lazuli-utils"; import { ActionFormData, MessageFormData, } from "@minecraft/server-ui"; /** * Namespace of Task Complete Tag. */ let taskNameSpace: string = getModId(); /** * Create a task. * @catetory Need Registry */ export class Task { constructor( /** * The unique id of the task. */ readonly id: string, /** * Title of the task, only support {@link RawMessage}. */ public _title: RawMessage, /** * Body of the task, only support {@link RawMessage}. */ public _body: RawMessage, /** * Options of the task. * * *You MUST assign the complete condition and the award, others are optional.* */ public options: TaskOptions ) { if (!this.options.unlock) { this.options.unlock = true; } } /** * * @param player * @param backTo The screen that player should return to after closing the form. */ protected displayInfo(player: Player, backTo?: TaskList | TaskCategory) { const body = TaskUtils.generateBody(this.body, this.options); const form = new MessageFormData() .title(this.title) .body(body) .button1({ translate: "gui.back" }); if (this.isCompleted(player)) { form.button2({ translate: "task.done" }); } else { form.button2({ translate: "task.check" }); } 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 ( TaskUtils.checkCondition(this.options.condition, player, this).result ) { this.complete(player); } else { player.sendMessage( TaskUtils.checkCondition(this.options.condition, player, this) .message ); } } }); } /** * * @param player * @param backTo */ protected displayFail(player: Player, backTo?: TaskList | TaskCategory) { const UNLOCK = this.options.unlock; modAlert({ translate: "task.displayFail.unlock" }, player); if (typeof UNLOCK === "object") { player.sendMessage( TaskUtils.checkCondition(UNLOCK, player, this).message ); } } /** * Display ui form to the player. * @param player * @param backTo The screen that player should return to after closing the form. */ display(player: Player, backTo?: TaskList | TaskCategory): void { if (this.isUnlocked(player)) { this.displayInfo(player, backTo); } else if (typeof this.options.unlock === "object") { if (TaskUtils.checkCondition(this.options.unlock, player, this)) { this.unlock(player); this.displayInfo(player, backTo); } else { this.displayFail(player, backTo); } } else { modAlert({ translate: "task.displayFail.noAccess" }, player); } } /** * Let a player complete the task and give award. * @param player */ complete(player: Player): void { player.addTag(TaskUtils.getCompleteTag(this)); 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); } if (this.options.award.custom) { this.options.award.custom(player); } player.playSound("random.levelup"); player.sendMessage({ translate: "task.finished", }); } /** * Let a player complete the task and give award. * @param player */ unlock(player: Player): void { player.addTag(TaskUtils.getUnlockTag(this)); player.onScreenDisplay.setActionBar({ translate: "task.unlock", }); } /** * Check if the player has the complete tag. * @param player */ isCompleted(player: Player): boolean { return player.hasTag(TaskUtils.getCompleteTag(this)); } /** * Check if the player has the unlock tag. * @param player */ isUnlocked(player: Player): boolean { const UNLOCK = this.options.unlock; if (typeof UNLOCK === "boolean") { return UNLOCK; } else { return player.hasTag(TaskUtils.getUnlockTag(this)); } } set body(content: RawMessage) { this._body = content; } set title(content: RawMessage) { this._title = content; } get body() { return this._body; } get title() { return this._title; } } export class TaskCategory { constructor( public title: RawMessage, public body: RawMessage, public items: Task[], public iconPath?: string ) {} /** * Display ui form to the player. * @param player * @param backTo The screen that player should return to after closing the form. */ display(player: Player, backTo?: TaskList): void { const form = new ActionFormData().title(this.title).body(this.body); this.items.forEach((task) => { let rawTitle: RawMessage = { rawtext: [this.title] }; if (task.isCompleted(player)) { rawTitle.rawtext?.push({ text: " §2✔" }); } form.button(rawTitle, task.options.iconPath); }); form.show(player).then((response) => { if (response.canceled || response.selection === undefined) { backTo?.display(player); } else { this.items[response.selection].display(player); } }); } /** * Returns whether the player has completed all tasks in the category. * @param player * @returns Whether the player has completed all tasks in the category. */ isCompleted(player: Player): boolean { let value = this.items.every((task) => { return task.isCompleted(player) === true; }); return value; } } export class TaskList { constructor( readonly id: string, public title: RawMessage, public body: RawMessage, public options: TaskListOptions ) {} display(player: Player) { const form = new ActionFormData().title(this.title).body(this.body); this.options.items.forEach((item) => { let rawTitle: RawMessage = { rawtext: [this.title] }; if (item.isCompleted(player)) { rawTitle.rawtext?.push({ text: " §2✔" }); } form.button(rawTitle); }); form.show(player).then(response=>{ if (response.canceled || response.selection === undefined) { return; } this.options.items[response.selection].display(player); }) } } export class TaskUtils { /** * Get a task's complete tag. * @param task * @return The task's complete tag. */ static getCompleteTag(task: Task) { return `${taskNameSpace}:${task.id}`; } /** * Get a task's complete tag. * @param task * @return The task's complete tag. */ static getUnlockTag(task: Task) { return `${taskNameSpace}:${task.id}.unlock`; } /** * Set name space of the task complete tag. * @param str */ static setNameSpace(str: string) { taskNameSpace = str; } static checkCondition( options: TaskUnlockCompleteOptions, player: Player, task: Task ): checkResult { let message: RawMessage = { rawtext: [] }; const CONTAINER: undefined | Container = player.getComponent( "minecraft:inventory" )?.container; if (!CONTAINER) { throw new Error("Task Api unable to access player's Container!"); } if ( options.item && getItemAmountInContainer(CONTAINER, options.item.itemStack.typeId) < options.item.itemStack.amount ) { message.rawtext?.push({ rawtext: [ { translate: "task.not_enough.item", with: { rawtext: [ { text: options.item.itemStack.amount.toString(), }, options.item.name, ], }, }, // Following items are missing from your inventory: { text: "\n", }, ], }); } if (options.level && player.level < options.level) { message.rawtext?.push({ rawtext: [ { translate: "task.not_enough.level", with: [player.level.toString()], }, // You need %%1 more level(s)! { text: "\n", }, ], }); } if (options.exp && player.getTotalXp() < options.exp) { message.rawtext?.push({ rawtext: [ { translate: "task.not_enough.xp", with: [player.getTotalXp.toString()], }, // You need %%1 more experience! { text: "\n", }, ], }); } if (options.tasks) { let finishList: boolean[] = []; let taskList: RawMessage = { rawtext: [], }; options.tasks.forEach((task) => { finishList.push(task.isCompleted(player)); taskList.rawtext?.push(task.title); }); if ( !finishList.every((value) => { return value === true; }) ) { message.rawtext?.push({ rawtext: [ { translate: "task.not_enough.tasks", with: taskList, }, // You need %%1 more experience! { text: "\n", }, ], }); } } if (!task.isCompleted(player) && options.killEntity) { message.rawtext?.push({ rawtext: [ { translate: "task.not_enough.entity", with: [options.killEntity.name], }, ], }); } return { result: message.rawtext?.length === 0, message: message, }; } /** * Generate the body of Task, includes descriptions, conditions, awards and tips. * @param description * @param options * @return A RawMessage that includes task's descriptions, conditions, awards and tips. */ static generateBody(description: RawMessage, options: TaskOptions) { const [CONDITION, AWARD] = [options.condition, options.award]; let body: RawMessage = { rawtext: [ description, { text: "\n\n" }, { translate: "task.condition.title" }, ], }; if (Object.getOwnPropertyNames(CONDITION).length === 0) { body.rawtext?.push({ translate: "task.condition.none", }); } else { if (CONDITION.item) { body.rawtext?.push({ translate: "task.item", with: { rawtext: [ { text: CONDITION.item.itemStack.amount.toString(), }, CONDITION.item.name, ], }, }); } if (CONDITION.exp) { body.rawtext?.push({ translate: "task.exp", with: [CONDITION.exp.toString()], }); } if (CONDITION.level) { body.rawtext?.push({ translate: "task.level", with: [CONDITION.level.toString()], }); } if (CONDITION.killEntity) { body.rawtext?.push({ translate: "task.entity", with: [CONDITION.killEntity.name], }); } if (CONDITION.tasks) { let taskList: RawMessage = { rawtext: [], }; CONDITION.tasks.forEach((task) => { taskList.rawtext?.push(task._title); }); body.rawtext?.push({ translate: "task.tasks", with: taskList, }); } } body.rawtext?.push({ text: "\n" }, { translate: "task.award.title" }); if (Object.getOwnPropertyNames(AWARD).length === 0) { body.rawtext?.push({ translate: "task.award.none", }); } else { if (AWARD.item) { body.rawtext?.push({ translate: "task.item", with: { rawtext: [ { text: AWARD.item.itemStack.amount.toString(), }, AWARD.item.name, ], }, }); } if (AWARD.exp) { body.rawtext?.push({ translate: "task.exp", with: [AWARD.exp.toString()], }); } if (AWARD.level) { body.rawtext?.push({ translate: "task.level", with: [AWARD.level.toString()], }); } } if (options.tips) { body.rawtext?.push( { text: "\n" }, { translate: "task.tips.title" }, options.tips ); } return body; } } /** * Contidions that control when unlock or complete the task. * * **NOTE: If specify more than one condition, the task will be unlocked when any condition is achieved, but it will be completed after all conditions are achieved.** */ export interface TaskUnlockCompleteOptions { /** * Unlock the task when player has the specific item(s). */ item?: ItemData; /** * The specific entity will be required to unlock the task. */ killEntity?: EntityData; /** * The specific level will be required to unlock the task. */ level?: number; /** * The specific point will be required to unlock the task. */ exp?: number; /** * The specific tasks will be required to unlock the task. */ tasks?: Task[]; } /** * Award of the task. */ export interface TaskAward { /** * Player will get these items when the task 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; /** * Custom award. * @param player */ custom?: (player: Player) => void; } export interface TaskOptions { condition: TaskUnlockCompleteOptions; award: TaskAward; unlock?: TaskUnlockCompleteOptions | boolean; tips?: RawMessage; iconPath?: string; } export interface TaskListOptions { items: (Task | TaskCategory)[]; displayCondition: DisplayCondition; iconPath?: string; } export interface checkResult { result: boolean; message: RawMessage; }