lazuli-api
Version:
Add some useful instances for Minecraft Bedrock Edition
690 lines (675 loc) • 17.9 kB
text/typescript
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 (<number>message.rawtext?.length > 0) player.sendMessage(message);
return <number>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;
}