From e43fb49d484e613248b10d6bc7b8652f80838d00 Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 12 Apr 2025 19:53:42 -0700 Subject: [PATCH] form start --- README.md | 3 +- scripts/classes/InstanceEditForm.js | 116 ++++++++++++++++++ scripts/classes/InstanceEditFormBuilder.js | 7 ++ scripts/classes/InstanceEditOptions.js | 15 +++ scripts/classes/MenuForm.js | 79 ++++++++++++ scripts/classes/MenuFormBuilder.js | 43 +++++++ scripts/classes/Raycaster.js | 1 - scripts/classes/StructureCollection.js | 53 +++++--- .../{Structure.js => StructureInstance.js} | 61 +++++---- scripts/commands/menu.js | 26 ++++ scripts/commands/struct.js | 10 +- scripts/main.js | 1 + scripts/utils.js | 5 +- 13 files changed, 372 insertions(+), 48 deletions(-) create mode 100644 scripts/classes/InstanceEditForm.js create mode 100644 scripts/classes/InstanceEditFormBuilder.js create mode 100644 scripts/classes/InstanceEditOptions.js create mode 100644 scripts/classes/MenuForm.js create mode 100644 scripts/classes/MenuFormBuilder.js rename scripts/classes/{Structure.js => StructureInstance.js} (68%) create mode 100644 scripts/commands/menu.js diff --git a/README.md b/README.md index e467fef..8a56fc4 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,12 @@ Removes a structure. ## Roadmap -- [ ] place structure **(in progress)** +- [ ] place structure using form **(in progress)** - [x] easyPlace rule - [x] Make a viewable material list - [ ] Make automatic material gathering from inventories - [ ] structure movement & rotation - [ ] structuraMode & holoprintMode rules for ease of use -- [ ] send structure as a scriptevent so that other addons can use it - [ ] more litematica features! ## Issues & Suggestions diff --git a/scripts/classes/InstanceEditForm.js b/scripts/classes/InstanceEditForm.js new file mode 100644 index 0000000..d50d972 --- /dev/null +++ b/scripts/classes/InstanceEditForm.js @@ -0,0 +1,116 @@ +import { structureCollection } from './StructureCollection'; +import { MenuForm } from '../classes/MenuForm'; +import { ActionFormData } from '@minecraft/server-ui'; +import { InstanceEditOptions } from './InstanceEditOptions'; + +export class InstanceEditForm { + instanceName; + options = { + isPlaced: [ + InstanceEditOptions.NextLayer, + InstanceEditOptions.PreviousLayer, + InstanceEditOptions.SetLayer, + InstanceEditOptions.Move, + InstanceEditOptions.Rotate, + InstanceEditOptions.Mirror, + InstanceEditOptions.RemovePlacement, + ], + notPlaced: [ + InstanceEditOptions.PlaceInstance + ], + common: [ + InstanceEditOptions.MaterialsList, + InstanceEditOptions.RenameInstance, + InstanceEditOptions.DeleteInstance, + InstanceEditOptions.MainMenu + ] + } + + constructor(player, instanceName) { + this.player = player; + this.instanceName = instanceName; + this.instance = structureCollection.get(this.instanceName); + this.show(); + } + + show() { + const form = this.buildInstanceForm(); + form.show(this.player).then((response) => { + if (response.canceled) return; + let selectedOption; + if (this.instance.isPlaced()) + selectedOption = this.options.isPlaced[response.selection] || this.options.common[response.selection-this.options.isPlaced.length]; + else + selectedOption = this.options.notPlaced[response.selection] || this.options.common[response.selection-this.options.notPlaced.length]; + this.handleOption(selectedOption); + }); + } + + buildInstanceForm() { + const instanceOptions = this.instance.isPlaced() ? this.options.isPlaced : this.options.notPlaced; + const form = new ActionFormData() + .title('§l§2StrucTool §8Menu') + .body(`Instance: §2${this.instanceName}`) + instanceOptions.forEach(option => { + form.button(`${option}`); + }); + this.options.common.forEach(option => { + form.button(`${option}`); + }); + return form; + } + + handleOption(option) { + switch (option) { + case InstanceEditOptions.PlaceInstance: + this.instance.place(this.player.dimension.id, this.player.location); + break; + case InstanceEditOptions.RemovePlacement: + this.instance.removePlacement(); + break; + case InstanceEditOptions.RenameInstance: + this.renameInstanceForm(); + break; + case InstanceEditOptions.DeleteInstance: + this.structureCollection.remove(this.instanceName); + break; + case InstanceEditOptions.NextLayer: + this.instance.setLayer(this.instance.getLayer() + 1); + new InstanceEditForm(this.player, this.instanceName); + break; + case InstanceEditOptions.PreviousLayer: + this.instance.setLayer(this.instance.getLayer() - 1); + new InstanceEditForm(this.player, this.instanceName); + break; + case InstanceEditOptions.SetLayer: + this.setLayerForm(); + break; + case InstanceEditOptions.Rotate: + this.player.sendMessage('§cRotating not implemented yet.'); + break; + case InstanceEditOptions.Mirror: + this.player.sendMessage('§cMirroring not implemented yet.'); + break; + case InstanceEditOptions.Move: + this.instance.move(this.player.dimension.id, this.player.location); + break; + case InstanceEditOptions.MaterialsList: + this.player.sendMessage('§cMaterial list not implemented yet.'); + break; + case InstanceEditOptions.MainMenu: + new MenuForm(this.player); + break; + default: + this.player.sendMessage(`§cUnknown option: ${option}`); + break; + } + } + + renameInstanceForm() { + + } + + setLayerForm() { + // should have a toggle for if it should layer the structure or notw + } +} \ No newline at end of file diff --git a/scripts/classes/InstanceEditFormBuilder.js b/scripts/classes/InstanceEditFormBuilder.js new file mode 100644 index 0000000..5b39a61 --- /dev/null +++ b/scripts/classes/InstanceEditFormBuilder.js @@ -0,0 +1,7 @@ +import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; + +export class InstanceEditFormBuilder { + static buildRenameInstance() { + + } +} \ No newline at end of file diff --git a/scripts/classes/InstanceEditOptions.js b/scripts/classes/InstanceEditOptions.js new file mode 100644 index 0000000..c67c8be --- /dev/null +++ b/scripts/classes/InstanceEditOptions.js @@ -0,0 +1,15 @@ +export const InstanceEditOptions = Object.freeze({ + Unknown: "unknown", + MainMenu: 'Back to Main Menu', + PlaceInstance: 'Place Instance', + RemovePlacement: 'Remove Placement', + RenameInstance: 'Rename Instance', + DeleteInstance: '§cDelete Instance', + NextLayer: 'Increase Layer', + PreviousLayer: 'Decrease Layer', + SetLayer: 'Set Layer', + Move: 'Move Here', + Rotate: 'Rotate', + Mirror: 'Mirror', + MaterialsList: 'Get Materials List', +}); \ No newline at end of file diff --git a/scripts/classes/MenuForm.js b/scripts/classes/MenuForm.js new file mode 100644 index 0000000..0e60e93 --- /dev/null +++ b/scripts/classes/MenuForm.js @@ -0,0 +1,79 @@ +import { forceShow } from '../utils'; +import { structureCollection } from './StructureCollection'; +import { MenuFormBuilder } from './MenuFormBuilder'; +import { InstanceEditForm } from './InstanceEditForm'; + +export class MenuForm { + constructor(player) { + this.player = player; + this.show(); + } + + async show() { + let instanceName = this.getInstanceFromLocation(); + if (!instanceName) + instanceName = await this.getInstanceNameFromForm(); + if (!instanceName) + return; + new InstanceEditForm(this.player, instanceName); + } + + getInstanceFromLocation() { + const locatedStructures = structureCollection.getStructuresAtLocation(this.player.location); + if (locatedStructures.length === 0) + return void 0; + const structure = locatedStructures[0]; + return structure.instanceName; + } + + async getInstanceNameFromForm() { + try { + return forceShow(this.player, MenuFormBuilder.buildAllInstanceNameForm()).then((response) => { + if (response.canceled) return; + const selectedInstanceName = structureCollection.getInstanceNames()[response.selection]; + return selectedInstanceName || this.createNewInstance(); + }); + } catch (e) { + if (e.message === 'Menu timed out.') { + this.player.sendMessage('§8Menu timed out.'); + return; + } + throw e; + } + } + + async createNewInstance() { + return MenuFormBuilder.buildNewInstanceForm().show(this.player).then(async (response) => { + if (response.canceled) + return; + const instanceName = response.formValues[0]; + if (instanceName === '') + return void 0; + const structureId = await this.getStructureId(); + if (!structureId) + return; + structureCollection.add(instanceName, structureId); + return instanceName; + }); + } + + async getStructureId() { + return MenuFormBuilder.buildAllStructuresForm().show(this.player).then((response) => { + if (response.canceled) + return; + const selectedStructureId = structureCollection.getWorldStructureIds()[response.selection]; + return selectedStructureId || this.getOtherStructureId(); + }); + } + + getOtherStructureId() { + return MenuFormBuilder.buildOtherStructureForm().show(this.player).then((response) => { + if (response.canceled) + return; + const structureId = response.formValues[0]; + if (structureId === '') + return void 0; + return structureId; + }); + } +} \ No newline at end of file diff --git a/scripts/classes/MenuFormBuilder.js b/scripts/classes/MenuFormBuilder.js new file mode 100644 index 0000000..01422e3 --- /dev/null +++ b/scripts/classes/MenuFormBuilder.js @@ -0,0 +1,43 @@ +import { ActionFormData, ModalFormData } from '@minecraft/server-ui'; +import { structureCollection } from './StructureCollection'; + +export class MenuFormBuilder { + static menuTitle = '§l§2StrucTool §8Menu'; + + static buildAllInstanceNameForm() { + const allInstanceNameForm = new ActionFormData() + .title(this.menuTitle) + .body('Select an instance:'); + structureCollection.getInstanceNames().forEach(instanceName => { + allInstanceNameForm.button(`§2${instanceName}`); + }); + allInstanceNameForm.button('Create New Instance'); + return allInstanceNameForm; + } + + static buildNewInstanceForm() { + return new ModalFormData() + .title(this.menuTitle) + .textField('Enter a name for the new instance:', 'example_instance') + .submitButton('Submit'); + } + + static buildAllStructuresForm() { + const allStructuresForm = new ActionFormData() + .title(this.menuTitle) + .body('Select a structure:'); + structureCollection.getWorldStructureIds().forEach(structureId => { + const structureName = structureId.replace('mystructure:', ''); + allStructuresForm.button(`§2${structureName}`); + }); + allStructuresForm.button('Other'); + return allStructuresForm; + } + + static buildOtherStructureForm() { + return new ModalFormData() + .title(this.menuTitle) + .textField('Enter the Structure ID:', 'example_structure') + .submitButton('Submit'); + } +} \ No newline at end of file diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js index bbe64e9..befb420 100644 --- a/scripts/classes/Raycaster.js +++ b/scripts/classes/Raycaster.js @@ -1,4 +1,3 @@ -import { world } from "@minecraft/server"; import { structureCollection } from "./StructureCollection"; export class Raycaster { diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index c558a04..781b07e 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -1,37 +1,41 @@ -import { Structure } from './Structure'; +import { StructureInstance } from './StructureInstance'; +import { world } from '@minecraft/server'; class StructureCollection { - #structures; + structures; constructor() { - this.#structures = {}; + this.structures = {}; } - add(name) { - if (this.#structures[name]) { - throw new Error(`Structure ${name} already exists.`); - } - const structure = new Structure(name); - this.#structures[name] = structure; + add(instanceName, structureId) { + if (this.structures[instanceName]) + throw new Error(`Instance ${instanceName} already exists.`); + const structure = new StructureInstance(instanceName, structureId); + this.structures[instanceName] = structure; return structure; } - get(name) { - const structure = this.#structures[name]; + get(instanceName) { + const structure = this.structures[instanceName]; if (!structure) { - throw new Error(`Structure ${name} not found.`); + throw new Error(`Instance ${instanceName} not found.`); } return structure; } - remove(name) { - const struct = this.get(name); - struct.remove(); - delete this.#structures[name]; + remove(instanceName) { + const struct = this.get(instanceName); + struct.removePlacement(); + delete this.structures[instanceName]; + } + + getInstanceNames() { + return Object.keys(this.structures); } getStructuresAtLocation(location) { - return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); + return Object.values(this.structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); } fetchStructureBlock(location) { @@ -41,6 +45,21 @@ class StructureCollection { const structure = locatedStructures[0]; return structure.getBlock(structure.toStructureCoords(location)); } + + getWorldStructureIds() { + return world.structureManager.getWorldStructureIds() + .filter(id => id.startsWith('mystructure:')) + .map(id => id.replace('mystructure:', '')); + } + + rename(instanceName, newName) { + const structure = this.get(instanceName); + if (this.structures[newName]) + throw new Error(`Instance ${newName} already exists.`); + this.structures[newName] = structure; + delete this.structures[instanceName]; + structure.name = newName; + } } export const structureCollection = new StructureCollection(); \ No newline at end of file diff --git a/scripts/classes/Structure.js b/scripts/classes/StructureInstance.js similarity index 68% rename from scripts/classes/Structure.js rename to scripts/classes/StructureInstance.js index 5824200..6ee2252 100644 --- a/scripts/classes/Structure.js +++ b/scripts/classes/StructureInstance.js @@ -1,7 +1,7 @@ import { world } from "@minecraft/server"; import { Outliner } from "./Outliner"; -export class Structure { +export class StructureInstance { #structure; #options = { isPlaced: false, @@ -12,13 +12,15 @@ export class Structure { currentLayer: 0 }; - constructor(structureName) { - this.name = structureName; - this.#structure = world.structureManager.get(structureName); + constructor(instanceName, structureId) { + this.name = instanceName; + this.structureId = structureId; + this.#structure = world.structureManager.get(structureId); if (!this.#structure) { - throw new Error(`[StrucTool] Structure '${structureName}' not found.`); + throw new Error(`[StrucTool] Structure '${this.structureId}' not found.`); } this.#options = this.loadOptions(); + this.#options.isPlaced = false; } loadOptions() { @@ -39,7 +41,7 @@ export class Structure { } getLocation() { - return this.#options.worldLocation; + return { dimensionId: this.#options.dimensionId, location: this.#options.worldLocation }; } getHeight() { @@ -47,7 +49,7 @@ export class Structure { } getLayer() { - return this.#options.currentLayer; + return this.#options.currentLayer || 0; } *getBlocks() { @@ -83,7 +85,7 @@ export class Structure { getLayeredBounds() { if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); + throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); return { min: { x: 0, y: this.#options.currentLayer - 1, z: 0 }, max: { x: this.#structure.size.x, y: this.#options.currentLayer, z: this.#structure.size.z } @@ -91,31 +93,44 @@ export class Structure { } place(dimensionId, worldLocation) { - this.#options = { - isPlaced: true, - dimensionId, - worldLocation: { x: Math.floor(worldLocation.x), y: Math.floor(worldLocation.y), z: Math.floor(worldLocation.z) }, - }; - this.updateOptions(); - this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); + this.#options.isPlaced = true; + this.move(dimensionId, worldLocation); } - remove() { + removePlacement() { if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); + throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); this.#options.isPlaced = false; this.updateOptions(); this.outliner.stopDraw(); } + move(dimensionId, location) { + if (!this.#options.isPlaced) + throw new Error(`[StrucTool] Instance '${this.name}' is not placed.`); + this.#options.dimensionId = dimensionId; + this.#options.worldLocation = { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) }; + this.updateOptions(); + this.refreshOutliner(); + } + setLayer(layer) { if (layer < 1 || layer > this.#structure.size.y) - throw new Error(`[StrucTool] Structure '${this.name}' does not have layer ${layer}.`); + throw new Error(`[StrucTool] Instance '${this.name}' of '${this.structureId}' does not have layer ${layer}.`); this.#options.currentLayer = layer; this.updateOptions(); - this.outliner.stopDraw(); - const { min, max } = this.getLayeredBounds(); - this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); + this.refreshOutliner(); + } + + refreshOutliner() { + if (this.outliner) + this.outliner.stopDraw(); + if (this.#options.currentLayer > 0) { + const { min, max } = this.getLayeredBounds(); + this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); + } else { + this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); + } } isLocationInStructure(structureLocation) { @@ -155,4 +170,8 @@ export class Structure { z: worldLocation.z - this.#options.worldLocation.z }; } + + isPlaced() { + return this.#options.isPlaced; + } } \ No newline at end of file diff --git a/scripts/commands/menu.js b/scripts/commands/menu.js new file mode 100644 index 0000000..fe20ea4 --- /dev/null +++ b/scripts/commands/menu.js @@ -0,0 +1,26 @@ +import { Command } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { structureCollection } from '../classes/StructureCollection'; +import { MaterialCounter } from '../classes/MaterialCounter'; +import { world, system } from '@minecraft/server'; +import { MenuForm } from '../classes/MenuForm'; + +const ACTION_ITEM = 'minecraft:paper'; + +const structCmd = new Command({ + name: 'menu', + description: { text: 'Manages current StrucTool structures.' }, + usage: 'menu', + callback: structCommand +}); +extension.addCommand(structCmd); + +world.beforeEvents.itemUse.subscribe((event) => { + if (!event.source || event.itemStack?.typeId !== ACTION_ITEM) return; + event.cancel = true; + system.run(() => structCommand(event.source)); +}); + +function structCommand(sender) { + new MenuForm(sender); +} \ No newline at end of file diff --git a/scripts/commands/struct.js b/scripts/commands/struct.js index b3664d0..320f4af 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -17,7 +17,7 @@ const structCmd = new Command({ extension.addCommand(structCmd); function structCommand(sender, args) { - const { option, name, arg3 } = args; + const { name, option, arg3 } = args; switch (option) { case 'add': addStructure(sender, name); @@ -41,7 +41,7 @@ function structCommand(sender, args) { function addStructure(sender, name) { try { - structureCollection.add(name); + structureCollection.add(name, name); } catch (e) { if (e.message.includes('already exists')) { sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); @@ -70,7 +70,7 @@ function placeStructure(sender, name) { structure = structureCollection.get(name); } catch (e) { try { - structure = structureCollection.add(name); + structure = structureCollection.add(name, name); } catch (e) { if (e.message.includes('already exists')) { sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); @@ -108,8 +108,8 @@ function printInfo(sender, name) { sender.sendMessage({ text: `§cStructure '${name}' not found.` }); return; } - const location = structure.getLocation(); - sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}]` }); + const { dimensionId, location } = structure.getLocation(); + sender.sendMessage({ text: `§7Structure '${name}' at [${location.x} ${location.y} ${location.z}] in '${dimensionId}'` }); sender.sendMessage({ text: `§7Current Layer: ${structure.getLayer()}` }); sender.sendMessage({ text: `§7Materials: ${MaterialCounter.getPrintable(name)}` }); } \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index 9b6d634..d7a4547 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -4,6 +4,7 @@ import './rules/fastEasyPlace'; // Commands import './commands/struct'; +import './commands/menu'; // Other import './classes/BlockInfo'; diff --git a/scripts/utils.js b/scripts/utils.js index 6cf2187..49caa4e 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,3 +1,4 @@ +import { system } from '@minecraft/server'; import { FormCancelationReason } from '@minecraft/server-ui'; export async function forceShow(player, form, timeout = Infinity) { @@ -5,9 +6,9 @@ export async function forceShow(player, form, timeout = Infinity) { while ((system.currentTick - startTick) < timeout) { const response = await form.show(player); if (startTick + 1 === system.currentTick && response.cancelationReason === FormCancelationReason.UserBusy) - player.sendMessage({ translate: 'commands.canopy.menu.busy' }); + player.sendMessage("§8Close your chat window to access the menu."); if (response.cancelationReason !== FormCancelationReason.UserBusy) return response; } - throw new Error({ translate: 'commands.canopy.menu.timeout', with: [String(timeout)] }); + throw new Error("Menu timed out."); }; \ No newline at end of file