diff --git a/src/library/@types/classes/uiFormBuilder.d.ts b/src/library/@types/classes/uiFormBuilder.d.ts index 22cd36dd2..5a8011342 100644 --- a/src/library/@types/classes/uiFormBuilder.d.ts +++ b/src/library/@types/classes/uiFormBuilder.d.ts @@ -6,34 +6,31 @@ type UIFormName = `$${string}` type UIAction = (ctx: MenuContext, player: Player) => S type DynamicElem = S | UIAction -interface BaseInput { +interface BaseInput { name: DynamicElem type: string + default?: DynamicElem } -interface Slider extends BaseInput { +interface Slider extends BaseInput { type: "slider" min: DynamicElem max: DynamicElem step?: DynamicElem - default?: DynamicElem } -interface Dropdown extends BaseInput { +interface Dropdown extends BaseInput { type: "dropdown" options: DynamicElem, - default?: DynamicElem } -interface TextField extends BaseInput { +interface TextField extends BaseInput { type: "textField" placeholder: DynamicElem, - default?: DynamicElem } -interface Toggle extends BaseInput { +interface Toggle extends BaseInput { type: "toggle" - default?: DynamicElem } type Input = Slider | Dropdown | TextField | Toggle @@ -41,22 +38,26 @@ type SubmitAction = (ctx: MenuContext, player: Player, input: { interface Button { text: DynamicElem - icon?: DynamicElem action: UIAction } +interface ActionButton extends Button { + icon?: DynamicElem + visible?: DynamicElem +} + interface BaseForm { /** The title of the UI form */ - title: DynamicElem, + title: DynamicElem /** Action to perform when the user exits or cancels the form */ cancel?: UIAction } -/** A form with a message and one or two options */ +/** A form with a message and two options */ interface MessageForm extends BaseForm { message: DynamicElem button1: Button - button2?: Button + button2: Button } /** A form with an array of buttons to interact with */ @@ -64,7 +65,7 @@ interface ActionForm extends BaseForm { /** Text that appears above the array of buttons */ message?: DynamicElem /** The array of buttons to interact with */ - buttons: DynamicElem[]> + buttons: DynamicElem[]> } interface ModalForm extends BaseForm { @@ -80,6 +81,8 @@ interface MenuContext { setData(key: S, value: T[S]): void goto(menu: UIFormName): void returnto(menu: UIFormName): void + back(): void + confirm(title: string, message: string, yes: UIAction, no?: UIAction): void } export { Form, FormData, UIAction, DynamicElem, MessageForm, ActionForm, SubmitAction, ModalForm, UIFormName, MenuContext }; \ No newline at end of file diff --git a/src/library/classes/uiFormBuilder.ts b/src/library/classes/uiFormBuilder.ts index c232fb1d7..01327243d 100644 --- a/src/library/classes/uiFormBuilder.ts +++ b/src/library/classes/uiFormBuilder.ts @@ -21,16 +21,14 @@ abstract class UIForm { public exit(player: Player, ctx: MenuContextType) { /**/ } protected handleCancel(response: FormResponse, player: Player, ctx: MenuContext) { - if (response.canceled) { - if (response.cancelationReason == FormCancelationReason.UserBusy) { - setTickTimeout(() => this.enter(player, ctx)); - } else { - ctx.goto(null); - this.cancelAction?.(ctx, player); - } - return true; + if (!response.canceled) return false; + if (response.cancelationReason == FormCancelationReason.UserBusy) { + setTickTimeout(() => this.enter(player, ctx)); + } else { + ctx.goto(undefined); + this.cancelAction?.(ctx, player); } - return false; + return true; } protected buildFormData(player: Player, ctx: MenuContext) { @@ -39,11 +37,7 @@ abstract class UIForm { } protected resolve(element: DynamicElem, player: Player, ctx: MenuContext) { - if (element instanceof Function) { - return element(ctx, player); - } else { - return element; - } + return element instanceof Function ? element(ctx, player) : element; } } @@ -53,31 +47,27 @@ class MessageUIForm extends UIForm { constructor(form: MessageForm) { super(form); - this.action1 = form.button1.action; - this.action2 = form.button2?.action; + this.action1 = form.button2.action; + this.action2 = form.button1.action; } protected build(form: MessageForm, resEl: (elem: DynamicElem) => S) { const formData = new MessageFormData(); formData.title(resEl(form.title)); formData.body(resEl(form.message)); - formData.button1(resEl(form.button1.text)); - if ("button2" in form) { - formData.button2(resEl(form.button2.text)); - } + formData.button1(resEl(form.button2.text)); + formData.button2(resEl(form.button1.text)); return formData; } enter(player: Player, ctx: MenuContext) { this.buildFormData(player, ctx).show(player).then((response: MessageFormResponse) => { - if (this.handleCancel(response, player, ctx)) { - return; - } - ctx.goto(null); + if (this.handleCancel(response, player, ctx)) return; + ctx.goto(undefined); if (response.selection == 0) { this.action1(ctx, player); } else if (response.selection == 1) { - (this.action2 ?? this.action1)(ctx, player); + this.action2(ctx, player); } }); } @@ -91,8 +81,10 @@ class ActionUIForm extends UIForm { const formData = new ActionFormData(); formData.title(resEl(form.title)); - if (form.message) { - formData.body(resEl(form.message)); + if (form.message) formData.body(resEl(form.message)); + if (resEl((ctx) => (>ctx).canGoBack())) { + formData.button("<< Back"); + this.actions.push((ctx) => ctx.back()); } for (const button of resEl(form.buttons)) { formData.button(resEl(button.text), resEl(button.icon)); @@ -105,10 +97,8 @@ class ActionUIForm extends UIForm { const form = this.buildFormData(player, ctx); const actions = this.actions; form.show(player).then((response: ActionFormResponse) => { - if (this.handleCancel(response, player, ctx)) { - return; - } - ctx.goto(null); + if (this.handleCancel(response, player, ctx)) return; + ctx.goto(undefined); actions[response.selection]?.(ctx, player); }); } @@ -161,23 +151,20 @@ class ModalUIForm extends UIForm { const form = this.buildFormData(player, ctx); const inputNames = this.inputNames; form.show(player).then((response: ModalFormResponse) => { - if (this.handleCancel(response, player, ctx)) { - return; - } + if (this.handleCancel(response, player, ctx)) return; const inputs: {[key: string]: string|number|boolean} = {}; for (const i in response.formValues) { inputs[inputNames[i]] = response.formValues[i]; } - ctx.goto(null); + ctx.goto(undefined); this.submit(ctx, player, inputs); }); } } class MenuContext implements MenuContextType { - private stack: string[] = []; + private stack: `$${string}`[] = []; private data: T = {} as T; - private currentForm: UIForm; constructor(private player: Player) {} @@ -189,31 +176,53 @@ class MenuContext implements MenuContextType { this.data[key] = value; } - goto(menu: UIFormName) { - if (menu) { - this.stack.push(menu); - } - if (this.stack.length >= 64) { - throw Error("UI Stack overflow!"); + goto(menu?: UIFormName) { + if (menu && this.stack[this.stack.length - 1] === "$___confirmMenu___") { + throw Error("Can't go to another form from a confirmation menu!"); } - this.currentForm = UIForms.goto(menu, this.player, this); + this._goto(menu); + } + + back() { + this.stack.pop(); + this._goto(this.stack.pop()); } returnto(menu: UIFormName) { - let popped: string; + let popped: string | undefined; // eslint-disable-next-line no-cond-assign - while (popped = this.stack.pop()) { - if (popped == menu) { - this.goto(menu); + while ((popped = this.stack.pop())) { + if (popped === menu) { + this._goto(menu); return; } } - this.goto(null); + this._goto(undefined); + } + + confirm(title: string, message: string, yes: UIAction, no?: UIAction) { + this.stack.push("$___confirmMenu___"); + const form = new MessageUIForm({ + title, + message, + button1: { text: "No", action: no ?? ((ctx) => ctx.back()) }, + button2: { text: "Yes", action: yes }, + }); + form.enter(this.player, this); + } + + canGoBack() { + return this.stack.length > 1; + } + + private _goto(menu?: UIFormName) { + if (menu && menu !== this.stack[this.stack.length - 1]) this.stack.push(menu); + if (this.stack.length >= 64) throw Error("UI Stack overflow!"); + UIForms.goto(menu, this.player, this); } } class UIFormBuilder { - private forms = new Map>(); private active = new Map>(); @@ -284,17 +293,11 @@ class UIFormBuilder { * @returns Whether the UI, or any at all is being displayed. */ displayingUI(player: Player, ui?: UIFormName) { - if (this.active.has(player)) { - if (ui) { - const form = this.active.get(player); - for (const registered of this.forms.values()) { - if (registered == form) { - return true; - } - } - return false; - } - return true; + if (!this.active.has(player)) return false; + if (!ui) return true; + const form = this.active.get(player); + for (const registered of this.forms.values()) { + if (registered == form) return true; } return false; } diff --git a/src/server/modules/directions.ts b/src/server/modules/directions.ts index 77ea34a7d..b810f97e7 100644 --- a/src/server/modules/directions.ts +++ b/src/server/modules/directions.ts @@ -95,4 +95,8 @@ export class Cardinal implements CustomArgType { return cardinal; } } + + getDirectionLetter() { + return this.direction; + } } \ No newline at end of file diff --git a/src/server/modules/hotbar_ui.ts b/src/server/modules/hotbar_ui.ts index 9fd7ea3a7..593cddc4b 100644 --- a/src/server/modules/hotbar_ui.ts +++ b/src/server/modules/hotbar_ui.ts @@ -112,7 +112,7 @@ class HotbarUIForm { } class HotbarContext implements MenuContext { - private stack: string[] = []; + private stack: UIFormName[] = []; private data: T = {} as T; private currentForm: HotbarUIForm; @@ -146,17 +146,25 @@ class HotbarContext implements MenuContext { } } + back() { + this.stack.pop(); + this.goto(this.stack.pop()); + } + returnto(menu: UIFormName) { - let popped: string; + let popped: string | undefined; // eslint-disable-next-line no-cond-assign - while (popped = this.stack.pop()) { - if (popped == menu) { + while ((popped = this.stack.pop())) { + if (popped === menu) { this.goto(menu); return; } } - this.goto(null); - this.base?.returnto(menu); + this.goto(undefined); + } + + confirm() { + throw "confirm() is not implemented in hotbar UI"; } } @@ -198,7 +206,7 @@ class HotbarUIBuilder { * @param player The player to display the UI form to * @param ctx The context to be passed to the UI form */ - goto(name: UIFormName, player: Player, ctx: MenuContext<{}>) { + goto(name: UIFormName, player: Player, ctx: MenuContext) { if (!(ctx instanceof HotbarContext)) { ctx = new HotbarContext(player, ctx); } diff --git a/src/server/modules/pattern.ts b/src/server/modules/pattern.ts index 583e4d8d2..c02151d8a 100644 --- a/src/server/modules/pattern.ts +++ b/src/server/modules/pattern.ts @@ -67,8 +67,12 @@ export class Pattern implements CustomArgType { } addBlock(permutation: BlockPermutation) { - if (this.block == null) { + if (!this.block) { this.block = new ChainPattern(null); + } else if (!(this.block instanceof ChainPattern)) { + const old = this.block; + this.block = new ChainPattern(null); + this.block.nodes.push(old); } const block = blockPermutation2ParsedBlock(permutation); diff --git a/src/server/sessions.ts b/src/server/sessions.ts index 8564c93d9..27d1e92bb 100644 --- a/src/server/sessions.ts +++ b/src/server/sessions.ts @@ -116,7 +116,10 @@ export class PlayerSession { this.selection = new Selection(player); this.drawOutlines = config.drawOutlines; this.gradients = new Database("gradients", player, (k, v) => { - if (k === "patterns") return (v).map(v => new Pattern(v)); + if (k === "patterns") return (v).map(v => { + console.warn(v); + return new Pattern(v); + }); return v; }); @@ -288,6 +291,15 @@ export class PlayerSession { return this.gradients.get(id); } + public getGradientNames() { + return this.gradients.keys() as string[]; + } + + public deleteGradient(id: string) { + this.gradients.delete(id); + this.gradients.save(); + } + delete() { for (const region of this.regions.values()) { region.deref(); diff --git a/src/server/ui/config_menu.ts b/src/server/ui/config_menu.ts index eed9821b4..d4b45c4db 100644 --- a/src/server/ui/config_menu.ts +++ b/src/server/ui/config_menu.ts @@ -1,8 +1,8 @@ -import { Player } from "@minecraft/server"; +import { Player, Vector3 } from "@minecraft/server"; import { HotbarUI } from "@modules/hotbar_ui.js"; import { Mask } from "@modules/mask.js"; import { Pattern } from "@modules/pattern.js"; -import { contentLog, Server } from "@notbeer-api"; +import { contentLog, regionSize, Server } from "@notbeer-api"; import { MenuContext, ModalForm } from "library/@types/classes/uiFormBuilder.js"; import { Brush } from "../brushes/base_brush.js"; import { SphereBrush } from "../brushes/sphere_brush.js"; @@ -114,9 +114,13 @@ Server.uiForms.register("$configMenu", { ctx.setData("editingBrush", true); ctx.goto("$tools"); } + }, + { + text: "%worldedit.config.gradients", + icon: "textures/ui/gradient_config", + action: ctx => ctx.goto("$gradients") } - ], - cancel: () => null + ] }); Server.uiForms.register("$generalOptions", { @@ -206,6 +210,117 @@ Server.uiForms.register("$tools", { cancel: ctx => ctx.returnto("$configMenu") }); +Server.uiForms.register("$gradients", { + title: "%worldedit.config.gradients", + buttons: (_, player) => { + const buttons = []; + buttons.push({ + text: "%worldedit.config.newGradient", + action: (ctx: MenuConfigCtx) => ctx.goto("$addGradient") + }); + for (const id of getSession(player).getGradientNames()) { + buttons.push({ + text: id, + action: (ctx: MenuConfigCtx) => { + ctx.setData("currentGradient", id); + ctx.goto("$gradientInfo"); + } + }); + } + return buttons; + } +}); + +Server.uiForms.register("$gradientInfo", { + title: (ctx) => ctx.getData("currentGradient"), + buttons: [ + { + text: "%worldedit.config.gradient.use", + action: (ctx, player) => { + const session = getSession(player); + session.globalPattern = new Pattern(`$${ctx.getData("currentGradient")}`); + } + }, + { + text: "%worldedit.config.gradient.remove", + action: (ctx) => + ctx.confirm("$worldedit.config.confirm", "%worldedit.config.confirm.delete", (ctx, player) => { + getSession(player).deleteGradient(ctx.getData("currentGradient")); + }) + } + ] +}); + +Server.uiForms.register("$addGradient", { + title: "%worldedit.config.gradient.add", + inputs: { + $id: { + type: "textField", + name: "%worldedit.config.name", + placeholder: "%worldedit.config.gradient.promptName" + }, + $dither: { + type: "slider", + name: "%worldedit.config.blend", + min: 0, max: 1, + default: 1 + } + }, + submit: (ctx, player, input) => { + ctx.setData("pickerData", { + return: "$gradients", + onFinish: (ctx, player) => { + ctx.setData("gradientData", [input.$id as string, input.$dither as number, ...getSession(player).selection.getRange()]); + ctx.goto("$confirmGradientSelection"); + } + }); + HotbarUI.goto("$selectBlocks", player, ctx); + }, + cancel: ctx => ctx.returnto("$gradients") +}); + +Server.uiForms.register("$confirmGradientSelection", { + title: "%worldedit.config.confirm", + message: "%worldedit.config.confirm.create", + button1: { + text: "%dr.button.ok", + action: (ctx, player) => { + const session = ctx.getData("session"); + const [id, dither, min, max] = ctx.getData("gradientData"); + const size = regionSize(min, max); + const dim = player.dimension; + const patterns = []; + type axis = "x" | "y" | "z"; + let s: axis, t: axis, u: axis; + if (size.x > size.y && size.x > size.z) { + s = "y"; t = "z"; u = "x"; + } else if (size.z > size.x && size.z > size.y) { + s = "x"; t = "y"; u = "z"; + } else { + s = "x"; t = "z"; u = "y"; + } + + for (let i = min[u]; i <= max[u]; i++) { + const pattern = new Pattern(); + for (let j = min[s]; j <= max[s]; j++) { + for (let k = min[t]; k <= max[t]; k++) { + pattern.addBlock(dim.getBlock({ [s]: j, [t]: k, [u]: i } as unknown as Vector3).permutation); + } + } + patterns.push(pattern); + } + + session.createGradient(id, dither, patterns); + ctx.returnto("$gradients"); + } + }, + button2: { + text: "%gui.cancel", + action: ctx => ctx.returnto("$gradients") + }, + cancel: ctx => ctx.returnto("$gradients") +}); + Server.uiForms.register("$toolNoConfig", { title: "%worldedit.config.tool.noProps", message: "%worldedit.config.tool.noProps.detail " + "\n\n\n", diff --git a/src/server/ui/hotbar_menus.ts b/src/server/ui/hotbar_menus.ts index 5403b60e2..f896b248e 100644 --- a/src/server/ui/hotbar_menus.ts +++ b/src/server/ui/hotbar_menus.ts @@ -3,14 +3,15 @@ import { Mask } from "@modules/mask.js"; import { Pattern } from "@modules/pattern"; import { Server } from "@notbeer-api"; import { ConfigContext } from "./types"; +import { getSession } from "server/sessions"; HotbarUI.register("$chooseItem", { title: "%worldedit.config.chooseItem", items: { - 0: { item: "minecraft:air", action: null }, 1: { item: "minecraft:air", action: null }, - 2: { item: "minecraft:air", action: null }, 3: { item: "minecraft:air", action: null }, - 4: { item: "minecraft:air", action: null }, 5: { item: "minecraft:air", action: null }, - 6: { item: "minecraft:air", action: null }, 7: { item: "minecraft:air", action: null }, + 0: { item: "minecraft:air", action: undefined }, 1: { item: "minecraft:air", action: undefined }, + 2: { item: "minecraft:air", action: undefined }, 3: { item: "minecraft:air", action: undefined }, + 4: { item: "minecraft:air", action: undefined }, 5: { item: "minecraft:air", action: undefined }, + 6: { item: "minecraft:air", action: undefined }, 7: { item: "minecraft:air", action: undefined }, }, tick: (ctx, player) => { const item = Server.player.getHeldItem(player); @@ -33,7 +34,7 @@ HotbarUI.register("$pickMask", { item: "wedit:confirm_button", action: (ctx, player) => { const mask = ctx.getData("session").globalMask; - ctx.getData("pickerData").onFinish(ctx, player, mask, null); + ctx.getData("pickerData").onFinish(ctx, player, mask, undefined); } } }, @@ -94,7 +95,7 @@ HotbarUI.register("$pickPattern", { item: "wedit:confirm_button", action: (ctx, player) => { const session = ctx.getData("session"); - ctx.getData("pickerData").onFinish(ctx, player, null, session.globalPattern); + ctx.getData("pickerData").onFinish(ctx, player, undefined, session.globalPattern); } } }, @@ -108,4 +109,34 @@ HotbarUI.register("$pickPattern", { session.globalPattern = ctx.getData("stashedPattern"); }, cancel: ctx => ctx.returnto(ctx.getData("pickerData").return) -}); \ No newline at end of file +}); + +HotbarUI.register("$selectBlocks", { + title: "worldedit.config.gradient.select", + items: { + 4: { + item: "minecraft:wooden_axe", + action: () => { /**/ } + }, + 7: { + item: "wedit:confirm_button", + action: (ctx, player) => { + ctx.getData("pickerData").onFinish(ctx, player, undefined, undefined); + } + } + }, + entered: (ctx, player) => { + const session = getSession(player); + if (!session.selection.isCuboid) { + ctx.setData("stashedSelectionMode", session.selection.mode); + session.selection.mode = "cuboid" + } else { + ctx.setData("stashedSelectionMode", undefined); + } + }, + exiting: (ctx, player) => { + const oldMode = ctx.getData("stashedSelectionMode"); + if (oldMode) getSession(player).selection.mode = oldMode; + }, + cancel: ctx => ctx.returnto(ctx.getData("pickerData").return) +}); diff --git a/src/server/ui/types.ts b/src/server/ui/types.ts index cbe6bea40..0d13ca9c6 100644 --- a/src/server/ui/types.ts +++ b/src/server/ui/types.ts @@ -1,6 +1,7 @@ -import { Player } from "@minecraft/server"; +import { Player, Vector3 } from "@minecraft/server"; import { Mask } from "@modules/mask"; import { Pattern } from "@modules/pattern"; +import { selectMode } from "@modules/selection"; import { MenuContext, UIFormName } from "library/@types/classes/uiFormBuilder"; import { Brush } from "server/brushes/base_brush"; import { PlayerSession } from "server/sessions"; @@ -13,9 +14,11 @@ export interface ConfigContext { currentItem?: string editingBrush?: boolean + currentGradient?: string creatingTool?: ToolTypes | BrushTypes toolData?: [number, Mask] | [Brush, Mask, number, Mask] | [string] | [Pattern] + gradientData?: [string, number, Vector3, Vector3] deletingTools?: string[] @@ -26,4 +29,5 @@ export interface ConfigContext { stashedMask?: Mask stashedPattern?: Pattern + stashedSelectionMode?: selectMode } \ No newline at end of file diff --git a/texts/en_US.po b/texts/en_US.po index 7cc253107..d40dc343b 100644 --- a/texts/en_US.po +++ b/texts/en_US.po @@ -93,6 +93,8 @@ msgid "worldedit.config.brushes" msgstr "Brushes" msgid "worldedit.config.tools" msgstr "Tools" +msgid "worldedit.config.gradients" +msgstr "Gradients" msgid "worldedit.config.general" msgstr "General" @@ -111,6 +113,8 @@ msgid "worldedit.config.newTool" msgstr "Create New Tool" msgid "worldedit.config.newBrush" msgstr "Create New Brush" +msgid "worldedit.config.newGradient" +msgstr "Create New Gradient" msgid "worldedit.selectionMode.cuboid" msgstr "Cuboid" @@ -176,12 +180,21 @@ msgstr "Erosion Brush" msgid "worldedit.config.brush.overlay" msgstr "Overlay Brush" +msgid "worldedit.config.gradient.use" +msgstr "Use Gradient in Pattern Picker" +msgid "worldedit.config.gradient.remove" +msgstr "Delete Gradient" +msgid "worldedit.config.gradient.add" +msgstr "Add new Gradient" + msgid "worldedit.config.chooseItem" msgstr "Hold an item to bind to." msgid "worldedit.config.choose.brush" msgstr "Choose what kind of brush to make." msgid "worldedit.config.choose.tool" msgstr "Choose what kind of tool to make." +msgid "worldedit.config.gradient.promptName" +msgstr "Insert Gradient ID Here" msgid "worldedit.config.patternMask.brush" msgstr "Make a pattern and optional mask for the brush." msgid "worldedit.config.pattern.tool" @@ -196,6 +209,8 @@ msgid "worldedit.config.confirm.create" msgstr "Are you ok with the setup you made?" msgid "worldedit.config.confirm.delete" msgstr "Are you sure you want to unbind this item?" +msgid "worldedit.config.name" +msgstr "Name" msgid "worldedit.config.radius" msgstr "Size" msgid "worldedit.config.height" @@ -216,6 +231,8 @@ msgid "worldedit.config.erosion" msgstr "Erosion Type" msgid "worldedit.config.structures" msgstr "Structures" +msgid "worldedit.config.blend" +msgstr "Blend Amount" msgid "worldedit.config.mask.height" msgstr "Height Mask" msgid "worldedit.config.usePicker"