From 97382e065c8cf75e389dc26b30fc551b1bc82a8a Mon Sep 17 00:00:00 2001 From: "Vincent Yanzee J. Tan" Date: Mon, 17 Jun 2024 18:28:21 +0800 Subject: [PATCH] added \shop command --- src/catalyst/@types/forms.d.ts | 269 +++++------ src/catalyst/core/forms.ts | 839 +++++++++++++++++---------------- src/catalyst/core/utils.ts | 122 ++--- src/server/client.ts | 10 +- src/server/commands/data.ts | 8 +- src/server/commands/index.ts | 1 + src/server/commands/shop.ts | 22 + src/server/index.ts | 1 + src/server/shop.ts | 166 +++++++ 9 files changed, 837 insertions(+), 601 deletions(-) create mode 100644 src/server/commands/shop.ts create mode 100644 src/server/shop.ts diff --git a/src/catalyst/@types/forms.d.ts b/src/catalyst/@types/forms.d.ts index be71ba9..6b09965 100644 --- a/src/catalyst/@types/forms.d.ts +++ b/src/catalyst/@types/forms.d.ts @@ -11,200 +11,200 @@ export type text = string /* | RawText */; * message form button */ export interface button { - /** - * label for the button - */ - text: text, - - /** - * function to be called if a player selected this button - */ - action: (ctx: UIContext) => void, + /** + * label for the button + */ + text: text, + + /** + * function to be called if a player selected this button + */ + action: (ctx: UIContext) => void, } /** * action form button */ export interface actionBtn extends button { - /** - * optional icon path for the button icon - */ - icon?: string, + /** + * optional icon path for the button icon + */ + icon?: string, } /** * base modal input structure */ export interface baseInput { - /** - * identifier for the input, which can later be accessed by the submit - * function - */ - id: string, - - /** - * type of the input - */ - type: string, - - /** - * label text for the input - */ - label: text, - - /** - * default value - */ - default?: any, + /** + * identifier for the input, which can later be accessed by the submit + * function + */ + id: string, + + /** + * type of the input + */ + type: string, + + /** + * label text for the input + */ + label: text, + + /** + * default value + */ + default?: any, } /** * dropdown input */ export interface dropdownInput extends baseInput { - /** - * type of the input - */ - type: "dropdown", - - /** - * an array of text as the input options - */ - options: text[], - - /** - * default index of selection on dropdown - */ - default?: number, + /** + * type of the input + */ + type: "dropdown", + + /** + * an array of text as the input options + */ + options: text[], + + /** + * default index of selection on dropdown + */ + default?: number, } /** * slider input */ export interface sliderInput extends baseInput { - /** - * type of input - */ - type: "slider", - - /** - * minimum slider range - */ - min: number, - /** - * maximum slider range - */ - max: number, - - /** - * slider step count - */ - step?: number, - - /** - * default slider value - */ - default?: number, + /** + * type of input + */ + type: "slider", + + /** + * minimum slider range + */ + min: number, + /** + * maximum slider range + */ + max: number, + + /** + * slider step count + */ + step?: number, + + /** + * default slider value + */ + default?: number, } /** * text input */ export interface textInput extends baseInput { - /** - * type of input - */ - type: "text", - - /** - * placeholder text for the text field - */ - placeholder?: text, - - /** - * default assigned text - */ - default?: text, + /** + * type of input + */ + type: "text", + + /** + * placeholder text for the text field + */ + placeholder?: text, + + /** + * default assigned text + */ + default?: text, } /** * toggle input */ export interface toggleInput extends baseInput { - /** - * type of input - */ - type: "toggle", - - /** - * default value of input - */ - default?: boolean, + /** + * type of input + */ + type: "toggle", + + /** + * default value of input + */ + default?: boolean, } /** * base form */ export interface baseForm { - /** - * title for the form - */ - title: text, - - /** - * form cancelation handler callback - */ - cancel?: (ctx: UIContext, reason: FormCancelationReason) => void, + /** + * title for the form + */ + title: text, + + /** + * form cancelation handler callback + */ + cancel?: (ctx: UIContext, reason: FormCancelationReason) => void, } /** * action form */ export interface actionForm extends baseForm { - /** - * body text - */ - body?: text, - - /** - * action buttons - */ - buttons: actionBtn[], + /** + * body text + */ + body?: text, + + /** + * action buttons + */ + buttons: actionBtn[], } /** * message form */ export interface messageForm extends baseForm { - /** - * message to show - */ - message: text, - - /** - * first button - */ - btn1: button, - - /** - * second button - */ - btn2: button, + /** + * message to show + */ + message: text, + + /** + * first button + */ + btn1: button, + + /** + * second button + */ + btn2: button, } /** * mod form */ export interface modalForm extends baseForm { - /** - * array of modal input field data - */ - inputs: (dropdownInput | sliderInput | textInput | toggleInput)[], - - /** - * function to execute when this modal form is submitted - */ - submit: (ctx: UIContext, result: modalResponse) => void, + /** + * array of modal input field data + */ + inputs: (dropdownInput | sliderInput | textInput | toggleInput)[], + + /** + * function to execute when this modal form is submitted + */ + submit: (ctx: UIContext, result: modalResponse) => void, } /** @@ -216,3 +216,4 @@ export type modalResponse = Record; * form types */ export type formType = actionForm | messageForm | modalForm; + diff --git a/src/catalyst/core/forms.ts b/src/catalyst/core/forms.ts index 04ed83f..dc16d98 100644 --- a/src/catalyst/core/forms.ts +++ b/src/catalyst/core/forms.ts @@ -6,8 +6,8 @@ import * as ui from "@minecraft/server-ui"; import { Player } from "@minecraft/server"; import { safeCall } from "./utils.js"; import { - text, button, baseForm, actionForm, - messageForm, modalForm, formType, modalResponse + text, button, baseForm, actionForm, + messageForm, modalForm, formType, modalResponse } from "../@types/forms"; // custom ui registry @@ -21,66 +21,80 @@ const playersOnUI: Set = new Set(); * @abstract */ export abstract class BaseFormBuilder { - /** - * @protected - */ - protected abstract _data: ui.ActionFormData | ui.MessageFormData | ui.ModalFormData; - protected _cancel: baseForm['cancel']; - - /** - * register this form - * @param id the id to use - * @returns self - */ - public register(id: string): this { - registry.set(id, this); - return this; - } - - /** - * handle form cancelation - * @param ctx the ui context - * @param response the form response - * @returns true if the form we're canceled - * @protected - */ - protected _handleCancel(ctx: UIContext, response: ui.FormResponse): boolean { - if (!response.canceled) return false; - safeCall(this._cancel, ctx, response.cancelationReason); - return true; - } - - /** - * set title for the form - * @param text the text to set - * @returns self - */ - public title(text: text): this { - this._data.title(text); - return this; - } - - /** - * set action when player canceled the form - * @param callback the callback - * @returns self - */ - public cancel(callback: baseForm['cancel']): this { - this._cancel = callback; - return this; - } - - /** - * show this form to player - * @param ctx ui context - * @returns promise of form response - * @abstract - */ - public abstract show(ctx: UIContext): Promise< - ui.ActionFormResponse | - ui.MessageFormResponse | - ui.ModalFormResponse - >; + /** + * @protected + */ + protected abstract _data: ui.ActionFormData | ui.MessageFormData | ui.ModalFormData; + protected _cancel: baseForm['cancel']; + + /** + * the id of this form + */ + public id: string = ''; + + /** + * set this form id + */ + public setId(id: string): this { + this.id = id; + return this; + } + + /** + * register this form + * @param id the id to use + * @returns self + */ + public register(id?: string): this { + registry.set(id ?? this.id, this); + this.id = id; + return this; + } + + /** + * handle form cancelation + * @param ctx the ui context + * @param response the form response + * @returns true if the form we're canceled + * @protected + */ + protected _handleCancel(ctx: UIContext, response: ui.FormResponse): boolean { + if (!response.canceled) return false; + safeCall(this._cancel, ctx, response.cancelationReason); + return true; + } + + /** + * set title for the form + * @param text the text to set + * @returns self + */ + public title(text: text): this { + this._data.title(text); + return this; + } + + /** + * set action when player canceled the form + * @param callback the callback + * @returns self + */ + public cancel(callback: baseForm['cancel']): this { + this._cancel = callback; + return this; + } + + /** + * show this form to player + * @param ctx ui context + * @returns promise of form response + * @abstract + */ + public abstract show(ctx: UIContext): Promise< + ui.ActionFormResponse | + ui.MessageFormResponse | + ui.ModalFormResponse + >; } /** @@ -88,69 +102,69 @@ export abstract class BaseFormBuilder { * action form builder class */ export class ActionFormBuilder extends BaseFormBuilder { - /** - * @constructor - * make a new action form - * @param [data] form data in raw json - */ - constructor(data?: actionForm) { - super(); - - // set data - if (data) { - this.title(data.title); - if (data.body) this.body(data.body); - if (data.cancel) this.cancel(data.cancel); - data.buttons?.forEach?.(v => this.button(v.text, v.icon, v.action)); - } - } - - protected _data: ui.ActionFormData = new ui.ActionFormData(); - private _actions: button['action'][] = []; - - /** - * set body text to the form - * @param text the text - * @returns self - */ - public body(text: text): this { - this._data.body(text); - return this; - } - - /** - * add a new action button - * @param text label for the button - * @param icon optional path to icon (you can set it to null) - * @param action callback to execute when the button is selected - * @returns self - */ - public button(text: text, icon: string | null, action: button['action']): this { - this._data.button(text, icon); - this._actions.push(action); - return this; - } - - /** - * show this form to a player - * @param ctx ui context for the player - * @returns promise for the form response - */ - public show(ctx: UIContext): Promise { - playersOnUI.add(ctx.user.id); - - return this._data.show(ctx.user).then(r => { - playersOnUI.delete(ctx.user.id); - - // the form were canceled - if (this._handleCancel(ctx, r)) return r; - - // button action - safeCall(this._actions[r.selection], ctx); - - return r; - }); - } + /** + * @constructor + * make a new action form + * @param [data] form data in raw json + */ + constructor(data?: actionForm) { + super(); + + // set data + if (data) { + this.title(data.title); + if (data.body) this.body(data.body); + if (data.cancel) this.cancel(data.cancel); + data.buttons?.forEach?.(v => this.button(v.text, v.icon, v.action)); + } + } + + protected _data: ui.ActionFormData = new ui.ActionFormData(); + private _actions: button['action'][] = []; + + /** + * set body text to the form + * @param text the text + * @returns self + */ + public body(text: text): this { + this._data.body(text); + return this; + } + + /** + * add a new action button + * @param text label for the button + * @param icon optional path to icon (you can set it to null) + * @param action callback to execute when the button is selected + * @returns self + */ + public button(text: text, icon: string | null, action: button['action']): this { + this._data.button(text, icon); + this._actions.push(action); + return this; + } + + /** + * show this form to a player + * @param ctx ui context for the player + * @returns promise for the form response + */ + public show(ctx: UIContext): Promise { + playersOnUI.add(ctx.user.id); + + return this._data.show(ctx.user).then(r => { + playersOnUI.delete(ctx.user.id); + + // the form were canceled + if (this._handleCancel(ctx, r)) return r; + + // button action + safeCall(this._actions[r.selection], ctx); + + return r; + }); + } } /** @@ -158,83 +172,83 @@ export class ActionFormBuilder extends BaseFormBuilder { * message form builder class */ export class MessageFormBuilder extends BaseFormBuilder { - /** - * @constructor - * make a new message form - * @param [data] form data in raw json - */ - constructor(data?: messageForm) { - super(); - - // set data - if (data) { - this.title(data.title); - if (data.message) this.message(data.message); - if (data.cancel) this.cancel(data.cancel); - this.btn1(data.btn1.text, data.btn1.action); - this.btn2(data.btn2.text, data.btn2.action); - } - } - - protected _data: ui.MessageFormData = new ui.MessageFormData(); - private _btn1: button['action']; - private _btn2: button['action']; - - /** - * set the message text - * @param text the text - * @returns self - */ - public message(text: text): this { - this._data.body(text); - return this; - } - - /** - * set the first button - * @param text label for the button - * @param action callback to execute when button is selected - * @returns self - */ - public btn1(text: text, action: button['action']): this { - this._data.button1(text); - this._btn1 = action; - return this; - } - - /** - * set the second button - * @param text label for the button - * @param action callback to execute when button is selected - * @returns self - */ - public btn2(text: text, action: button['action']): this { - this._data.button2(text); - this._btn2 = action; - return this; - } - - /** - * show this form to a player - * @param ctx ui context for the player - * @returns promise for the form response - */ - public show(ctx: UIContext): Promise { - playersOnUI.add(ctx.user.id); - - return this._data.show(ctx.user).then(r => { - playersOnUI.delete(ctx.user.id); - - // the form were canceled - if (this._handleCancel(ctx, r)) return r; - - // button action - if (r.selection == 0) safeCall(this._btn1, ctx); - else if (r.selection == 1) safeCall(this._btn2, ctx); - - return r; - }); - } + /** + * @constructor + * make a new message form + * @param [data] form data in raw json + */ + constructor(data?: messageForm) { + super(); + + // set data + if (data) { + this.title(data.title); + if (data.message) this.message(data.message); + if (data.cancel) this.cancel(data.cancel); + this.btn1(data.btn1.text, data.btn1.action); + this.btn2(data.btn2.text, data.btn2.action); + } + } + + protected _data: ui.MessageFormData = new ui.MessageFormData(); + private _btn1: button['action']; + private _btn2: button['action']; + + /** + * set the message text + * @param text the text + * @returns self + */ + public message(text: text): this { + this._data.body(text); + return this; + } + + /** + * set the first button + * @param text label for the button + * @param action callback to execute when button is selected + * @returns self + */ + public btn1(text: text, action: button['action']): this { + this._data.button1(text); + this._btn1 = action; + return this; + } + + /** + * set the second button + * @param text label for the button + * @param action callback to execute when button is selected + * @returns self + */ + public btn2(text: text, action: button['action']): this { + this._data.button2(text); + this._btn2 = action; + return this; + } + + /** + * show this form to a player + * @param ctx ui context for the player + * @returns promise for the form response + */ + public show(ctx: UIContext): Promise { + playersOnUI.add(ctx.user.id); + + return this._data.show(ctx.user).then(r => { + playersOnUI.delete(ctx.user.id); + + // the form were canceled + if (this._handleCancel(ctx, r)) return r; + + // button action + if (r.selection == 0) safeCall(this._btn1, ctx); + else if (r.selection == 1) safeCall(this._btn2, ctx); + + return r; + }); + } } /** @@ -242,123 +256,123 @@ export class MessageFormBuilder extends BaseFormBuilder { * modal form builder class */ export class ModalFormBuilder extends BaseFormBuilder { - /** - * @constructor - * make a new modal form - * @param [data] form data in raw json - */ - constructor(data?: modalForm) { - super(); - - // set data - if (data) { - this.title(data.title); - if (data.cancel) this.cancel(data.cancel); - data.inputs?.forEach?.(v => { - if (!v) return; - if (v.type == 'dropdown') this.dropdown(v.id, v.label, v.options, v.default); - if (v.type == 'slider') this.slider(v.id, v.label, v.min, v.max, v.step, v.default); - if (v.type == 'text') this.text(v.id, v.label, v.placeholder, v.default); - if (v.type == 'toggle') this.toggle(v.id, v.label, v.default); - }); - this.submit(data.submit); - } - } - - protected _data: ui.ModalFormData = new ui.ModalFormData(); - private _inputIds: string[] = []; - private _submit: modalForm['submit']; - - /** - * adds a dropdown to the modal form - * @param id identifier for the input - * @param label description for the input - * @param options array of text, options for the form - * @param [def] index of the default option on the options array - * @returns self - */ - public dropdown(id: string, label: text, options: text[], def?: number): this { - this._inputIds.push(id); - this._data.dropdown(label, options, def); - return this; - } - - /** - * adds a slider to the modal form - * @param id identifier for the input - * @param label description for the input - * @param min minimum range for the slider - * @param max maximum range for the slider - * @param [step] step count for the slider - * @param [def] default slider value - * @returns self - */ - public slider(id: string, label: text, min: number, max: number, step?: number, def?: number): this { - this._inputIds.push(id); - this._data.slider(label, min, max, step ?? 1, def); - return this; - } - - /** - * adds a text field to the modal form - * @param id identifier for the input - * @param label description for the input - * @param [placeholder] placeholder text - * @param [def] default text - * @returns self - */ - public text(id: string, label: text, placeholder?: text, def?: text): this { - this._inputIds.push(id); - this._data.textField(label, placeholder, def); - return this; - } - - /** - * adds a toggle to the modal form - * @param id identifier for the input - * @param label description for the input - * @param [def] default state of the toggle - * @returns self - */ - public toggle(id: string, label: text, def?: boolean): this { - this._inputIds.push(id); - this._data.toggle(label, def); - return this; - } - - /** - * set the submit action - * @param submit the callback - * @returns self - */ - public submit(submit: modalForm['submit']): this { - this._submit = submit; - return this; - } - - /** - * show this form to a player - * @param ctx ui context for the player - * @returns promise for the form response - */ - public show(ctx: UIContext): Promise { - playersOnUI.add(ctx.user.id); - - return this._data.show(ctx.user).then(r => { - playersOnUI.delete(ctx.user.id); - - // the form were canceled - if (this._handleCancel(ctx, r)) return r; - - // process result - const result: modalResponse = {}; - r.formValues.forEach((val, idx) => result[this._inputIds[idx]] = val); - // run the submit action - safeCall(this._submit, ctx, result); - - return r; - }); - } + /** + * @constructor + * make a new modal form + * @param [data] form data in raw json + */ + constructor(data?: modalForm) { + super(); + + // set data + if (data) { + this.title(data.title); + if (data.cancel) this.cancel(data.cancel); + data.inputs?.forEach?.(v => { + if (!v) return; + if (v.type == 'dropdown') this.dropdown(v.id, v.label, v.options, v.default); + if (v.type == 'slider') this.slider(v.id, v.label, v.min, v.max, v.step, v.default); + if (v.type == 'text') this.text(v.id, v.label, v.placeholder, v.default); + if (v.type == 'toggle') this.toggle(v.id, v.label, v.default); + }); + this.submit(data.submit); + } + } + + protected _data: ui.ModalFormData = new ui.ModalFormData(); + private _inputIds: string[] = []; + private _submit: modalForm['submit']; + + /** + * adds a dropdown to the modal form + * @param id identifier for the input + * @param label description for the input + * @param options array of text, options for the form + * @param [def] index of the default option on the options array + * @returns self + */ + public dropdown(id: string, label: text, options: text[], def?: number): this { + this._inputIds.push(id); + this._data.dropdown(label, options, def); + return this; + } + + /** + * adds a slider to the modal form + * @param id identifier for the input + * @param label description for the input + * @param min minimum range for the slider + * @param max maximum range for the slider + * @param [step] step count for the slider + * @param [def] default slider value + * @returns self + */ + public slider(id: string, label: text, min: number, max: number, step?: number, def?: number): this { + this._inputIds.push(id); + this._data.slider(label, min, max, step ?? 1, def); + return this; + } + + /** + * adds a text field to the modal form + * @param id identifier for the input + * @param label description for the input + * @param [placeholder] placeholder text + * @param [def] default text + * @returns self + */ + public text(id: string, label: text, placeholder?: text, def?: text): this { + this._inputIds.push(id); + this._data.textField(label, placeholder, def); + return this; + } + + /** + * adds a toggle to the modal form + * @param id identifier for the input + * @param label description for the input + * @param [def] default state of the toggle + * @returns self + */ + public toggle(id: string, label: text, def?: boolean): this { + this._inputIds.push(id); + this._data.toggle(label, def); + return this; + } + + /** + * set the submit action + * @param submit the callback + * @returns self + */ + public submit(submit: modalForm['submit']): this { + this._submit = submit; + return this; + } + + /** + * show this form to a player + * @param ctx ui context for the player + * @returns promise for the form response + */ + public show(ctx: UIContext): Promise { + playersOnUI.add(ctx.user.id); + + return this._data.show(ctx.user).then(r => { + playersOnUI.delete(ctx.user.id); + + // the form were canceled + if (this._handleCancel(ctx, r)) return r; + + // process result + const result: modalResponse = {}; + r.formValues.forEach((val, idx) => result[this._inputIds[idx]] = val); + // run the submit action + safeCall(this._submit, ctx, result); + + return r; + }); + } } /** @@ -366,90 +380,110 @@ export class ModalFormBuilder extends BaseFormBuilder { * ui context class */ export class UIContext { - /** - * @constructor - * initialize a new ui context - * @param player the subject player - */ - constructor(player: Player) { - this.user = player; - } - - /** - * player ui stack - * @private - */ - private _uiStack: BaseFormBuilder[] = []; - - /** - * the subject player - */ - public readonly user: Player; - - /** - * whether we're on the top ui (we cant go back) - */ - public get topUI(): boolean { - return this._uiStack.length == 1; - } - - /** - * the current ui - */ - public get currentUI(): BaseFormBuilder { - return this._uiStack[this._uiStack.length - 1]; - } - - /** - * open ui - * @param id identifier of a registered form to open - * @returns true if the ui were shown - */ - public goto(id: string): boolean { - // player is still on a custom ui - if (displayingUI(this.user)) return false; - - // get form data - const ui = registry.get(id); - // not found - if (!ui) return false; - - // show to player - this._uiStack.push(ui); - ui.show(this); - return true; - } - - /** - * back to last ui - * @returns true if the ui were shown - */ - public back(): boolean { - // player is still on a custom ui - if (displayingUI(this.user)) return false; - // we're on the top most ui - if (this.topUI) return false; - - // pop the current ui - this._uiStack.pop(); - // show the last ui - this.currentUI.show(this); - - return true; - } - + /** + * @constructor + * initialize a new ui context + * @param player the subject player + */ + constructor(player: Player) { + this.user = player; + } + + /** + * player ui stack + * @private + */ + private _uiStack: BaseFormBuilder[] = []; + + /** + * the subject player + */ + public readonly user: Player; + + /** + * whether we're on the top ui (we cant go back) + */ + public get topUI(): boolean { + return this._uiStack.length == 1; + } + + /** + * the current ui + */ + public get currentUI(): BaseFormBuilder { + return this._uiStack[this._uiStack.length - 1]; + } + + /** + * open ui + * @param form the form class or registered id to show + * @returns true if the ui were shown + */ + public goto(form: BaseFormBuilder | string): boolean { + // player is still on a custom ui + if (displayingUI(this.user)) return false; + + // get form data + const ui = form instanceof BaseFormBuilder ? form : registry.get(form); + // not found + if (!ui) return false; + + // show to player + this._uiStack.push(ui); + ui.show(this); + return true; + } + + /** + * back to previous ui + * @returns true if the ui were shown + */ + public back(): boolean { + // player is still on a custom ui + if (displayingUI(this.user)) return false; + // we're on the top most ui + if (this.topUI) return false; + + // pop the current ui + this._uiStack.pop(); + // show the last ui + this.currentUI.show(this); + + return true; + } + + /** + * pop the ui stack up to the given ui id + * @returns true if the ui were shown + */ + public backto(id: string): boolean { + // player is still on another form + if (displayingUI(this.user)) return false; + + // pop the ui until the given form id + while (!this.topUI) { + this._uiStack.pop(); + if (this.currentUI.id != id) + continue; + // found it! + this.currentUI.show(this); + return true; + } + + return false; + } } /** * show ui to player - * @param id form identifier + * @param form the form class or id * @param player the player * @returns ui context */ -export function showForm(id: string, player: Player): UIContext { - const ctx = new UIContext(player); - ctx.goto(id); - return ctx; +export function showForm(form: BaseFormBuilder | string, player: Player): UIContext { + const ctx = new UIContext(player); + ctx.goto(form); + return ctx; } /** @@ -458,7 +492,7 @@ export function showForm(id: string, player: Player): UIContext { * @returns boolean */ export function displayingUI(player: Player): boolean { - return playersOnUI.has(player.id); + return playersOnUI.has(player.id); } /** @@ -472,10 +506,10 @@ export function registerForm(id: string, data: actionForm): ActionFormBuilder; export function registerForm(id: string, data: messageForm): MessageFormBuilder; export function registerForm(id: string, data: modalForm): ModalFormBuilder; export function registerForm(id: string, data: formType): BaseFormBuilder { - if ('buttons' in data) return new ActionFormBuilder(data).register(id); - if ('message' in data) return new MessageFormBuilder(data).register(id); - if ('inputs' in data) return new ModalFormBuilder(data).register(id); - throw new Error("Invalid form type!"); + if ('buttons' in data) return new ActionFormBuilder(data).register(id); + if ('message' in data) return new MessageFormBuilder(data).register(id); + if ('inputs' in data) return new ModalFormBuilder(data).register(id); + throw new Error("Invalid form type!"); } /** @@ -484,5 +518,6 @@ export function registerForm(id: string, data: formType): BaseFormBuilder { * @returns the form */ export function getForm(id: string): BaseFormBuilder | undefined { - return registry.get(id); + return registry.get(id); } + diff --git a/src/catalyst/core/utils.ts b/src/catalyst/core/utils.ts index 9890451..ff83dbd 100644 --- a/src/catalyst/core/utils.ts +++ b/src/catalyst/core/utils.ts @@ -6,9 +6,9 @@ * calls a function and ignore exceptions */ export function safeCall(fn: (...args: A) => R, ...args: A): R { - try { - return fn(...args); - } catch { /* no-op */ } + try { + return fn(...args); + } catch { /* no-op */ } } /** @@ -17,32 +17,32 @@ export function safeCall(fn: (...args: A) => R, * @returns the resulting string */ export function msToString(ms: number): string { - // scale: - // sec 1,000 ms 1e+3 - // min 60,000 ms 6e+4 - // hour 3,600,000 ms 3.6e+6 - // day 86,400,000 ms 8.64e+7 - // week 604,800,000 ms 6.048e+8 - // month 30d 2,592,000,000 ms 2.592e+9 - // year 365d 31,536,000,000 ms 3.1536e+10 + // scale: + // sec 1,000 ms 1e+3 + // min 60,000 ms 6e+4 + // hour 3,600,000 ms 3.6e+6 + // day 86,400,000 ms 8.64e+7 + // week 604,800,000 ms 6.048e+8 + // month 30d 2,592,000,000 ms 2.592e+9 + // year 365d 31,536,000,000 ms 3.1536e+10 - let txt = ''; + let txt = ''; - [ - { name: 'y', ms: 3.1536e+10 }, - { name: 'mo', ms: 2.592e+9 }, - { name: 'w', ms: 6.048e+8 }, - { name: 'd', ms: 8.64e+7 }, - { name: 'h', ms: 3.6e+6 }, - { name: 'm', ms: 6e+4 }, - { name: 's', ms: 1e+3 } - ].forEach(v => { - if (ms < v.ms) return; - txt += ' ' + Math.floor(ms / v.ms) + v.name; - ms = ms % v.ms; - }); + [ + { name: 'y', ms: 3.1536e+10 }, + { name: 'mo', ms: 2.592e+9 }, + { name: 'w', ms: 6.048e+8 }, + { name: 'd', ms: 8.64e+7 }, + { name: 'h', ms: 3.6e+6 }, + { name: 'm', ms: 6e+4 }, + { name: 's', ms: 1e+3 } + ].forEach(v => { + if (ms < v.ms) return; + txt += ' ' + Math.floor(ms / v.ms) + v.name; + ms = ms % v.ms; + }); - return txt.slice(1); + return txt.slice(1); } /** @@ -51,18 +51,18 @@ export function msToString(ms: number): string { * @returns resulting string */ export function compressNumber(n: number): string { - // zero or NaN, return '0' - if (!n) return '0'; - // our SI unit prefix list - const prefixes = [ '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q' ]; + // zero or NaN, return '0' + if (!n) return '0'; + // our SI unit prefix list + const prefixes = [ '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q' ]; - // how many groups of 3 decimal places are there? - const magnitude = Math.floor(Math.log10(Math.abs(n)) / 3); - // scale down the most significant digits - const scaled = n / (10 ** (magnitude * 3)); + // how many groups of 3 decimal places are there? + const magnitude = Math.floor(Math.log10(Math.abs(n)) / 3); + // scale down the most significant digits + const scaled = n / (10 ** (magnitude * 3)); - // string result - return scaled.toFixed(1) + prefixes[magnitude]; + // string result + return scaled.toFixed(1) + prefixes[magnitude]; } /** @@ -71,8 +71,8 @@ export function compressNumber(n: number): string { * @returns resulting string */ export function formatNumber(n: number): string { - // thx: https://stackoverflow.com/questions/2901102 - return n.toString().replace(/\B(? 3999) return num.toString(); + // num is higher than the representable roman number + if (num > 3999) return num.toString(); - // in minecraft, unicode diacritic `\u0305` (macron) may not display - // correctly, better to not add them + // in minecraft, unicode diacritic `\u0305` (macron) may not display + // correctly, better to not add them - // i used recursion because its easy to debug :) - if(num >= 1000) return 'M' + toRomanNumeral(num - 1000); - if(num >= 900) return 'CM' + toRomanNumeral(num - 900); - if(num >= 500) return 'D' + toRomanNumeral(num - 500); - if(num >= 400) return 'CD' + toRomanNumeral(num - 400); - if(num >= 100) return 'C' + toRomanNumeral(num - 100); - if(num >= 90) return 'XC' + toRomanNumeral(num - 90); - if(num >= 50) return 'L' + toRomanNumeral(num - 50); - if(num >= 40) return 'XL' + toRomanNumeral(num - 40); - if(num >= 10) return 'X' + toRomanNumeral(num - 10); - if(num >= 9) return 'IX' + toRomanNumeral(num - 9); - if(num >= 5) return 'V' + toRomanNumeral(num - 5); - if(num >= 4) return 'IV' + toRomanNumeral(num - 4); - if(num >= 1) return 'I' + toRomanNumeral(num - 1); + // i used recursion because its easy to debug :) + if(num >= 1000) return 'M' + toRomanNumeral(num - 1000); + if(num >= 900) return 'CM' + toRomanNumeral(num - 900); + if(num >= 500) return 'D' + toRomanNumeral(num - 500); + if(num >= 400) return 'CD' + toRomanNumeral(num - 400); + if(num >= 100) return 'C' + toRomanNumeral(num - 100); + if(num >= 90) return 'XC' + toRomanNumeral(num - 90); + if(num >= 50) return 'L' + toRomanNumeral(num - 50); + if(num >= 40) return 'XL' + toRomanNumeral(num - 40); + if(num >= 10) return 'X' + toRomanNumeral(num - 10); + if(num >= 9) return 'IX' + toRomanNumeral(num - 9); + if(num >= 5) return 'V' + toRomanNumeral(num - 5); + if(num >= 4) return 'IV' + toRomanNumeral(num - 4); + if(num >= 1) return 'I' + toRomanNumeral(num - 1); - return ''; + return ''; } diff --git a/src/server/client.ts b/src/server/client.ts index b9e6d56..ebfcf07 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -1,4 +1,4 @@ -import { GameMode, Player, system, world } from "@minecraft/server"; +import { GameMode, ItemStack, Player, system, world } from "@minecraft/server"; import { Database, events, @@ -108,6 +108,14 @@ export class Client { * message the player */ public msg(txt: string) { this.player.sendMessage(txt); } + + /** + * add an item into player's inventory + * @param item the ItemStack instance of item + */ + public giveItem(item: ItemStack) { + this.player.getComponent('minecraft:inventory').container.addItem(item); + } } /** diff --git a/src/server/commands/data.ts b/src/server/commands/data.ts index c96562e..8549fcf 100644 --- a/src/server/commands/data.ts +++ b/src/server/commands/data.ts @@ -76,9 +76,11 @@ makeCommand({ throw `player with name '${args.subject}' not found!`; // the database - const db = new Database(args.id, subject instanceof Client - ? subject.player - : subject); + const db = args.subject != 'world' && args.id == 'local_player' + ? (subject as Client).db + : new Database(args.id, subject instanceof Client + ? subject.player + : subject); if (!args.create) db.load(); else diff --git a/src/server/commands/index.ts b/src/server/commands/index.ts index de6eb0d..daf9198 100644 --- a/src/server/commands/index.ts +++ b/src/server/commands/index.ts @@ -51,5 +51,6 @@ import("./help.js"); import("./home.js"); import("./kit.js"); import("./perm.js"); +import("./shop.js"); import("./warp.js"); diff --git a/src/server/commands/shop.ts b/src/server/commands/shop.ts new file mode 100644 index 0000000..53a1e0f --- /dev/null +++ b/src/server/commands/shop.ts @@ -0,0 +1,22 @@ +import { makeCommand } from "./index.js"; +import { commandSub } from "../../catalyst/@types/commands"; +import { setTickTimeout } from "../../catalyst/index.js"; +import { showShop } from "../shop.js"; +import { assertNotInCombat } from "../utils.js"; + +const info: commandSub = { + name: "shop", + dest: "", + help: "open shop", +}; + +makeCommand(info, (args, ev, plr) => { + assertNotInCombat(plr); + + // show the shop form + setTickTimeout(() => { + showShop(plr); + plr.msg('§eplease close the chat box'); + }); +}); + diff --git a/src/server/index.ts b/src/server/index.ts index 985b2ad..a7f13a7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -49,4 +49,5 @@ export const kits: { name: string, rank?: number, admin?: boolean }[] = [ import "./client.js"; import "./chats.js"; import "./commands/index.js"; +import "./shop.js"; diff --git a/src/server/shop.ts b/src/server/shop.ts new file mode 100644 index 0000000..15abf64 --- /dev/null +++ b/src/server/shop.ts @@ -0,0 +1,166 @@ +import { + BaseFormBuilder, + ActionFormBuilder, + MessageFormBuilder, + ModalFormBuilder, + showForm, + setTickTimeout, + formatNumber, +} from "../catalyst/index.js"; +import { Client } from "./client.js"; +import { ItemStack } from "@minecraft/server"; + +interface shopItem { + name: string, // name of the item + id: string, // item type id + icon?: string,// icon for the item + price: number,// price of the item +} + +const items: shopItem[] = [ + { + name: 'Iron Ingot', + id: 'minecraft:iron_ingot', + icon: 'textures/items/iron_ingot', + price: 10, + }, + { + name: '§bGold Ingot', + id: 'minecraft:gold_ingot', + icon: 'textures/items/gold_ingot', + price: 200, + }, + { + name: '§bDiamond', + id: 'minecraft:diamond', + icon: 'textures/items/diamond', + price: 1000, + }, + { + name: '§dNetherite Ingot', + id: 'minecraft:netherite_ingot', + icon: 'textures/items/netherite_ingot', + price: 25_000, + }, + { + name: '§dEmerald', + id: 'minecraft:emerald', + icon: 'textures/items/emerald', + price: 40_000, + }, +]; + + +export function showShop(plr: Client) { + const shopForm = new ActionFormBuilder() + .setId('shopmenu') + .title('§l§dshop') + .body('buy items here'); + + // set each item + for (const item of items) { + shopForm.button(item.name + '\n' + '§r§a$' + formatNumber(item.price) + ' each', + item.icon, (ctx) => { + + const itemStack = new ItemStack(item.id); + + const purchaseDialog = new ModalFormBuilder() + .title('§l§cpurchase item: §r' + item.name) + .slider('amount', 'amount to buy', 1, itemStack.maxAmount, 1, 1) + .slider('stacks', 'stacks to buy', 1, 30, 1, 1) + .cancel((ctx, reason) => { + ctx.back() + }); + + // submission handler + purchaseDialog.submit((ctx, res) => { + const amount = res.amount as number; + const stacks = res.stacks as number; + + const totalPrice = item.price * amount * stacks; + const changeMoney = plr.money - totalPrice; + + // player's money is not enough + if (changeMoney < 0) { + const insufficientMoneyDialog = new MessageFormBuilder() + .title('§cinsufficient money') + .message('you dont have enough money to buy this item!\n' + + 'price per unit: §a$' + formatNumber(item.price) + '§r\n' + + 'amount to buy: §6' + amount + '§r\n' + + 'stacks to buy: §6' + stacks + '§r\n' + + 'total price: §c$' + formatNumber(totalPrice) + '§r\n' + + 'your current money: §c$' + formatNumber(plr.money)) + .btn1('go back', (ctx) => { + ctx.back(); + }) + .btn2('cancel item', (ctx) => { + ctx.backto('shopmenu'); + }) + .cancel((ctx, reason) => { + ctx.back(); + }); + + // show insufficient money + ctx.goto(insufficientMoneyDialog); + return; + } + + // setup this dialog + const confirmDialog = new MessageFormBuilder() + .title('§l§9confirm purchase') + .message('do you really want to buy: ' + item.name + '§r\n' + + 'price per unit: §a$' + formatNumber(item.price) + '§r\n' + + 'amount to buy: §6' + amount + '§r\n' + + 'stacks to buy: §6' + stacks + '§r\n' + + 'total price: §c$' + formatNumber(totalPrice) + '§r\n' + + 'your current money: §e$' + formatNumber(plr.money) + '§r\n' + + 'your change will be: §e$' + formatNumber(changeMoney)) + .btn1('§ccontinue', (ctx) => { + // set-up some stuff.. + plr.money -= totalPrice; + itemStack.amount = amount; + const inv = plr.player.getComponent('minecraft:inventory').container; + + // give the items + for (let i = 0; i < stacks; i++) { + if (inv.emptySlotsCount == 0) // player's inv is full + plr.player.dimension.spawnItem(itemStack, plr.player.location); + else + plr.giveItem(itemStack); + } + + // send message + plr.msg('§ayou bought §6' + item.name + + '§r§b x' + amount + '§a, ' + + '§6' + stacks + '§a stacks ' + + 'for §c$' + formatNumber(totalPrice)); + ctx.backto('shopmenu'); + }) + .btn2('§acancel', (ctx) => { + ctx.back(); + }) + .cancel((ctx, reason) => { + ctx.back(); + }); + + // show the confirmation dialog + ctx.goto(confirmDialog); + }); + + // show this form + ctx.goto(purchaseDialog); + }); + } + + // handle cancel + shopForm.cancel((ctx, reason) => { + if (reason == 'UserBusy') + setTickTimeout(() => ctx.goto(shopForm)); + else + plr.msg('§eshop closed'); + }); + + // show this form + showForm(shopForm, plr.player); +} +