form start

This commit is contained in:
ForestOfLight
2025-04-12 19:53:42 -07:00
Unverified
parent 0c90e13e2c
commit e43fb49d48
13 changed files with 372 additions and 48 deletions
+1 -2
View File
@@ -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
+116
View File
@@ -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
}
}
@@ -0,0 +1,7 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
export class InstanceEditFormBuilder {
static buildRenameInstance() {
}
}
+15
View File
@@ -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',
});
+79
View File
@@ -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;
});
}
}
+43
View File
@@ -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');
}
}
-1
View File
@@ -1,4 +1,3 @@
import { world } from "@minecraft/server";
import { structureCollection } from "./StructureCollection";
export class Raycaster {
+36 -17
View File
@@ -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();
@@ -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.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;
}
}
+26
View File
@@ -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);
}
+5 -5
View File
@@ -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)}` });
}
+1
View File
@@ -4,6 +4,7 @@ import './rules/fastEasyPlace';
// Commands
import './commands/struct';
import './commands/menu';
// Other
import './classes/BlockInfo';
+3 -2
View File
@@ -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.");
};