Merge branch 'player-settings'

This commit is contained in:
ForestOfLight
2025-04-20 02:42:35 -07:00
Unverified
40 changed files with 437 additions and 215 deletions
+3 -11
View File
@@ -1,8 +1,8 @@
{
"format_version": 2,
"header": {
"name": "StrucTool [BP] v1.0.0",
"description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.",
"name": "Construct [BP] v1.0.0",
"description": "Survival building addon by §aForestOfLight§r.",
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58",
"min_engine_version": [1, 21, 70],
"version": [1, 0, 0]
@@ -33,16 +33,8 @@
"version": "2.0.0-beta"
},
{
"uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", // StrucTool RP
"uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4", // Construct RP
"version": [1, 0, 0]
},
{
"uuid": "bcf34368-ed0c-4cf7-938e-582cccf9950d", // Canopy RP
"version": [1, 0, 3]
},
{
"uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP
"version": [1, 3, 9]
}
],
"metadata": {
@@ -0,0 +1,15 @@
import { BuilderOptions } from "./BuilderOptions";
export class Builder {
constructor(playerId) {
this.playerId = playerId;
}
isOptionEnabled(optionId) {
return BuilderOptions.isEnabled(optionId, this.playerId);
}
setOption(optionId, value) {
return BuilderOptions.setValue(optionId, this.playerId, value);
}
}
@@ -0,0 +1,37 @@
import { BuilderFormBuilder } from "./BuilderFormBuilder";
import { BuilderOptions } from "./BuilderOptions";
import { forceShow } from '../../utils';
export class BuilderForm {
constructor(player) {
this.player = player;
this.show();
}
show() {
forceShow(this.player, BuilderFormBuilder.buildBuilderOptions(this.player)).then((response) => {
if (response.canceled) return;
this.applySettings(response.formValues);
});
}
applySettings(formValues) {
const optionIds = BuilderOptions.getOptionIds();
for (let i = 0; i < optionIds.length; i++) {
const option = BuilderOptions.get(optionIds[i]);
const changedToValue = option.setValue(this.player.id, formValues[i]);
if (changedToValue === true)
this.player.sendMessage(`§a${option.displayName} is now enabled!§7 ${option.howToUse}`);
else if (changedToValue === false)
this.player.sendMessage(`§c${option.displayName} is now disabled.`)
}
}
valueChangedToEnabled(hasChanged, newValue) {
return hasChanged && newValue === true;
}
valueChangedToDisabled(hasChanged, newValue) {
return hasChanged && newValue === false;
}
}
@@ -0,0 +1,16 @@
import { ModalFormData } from "@minecraft/server-ui";
import { MenuFormBuilder } from "../MenuFormBuilder";
import { BuilderOptions } from "./BuilderOptions";
export class BuilderFormBuilder {
static buildBuilderOptions(player) {
const form = new ModalFormData()
.title(MenuFormBuilder.menuTitle);
for (const optionId of BuilderOptions.getOptionIds()) {
const option = BuilderOptions.get(optionId);
form.toggle(`${option.displayName} - ${option.description}`, option.isEnabled(player.id));
}
form.submitButton('§2Apply');
return form;
}
}
@@ -0,0 +1,43 @@
import { BuilderOptions } from "./BuilderOptions";
import { world } from "@minecraft/server";
export class BuilderOption {
identifier;
displayName;
description;
howToUse;
#onEnable;
#onDisable;
#DP_NAMESPACE = "builderOptions";
constructor({ identifier, displayName, description, howToUse, onEnableCallback = () => {}, onDisableCallback = () => {} }) {
this.identifier = identifier;
this.displayName = displayName;
this.description = description;
this.howToUse = howToUse;
this.#onEnable = onEnableCallback;
this.#onDisable = onDisableCallback;
BuilderOptions.add(this);
}
isEnabled(playerId) {
return world.getDynamicProperty(`${this.#DP_NAMESPACE}:${playerId}:${this.identifier}`) === true;
}
setValue(playerId, value) {
if (value)
this.#onEnable(playerId);
else
this.#onDisable(playerId);
if (this.isEnabled(playerId) !== value) {
this.save(playerId, value);
return value;
}
this.save(playerId, value);
return void 0;
}
save(playerId, value) {
world.setDynamicProperty(`${this.#DP_NAMESPACE}:${playerId}:${this.identifier}`, value);
}
}
@@ -0,0 +1,23 @@
export class BuilderOptions {
static options = {};
static add(builderOption) {
this.options[builderOption.identifier] = builderOption;
}
static get(optionId) {
return this.options[optionId];
}
static getOptionIds() {
return Object.keys(this.options).sort((a, b) => a - b);
}
static isEnabled(optionId, playerId) {
return this.options[optionId].isEnabled(playerId);
}
static setValue(optionId, playerId, value) {
return this.options[optionId].setValue(playerId, value);
}
}
@@ -0,0 +1,36 @@
import { world } from "@minecraft/server";
import { Builder } from "./Builder";
export class Builders {
static builders = {};
static add(playerId) {
if (this.builders[playerId])
return;
this.builders[playerId] = new Builder(playerId);
}
static remove(playerId) {
delete this.builders[playerId];
}
static get(id) {
return this.builders[id];
}
static onJoin(playerId) {
this.add(playerId);
}
static onLeave(playerId) {
this.remove(playerId);
}
}
world.afterEvents.playerJoin.subscribe((event) => Builders.onJoin(event.playerId));
world.beforeEvents.playerLeave.subscribe((event) => Builders.onLeave(event.player.id));
world.afterEvents.worldLoad.subscribe((event) => {
for (const player of world.getAllPlayers()) {
Builders.onJoin(player.id);
}
});
@@ -0,0 +1,6 @@
export class InvalidInstanceError extends Error {
constructor(message) {
super(message);
this.name = 'InvalidInstanceError';
}
}
@@ -1,34 +1,33 @@
import { structureCollection } from './StructureCollection';
import { MenuForm } from './MenuForm';
import { forceShow } from '../utils';
import { InstanceEditButtons } from './enums/InstanceEditButtons';
import { InstanceEditFormBuilder } from './InstanceEditFormBuilder';
import { structureCollection } from '../Structure/StructureCollection';
import { MenuForm } from '../MenuForm';
import { forceShow } from '../../utils';
import { InstanceButtons } from '../Enums/InstanceButtons';
import { InstanceFormBuilder } from './InstanceFormBuilder';
import { FormCancelationReason } from '@minecraft/server-ui';
export class InstanceEditForm {
export class InstanceForm {
instanceName;
#buttons = {
isEnabled: [
InstanceEditButtons.NextLayer,
InstanceEditButtons.PreviousLayer,
InstanceEditButtons.SetLayer,
InstanceEditButtons.Move,
InstanceEditButtons.Statistics,
InstanceEditButtons.Settings,
InstanceEditButtons.Rename,
InstanceEditButtons.Disable,
InstanceButtons.NextLayer,
InstanceButtons.PreviousLayer,
InstanceButtons.Move,
InstanceButtons.Statistics,
InstanceButtons.Settings,
InstanceButtons.Rename,
InstanceButtons.Disable
],
isNotEnabledAndIsNotPlaced: [
InstanceEditButtons.Place,
InstanceEditButtons.Rename
InstanceButtons.Place,
InstanceButtons.Rename
],
isNotEnabledButIsPlaced: [
InstanceEditButtons.Enable,
InstanceEditButtons.Rename
InstanceButtons.Enable,
InstanceButtons.Rename
],
common: [
InstanceEditButtons.Delete,
InstanceEditButtons.MainMenu
InstanceButtons.Delete,
InstanceButtons.MainMenu
]
}
@@ -41,7 +40,7 @@ export class InstanceEditForm {
show() {
const currentOptions = this.getActiveOptions();
forceShow(this.player, InstanceEditFormBuilder.buildInstance(this.instance, currentOptions)).then((response) => {
forceShow(this.player, InstanceFormBuilder.buildInstance(this.instance, currentOptions)).then((response) => {
if (response.canceled) return;
this.handleOption(currentOptions[response.selection]);
});
@@ -59,48 +58,48 @@ export class InstanceEditForm {
if (!this.instance.hasLayers())
currentOptions = currentOptions.filter(option =>
option !== InstanceEditButtons.SetLayer
&& option !== InstanceEditButtons.NextLayer
&& option !== InstanceEditButtons.PreviousLayer
option !== InstanceButtons.SetLayer
&& option !== InstanceButtons.NextLayer
&& option !== InstanceButtons.PreviousLayer
);
return currentOptions;
}
handleOption(option) {
switch (option) {
case InstanceEditButtons.Enable:
case InstanceButtons.Enable:
this.instance.enable();
break;
case InstanceEditButtons.Disable:
case InstanceButtons.Disable:
this.instance.disable();
break;
case InstanceEditButtons.Place:
case InstanceButtons.Place:
this.instance.place(this.player.dimension.id, this.player.location);
break;
case InstanceEditButtons.Rename:
case InstanceButtons.Rename:
this.renameInstanceForm();
break;
case InstanceEditButtons.Delete:
case InstanceButtons.Delete:
structureCollection.delete(this.instanceName);
break;
case InstanceEditButtons.NextLayer:
case InstanceButtons.NextLayer:
this.instance.increaseLayer();
new InstanceEditForm(this.player, this.instanceName);
new InstanceForm(this.player, this.instanceName);
break;
case InstanceEditButtons.PreviousLayer:
case InstanceButtons.PreviousLayer:
this.instance.decreaseLayer();
new InstanceEditForm(this.player, this.instanceName);
new InstanceForm(this.player, this.instanceName);
break;
case InstanceEditButtons.Settings:
case InstanceButtons.Settings:
this.settingsForm();
break;
case InstanceEditButtons.Move:
case InstanceButtons.Move:
this.instance.move(this.player.dimension.id, this.player.location);
break;
case InstanceEditButtons.Statistics:
case InstanceButtons.Statistics:
this.statisticsForm();
break;
case InstanceEditButtons.MainMenu:
case InstanceButtons.MainMenu:
new MenuForm(this.player, { jumpToInstance: false });
break;
default:
@@ -110,7 +109,7 @@ export class InstanceEditForm {
}
renameInstanceForm() {
InstanceEditFormBuilder.buildRenameInstance(this.instanceName).show(this.player).then((response) => {
InstanceFormBuilder.buildRenameInstance(this.instanceName).show(this.player).then((response) => {
if (response.canceled)
return;
const newName = response.formValues[0];
@@ -129,7 +128,7 @@ export class InstanceEditForm {
}
setLayerForm() {
InstanceEditFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => {
InstanceFormBuilder.buildSetLayer(this.instance.getBounds().max.y, this.instance.getLayer()).show(this.player).then((response) => {
if (response.canceled)
return;
this.instance.setLayer(parseInt(response.formValues[0]));
@@ -137,7 +136,7 @@ export class InstanceEditForm {
}
async statisticsForm() {
const statsForm = await InstanceEditFormBuilder.buildStatistics(this.instance)
const statsForm = await InstanceFormBuilder.buildStatistics(this.instance)
statsForm.form.show(this.player).then((response) => {
if (response.canceled && response.cancelationReason === FormCancelationReason.UserBusy)
this.player.sendMessage(statsForm.stats);
@@ -145,11 +144,15 @@ export class InstanceEditForm {
}
settingsForm() {
InstanceEditFormBuilder.buildSettings(this.instance).show(this.player).then((response) => {
InstanceFormBuilder.buildSettings(this.instance).show(this.player).then((response) => {
if (response.canceled)
return;
this.instance.setVerifierEnabled(response.formValues[0]);
this.instance.setLayer(parseInt(response.formValues[1]));
if (response.formValues[1])
this.instance.setVerifierDistance(5);
else
this.instance.setVerifierDistance(0);
this.instance.setLayer(parseInt(response.formValues[2]));
});
}
}
@@ -1,10 +1,10 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { MenuFormBuilder } from './MenuFormBuilder';
import { StructureVerifier } from './StructureVerifier';
import { StructureStatistics } from './StructureStatistics';
import { MenuFormBuilder } from '../MenuFormBuilder';
import { StructureVerifier } from '../Verifier/StructureVerifier';
import { StructureStatistics } from '../Structure/StructureStatistics';
import { TicksPerSecond } from '@minecraft/server';
export class InstanceEditFormBuilder {
export class InstanceFormBuilder {
static buildInstance(instance, options) {
const location = instance.getLocation();
const form = new ActionFormData()
@@ -29,7 +29,7 @@ export class InstanceEditFormBuilder {
static async buildStatistics(instance) {
const buildStatisticsForm = new ActionFormData()
.title(MenuFormBuilder.menuTitle)
const structureVerifier = new StructureVerifier(instance, { isEnabled: true, trackPlayerDistance: 0, intervalOrLifetime: 30 * TicksPerSecond });
const structureVerifier = new StructureVerifier(instance, { isEnabled: true, trackPlayerDistance: 0, intervalOrLifetime: 30 * TicksPerSecond, isStandalone: true });
const verification = await structureVerifier.verifyStructure();
const statistics = new StructureStatistics(instance, verification);
const statsMessage = statistics.getMessage();
@@ -41,8 +41,9 @@ export class InstanceEditFormBuilder {
return new ModalFormData()
.title(MenuFormBuilder.menuTitle)
.label('Use the slider to select the layer. Use 0 for all layers.')
.toggle('Toggle block validation.', instance.options.verifier.isEnabled)
.slider("Layer", 0, maxLayer, 1, currentLayer)
.submitButton('§aApply');
.toggle('Block Validation', instance.options.verifier.isEnabled)
.toggle('Distance-Based Block Validation', instance.verifier.getTrackPlayerDistance() !== 0)
.slider("Layer", 0, instance.getMaxLayer(), 1, instance.getLayer())
.submitButton('§2Apply');
}
}
@@ -1,7 +1,9 @@
import { Vector } from "../lib/Vector";
import { Vector } from "../../lib/Vector";
import { world } from "@minecraft/server";
import { Option } from "../Option";
export class InstanceOptions {
export class InstanceOptions extends Option {
#DP_NAMESPACE = "instanceOptions";
instanceName = void 0;
structureId = void 0;
isEnabled = false;
@@ -20,32 +22,23 @@ export class InstanceOptions {
}
constructor(instanceName, structureId) {
super();
this.instanceName = instanceName;
this.structureId = structureId;
this.load();
}
save() {
world.setDynamicProperty(`instanceOptions:${this.instanceName}`, JSON.stringify(this));
this.saveToDP(this.#DP_NAMESPACE, this.instanceName, this);
}
load() {
try {
const options = JSON.parse(world.getDynamicProperty(`instanceOptions:${this.instanceName}`));
if (options)
Object.assign(this, options);
else
throw new Error("Options not found");
} catch {
this.save();
const options = JSON.parse(world.getDynamicProperty(`instanceOptions:${this.instanceName}`) || "{}");
Object.assign(this, options);
}
this.loadFromDP(this.#DP_NAMESPACE, this.instanceName);
this.worldLocation = Vector.from(this.worldLocation);
}
clear() {
world.setDynamicProperty(`instanceOptions:${this.instanceName}`, void 0);
this.clearDP(this.#DP_NAMESPACE, this.instanceName);
}
getDimension() {
@@ -1,8 +1,8 @@
import { Vector } from "../lib/Vector";
import { StructureOutliner } from "./StructureOutliner";
import { StructureVerifier } from "./StructureVerifier";
import { Vector } from "../../lib/Vector";
import { StructureOutliner } from "../Structure/StructureOutliner";
import { StructureVerifier } from "../Verifier/StructureVerifier";
import { Structure } from "../Structure/Structure";
import { InstanceOptions } from "./InstanceOptions";
import { Structure } from "./Structure";
import { TicksPerSecond } from "@minecraft/server";
export class StructureInstance {
@@ -19,11 +19,11 @@ export class StructureInstance {
delete() {
this.disable();
this.options.clear();
delete this.options;
delete this.structure;
delete this.outliner;
delete this.verifier;
this.options.clear();
}
refreshBox() {
@@ -70,7 +70,7 @@ export class StructureInstance {
getActiveBounds() {
if (!this.options.isEnabled)
throw new Error(`[StrucTool] Instance '${this.options.instanceName}' is not placed.`);
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
if (this.hasLayerSelected())
return this.getLayerBounds(this.getLayer());
return this.getBounds();
@@ -78,7 +78,7 @@ export class StructureInstance {
getLayerBounds(layer) {
if (!this.options.isEnabled)
throw new Error(`[StrucTool] Instance '${this.options.instanceName}' is not placed.`);
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
const min = this.structure.getMin();
const max = this.structure.getMax();
return {
@@ -105,7 +105,7 @@ export class StructureInstance {
getActiveBlocks() {
if (!this.options.isEnabled)
throw new Error(`[StrucTool] Instance '${this.options.instanceName}' is not placed.`);
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
if (this.hasLayerSelected())
return this.getLayerBlocks(this.getLayer());
return this.getAllBlocks();
@@ -126,7 +126,7 @@ export class StructureInstance {
getAllActiveLocations() {
if (!this.options.isEnabled)
throw new Error(`[StrucTool] Instance '${this.options.instanceName}' is not placed.`);
throw new Error(`[Construct] Instance '${this.options.instanceName}' is not placed.`);
if (this.hasLayerSelected())
return this.structure.getLayerLocations(this.getLayer()-1);
else
@@ -187,7 +187,7 @@ export class StructureInstance {
setLayer(layer) {
if (layer < 0 || layer > this.getMaxLayer())
throw new Error(`[StrucTool] Layer ${layer} is out of bounds.`);
throw new Error(`[Construct] Layer ${layer} is out of bounds.`);
this.options.setLayer(layer);
this.refreshBox();
}
+14 -6
View File
@@ -1,7 +1,8 @@
import { forceShow } from '../utils';
import { structureCollection } from './StructureCollection';
import { structureCollection } from './Structure/StructureCollection';
import { MenuFormBuilder } from './MenuFormBuilder';
import { InstanceEditForm } from './InstanceEditForm';
import { InstanceForm } from './Instance/InstanceForm';
import { BuilderForm } from './Builder/BuilderForm';
export class MenuForm {
constructor(player, { jumpToInstance = false, instanceName = void 0 } = {}) {
@@ -13,15 +14,15 @@ export class MenuForm {
if (jumpToInstance) {
if (!instanceName)
instanceName = structureCollection.getStructure(this.player.dimension.id, this.player.location, { useActiveLayer: false })?.getName();
if (structureCollection.get(instanceName)) {
new InstanceEditForm(this.player, instanceName);
if (instanceName) {
new InstanceForm(this.player, instanceName);
return;
}
}
instanceName = await this.getInstanceNameFromForm();
if (!instanceName)
return;
new InstanceEditForm(this.player, instanceName);
new InstanceForm(this.player, instanceName);
}
async getInstanceNameFromForm() {
@@ -29,8 +30,15 @@ export class MenuForm {
return forceShow(this.player, MenuFormBuilder.buildAllInstanceName()).then((response) => {
if (response.canceled)
return;
const selectedInstanceName = structureCollection.getInstanceNames()[response.selection];
let selection = response.selection;
if (selection === 0) {
new BuilderForm(this.player);
return void 0;
} else {
selection--;
const selectedInstanceName = structureCollection.getInstanceNames()[selection];
return selectedInstanceName || this.createNewInstance();
}
});
} catch (e) {
if (e.message === 'Menu timed out.') {
@@ -1,13 +1,14 @@
import { ActionFormData, ModalFormData } from '@minecraft/server-ui';
import { structureCollection } from './StructureCollection';
import { structureCollection } from './Structure/StructureCollection';
export class MenuFormBuilder {
static menuTitle = '§l§2StrucTool §8Menu';
static menuTitle = '§l§2Construct';
static buildAllInstanceName() {
const allInstanceNameForm = new ActionFormData()
.title(this.menuTitle)
.body('Select an instance:');
allInstanceNameForm.button('Builder Settings');
structureCollection.getInstanceNames().forEach(instanceName => {
allInstanceNameForm.button(`§2${instanceName}`);
});
+25
View File
@@ -0,0 +1,25 @@
import { world } from "@minecraft/server";
export class Option {
saveToDP(namespace, id, data) {
world.setDynamicProperty(`${namespace}:${id}`, JSON.stringify(data));
}
loadFromDP(namespace, id) {
try {
const options = JSON.parse(world.getDynamicProperty(`${namespace}:${id}`));
if (options)
Object.assign(this, options);
else
throw new Error("Options not found");
} catch {
this.saveToDP(namespace, id, {});
const options = JSON.parse(world.getDynamicProperty(`${namespace}:${id}`) || "{}");
Object.assign(this, options);
}
}
clearDP(namespace, id) {
world.setDynamicProperty(`${namespace}:${id}`, void 0);
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ export class Outliner {
dimension;
min = new Vector();
max = new Vector();
drawParticle = "structool:outline";
drawParticle = "construct:outline";
drawFrequency = 10;
#drawParticles = [];
+1 -1
View File
@@ -1,4 +1,4 @@
import { structureCollection } from "./StructureCollection";
import { structureCollection } from "./Structure/StructureCollection";
import { world } from "@minecraft/server";
export class Raycaster {
@@ -1,5 +1,5 @@
import { world } from "@minecraft/server";
import { Vector } from "../lib/Vector";
import { Vector } from "../../lib/Vector";
export class Structure {
structureId;
@@ -9,7 +9,7 @@ export class Structure {
this.structureId = structureId;
this.#structure = world.structureManager.get(structureId);
if (!this.#structure)
throw new Error(`[StrucTool] Structure '${structureId}' not found.`);
throw new Error(`[Construct] Structure '${structureId}' not found.`);
this.#structure.saveToWorld();
}
@@ -1,5 +1,6 @@
import { InstanceOptions } from './InstanceOptions';
import { StructureInstance } from './StructureInstance';
import { InvalidInstanceError } from '../Errors/InvalidInstanceError';
import { InstanceOptions } from '../Instance/InstanceOptions';
import { StructureInstance } from '../Instance/StructureInstance';
import { world } from '@minecraft/server';
class StructureCollection {
@@ -17,7 +18,7 @@ class StructureCollection {
structureId = InstanceOptions.getInstanceStrucetureId(instanceName);
this.structures[instanceName] = new StructureInstance(instanceName, structureId);
} catch (e) {
world.sendMessage(`§c[StrucTool] Error loading structure instance '${instanceName}'. It will be removed.`);
world.sendMessage(`§c[Construct] Error loading structure instance '${instanceName}'. It will be removed.`);
world.setDynamicProperty(id, void 0);
throw e;
}
@@ -26,7 +27,7 @@ class StructureCollection {
add(instanceName, structureId) {
if (this.structures[instanceName])
throw new Error(`Instance ${instanceName} already exists.`);
throw new InvalidInstanceError(`Instance ${instanceName} already exists.`);
const structure = new StructureInstance(instanceName, structureId);
this.structures[instanceName] = structure;
return structure;
@@ -34,9 +35,8 @@ class StructureCollection {
get(instanceName) {
const structure = this.structures[instanceName];
if (!structure) {
throw new Error(`Instance ${instanceName} not found.`);
}
if (!structure)
throw new InvalidInstanceError(`Instance ${instanceName} not found.`);
return structure;
}
@@ -1,4 +1,4 @@
import { Outliner } from './Outliner';
import { Outliner } from '../Outliner';
export class StructureOutliner {
constructor(instance) {
@@ -42,7 +42,7 @@ export class StructureOutliner {
}
layeredDraw() {
const { min, max } = this.instance.getLayeredBounds();
const { min, max } = this.instance.getLayerBounds(this.instance.getLayer());
this.outliner.setVertices(this.dimension, this.instance.toGlobalCoords(min), this.instance.toGlobalCoords(max));
this.outliner.addStandaloneParticles(this.getCornerVertices());
}
@@ -1,4 +1,4 @@
import { BlockVerificationLevel } from './enums/BlockVerificationLevel.js';
import { BlockVerificationLevel } from '../Enums/BlockVerificationLevel.js';
export class StructureStatistics {
constructor(instance, verification) {
@@ -1,6 +1,6 @@
import { MolangVariableMap } from "@minecraft/server";
import { BlockVerificationLevel } from "./enums/BlockVerificationLevel";
import { Vector } from "../lib/Vector";
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { Vector } from "../../lib/Vector";
export class BlockVerificationLevelRender {
opacity = 0.2;
@@ -36,12 +36,12 @@ export class BlockVerificationLevelRender {
const frontFace = new Vector(0.5, 0.5, 1);
const backFace = new Vector(0.5, 0.5, 0);
return [
{ particleType: "structool:blockoverlay_xz", location: this.location.add(topFace) },
{ particleType: "structool:blockoverlay_xz", location: this.location.add(bottomFace) },
{ particleType: "structool:blockoverlay_yz", location: this.location.add(leftFace) },
{ particleType: "structool:blockoverlay_yz", location: this.location.add(rightFace) },
{ particleType: "structool:blockoverlay_xy", location: this.location.add(frontFace) },
{ particleType: "structool:blockoverlay_xy", location: this.location.add(backFace) }
{ particleType: "construct:blockoverlay_xz", location: this.location.add(topFace) },
{ particleType: "construct:blockoverlay_xz", location: this.location.add(bottomFace) },
{ particleType: "construct:blockoverlay_yz", location: this.location.add(leftFace) },
{ particleType: "construct:blockoverlay_yz", location: this.location.add(rightFace) },
{ particleType: "construct:blockoverlay_xy", location: this.location.add(frontFace) },
{ particleType: "construct:blockoverlay_xy", location: this.location.add(backFace) }
];
}
@@ -1,4 +1,4 @@
import { BlockVerificationLevel } from "./enums/BlockVerificationLevel";
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
export class BlockVerifier {
constructor(block, instance) {
@@ -1,8 +1,8 @@
import { BlockVerifier } from "./BlockVerifier";
import { BlockVerificationLevel } from "./enums/BlockVerificationLevel";
import { BlockVerificationLevelRender } from "./BlockVerificationLevelRender";
import { BlockVerificationLevel } from "../Enums/BlockVerificationLevel";
import { BlockVerificationLevelRender } from "../Verifier/BlockVerificationLevelRender";
import { system, TicksPerSecond } from "@minecraft/server";
import { Vector } from "../lib/Vector";
import { Vector } from "../../lib/Vector";
const MIN_TRACK_PLAYER_DISTANCE = 0;
const MAX_TRACK_PLAYER_DISTANCE = 7;
@@ -21,11 +21,17 @@ export class StructureVerifier {
#verifyJob;
#populateJob = {};
constructor(instance, { isEnabled = false, trackPlayerDistance = 0, intervalOrLifetime = 10 } = {}) {
constructor(instance, { isEnabled = false, trackPlayerDistance = 0, intervalOrLifetime = 10, isStandalone: isIndependent = false } = {}) {
this.instance = instance;
this.intervalOrLifetime = Math.max(intervalOrLifetime, MIN_LIFETIME);
if (isIndependent) {
this.isIndependent = isIndependent;
this.enabled = isEnabled;
this.trackPlayerDistance = trackPlayerDistance;
} else {
this.instance.options.setVerifierEnabled(isEnabled);
this.instance.options.setVerifierDistance(trackPlayerDistance);
}
this.locationsToVerify = new Set();
}
@@ -50,11 +56,18 @@ export class StructureVerifier {
}
isEnabled() {
if (this.isIndependent)
return this.enabled;
return this.instance.options.verifier.isEnabled;
}
getTrackPlayerDistance() {
return Math.min(MAX_TRACK_PLAYER_DISTANCE, Math.max(MIN_TRACK_PLAYER_DISTANCE, this.instance.options.verifier.trackPlayerDistance));
let distance;
if (this.isIndependent)
distance = this.trackPlayerDistance;
else
distance = this.instance.options.verifier.trackPlayerDistance
return Math.min(MAX_TRACK_PLAYER_DISTANCE, Math.max(MIN_TRACK_PLAYER_DISTANCE, distance));
}
init() {
@@ -1,4 +1,4 @@
export const InstanceEditButtons = Object.freeze({
export const InstanceButtons = Object.freeze({
Unknown: 'Unknown',
MainMenu: '<<',
Place: '§aPlace Instance',
+10 -5
View File
@@ -2,13 +2,14 @@ import { Command } from '../lib/canopy/CanopyExtension';
import { extension } from '../config';
import { world, system } from '@minecraft/server';
import { MenuForm } from '../classes/MenuForm';
import { structureCollection } from '../classes/Structure/StructureCollection'
const ACTION_ITEM = 'minecraft:paper';
const menuCmd = new Command({
name: 'structool',
description: { text: 'Opens the StrucTool Menu. Using a paper will also open the menu.' },
usage: 'structool',
name: 'construct',
description: { text: 'Opens the Construct Menu. Using a paper will also open the menu.' },
usage: 'construct',
callback: (sender) => openMenu(sender)
});
extension.addCommand(menuCmd);
@@ -21,7 +22,11 @@ world.beforeEvents.itemUse.subscribe((event) => {
function openMenu(sender, event = void 0) {
const options = { jumpToInstance: true }
if (event)
options.instanceName = event.itemStack?.typeId;
if (event) {
const instanceNames = structureCollection.getInstanceNames();
const instanceName = event.itemStack?.nameTag;
if (instanceNames.includes(instanceName))
options.instanceName = instanceName;
}
new MenuForm(sender, options);
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { CanopyExtension } from './lib/canopy/CanopyExtension';
export const extension = new CanopyExtension({
author: 'ForestOfLight',
name: 'StrucTool',
description: 'Survival building extension for §l§aCanopy§r!',
name: 'Construct',
description: 'Survival building addon by §aForestOfLight§r.',
version: '1.0.0'
});
+1 -7
View File
@@ -30,13 +30,11 @@ class Rule {
#contingentRules;
#independentRules;
constructor({ identifier, description, contingentRules = [], independentRules = [], onEnableCallback = () => {}, onDisableCallback = () => {} }) {
constructor({ identifier, description, contingentRules = [], independentRules = [] }) {
this.#identifier = identifier;
this.#description = description;
this.#contingentRules = contingentRules;
this.#independentRules = independentRules;
this.onEnable = onEnableCallback;
this.onDisable = onDisableCallback;
}
getID() {
@@ -65,10 +63,6 @@ class Rule {
}
setValue(value) {
if (value === true)
this.onEnable();
else
this.onDisable();
world.setDynamicProperty(this.#identifier, value);
}
}
+7 -4
View File
@@ -1,7 +1,10 @@
// Rules
import './rules/easyPlace';
import './rules/fastEasyPlace';
import './rules/materialGrabber';
// Setup
import './classes/Builder/Builders';
// Options
import './options/easyPlace';
import './options/fastEasyPlace';
import './options/materialGrabber';
// Commands
import './commands/construct';
@@ -1,24 +1,24 @@
import { Rule } from '../lib/canopy/CanopyExtension';
import { extension } from '../config';
import { BuilderOption } from '../classes/Builder/BuilderOption';
import { BlockPermutation, EntityComponentTypes, GameMode, ItemStack, system, world } from '@minecraft/server';
import { structureCollection } from '../classes/StructureCollection';
import { structureCollection } from '../classes/Structure/StructureCollection';
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions,
blockIdToItemStackMap } from '../data';
import { fetchMatchingItemSlot } from '../utils';
const ACTION_SLOT = 35;
const ACTION_SLOT = 27;
const easyPlace = new Rule({
const builderOption = new BuilderOption({
identifier: 'easyPlace',
description: { text: "Automatically places the correct block in a structure (paper named 'easyPlace' in bottom right inventory slot)." },
onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); },
onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); }
})
extension.addRule(easyPlace);
displayName: 'Easy Place',
description: 'Always place the correct structure block.',
howToUse: "Place blocks in a structure with a paper named 'Easy Place' in the inventory slot above your first hotbar slot to always place the correct block."
});
world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock);
function onPlayerPlaceBlock(event) {
const { player, block, permutationBeingPlaced } = event;
if (!player || !block || !hasActionItemInCorrectSlot(player)) return;
const { player, block } = event;
if (!player || !block || !builderOption.isEnabled(player.id) || !hasActionItemInCorrectSlot(player)) return;
const structureBlock = structureCollection.fetchStructureBlock(block.dimension.id, block.location);
if (!structureBlock)
return;
@@ -30,7 +30,7 @@ function hasActionItemInCorrectSlot(player) {
if (!inventory)
return false;
const actionSlot = inventory.getSlot(ACTION_SLOT);
return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'easyPlace';
return actionSlot.hasItem() && actionSlot.typeId === 'minecraft:paper' && actionSlot.nameTag === 'Easy Place';
}
function tryPlaceBlock(event, player, block, structureBlock) {
@@ -1,22 +1,24 @@
import { Rule } from '../lib/canopy/CanopyExtension';
import { extension } from '../config';
import { BuilderOption } from '../classes/Builder/BuilderOption';
import { BlockPermutation, EntityComponentTypes, EquipmentSlot, GameMode, ItemStack, system, world } from '@minecraft/server';
import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions,
blockIdToItemStackMap } from '../data';
import { Raycaster } from '../classes/Raycaster';
const PROCESS_INTERVAL = 2;
let runner = void 0;
const easyPlace = new Rule({
const builderOption = new BuilderOption({
identifier: 'fastEasyPlace',
description: { text: "Looking at a structure block with a paper named 'easyPlace' in your hand will place it." },
onEnableCallback: () => { runner = system.runInterval(onTick, 2); },
onDisableCallback: () => { system.clearRun(runner); }
})
extension.addRule(easyPlace);
displayName: 'Fast Easy Place',
description: 'Place structure blocks just by looking at them.',
howToUse: "Look at structure blocks with a paper named 'Easy Place' in your hand to place them.",
});
system.runInterval(onTick, PROCESS_INTERVAL);
function onTick() {
for (const player of world.getAllPlayers()) {
if (!player)
if (!player || !builderOption.isEnabled(player.id))
continue;
processEasyPlace(player);
}
@@ -35,7 +37,7 @@ function isHoldingActionItem(player) {
const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand);
if (!mainhandItemStack)
return false;
return mainhandItemStack.typeId === 'minecraft:paper' && mainhandItemStack.nameTag === 'easyPlace';
return mainhandItemStack.typeId === 'minecraft:paper' && mainhandItemStack.nameTag === 'Easy Place';
}
function tryPlaceBlock(player, worldBlock, structureBlock) {
@@ -0,0 +1,19 @@
import { BuilderOption } from '../classes/Builder/BuilderOption';
import { world } from '@minecraft/server';
const builderOption = new BuilderOption({
identifier: 'materialGrabber',
displayName: 'Material Grabber',
description: 'Pulls structure items from inventories.',
howToUse: "Interact with inventories using a paper named 'Material Grabber' to pull structure items from them.",
});
world.beforeEvents.playerInteractWithBlock.subscribe(onPlayerInteract);
world.beforeEvents.playerInteractWithEntity.subscribe(onPlayerInteract);
function onPlayerInteract(event) {
// get inventory
// analyze active structure items if not analyzed -- there needs to be a way to refresh this that is efficient
// find items in inventory that match items in structure
// transfer to player
}
@@ -1,24 +0,0 @@
import { Rule } from '../lib/canopy/CanopyExtension';
import { extension } from '../config';
import { world } from '@minecraft/server';
const easyPlace = new Rule({
identifier: 'materialGrabber',
description: { text: "Automatically grabs structure materials out of inventories. Use a paper named 'materialGrabber' to get started." },
onEnableCallback: () => {
world.beforeEvents.playerInteractWithBlock.subscribe(onPlayerInteract);
world.beforeEvents.playerInteractWithEntity.subscribe(onPlayerInteract);
},
onDisableCallback: () => {
world.beforeEvents.playerInteractWithBlock.unsubscribe(onPlayerInteract);
world.beforeEvents.playerInteractWithEntity.unsubscribe(onPlayerInteract);
}
})
extension.addRule(easyPlace);
function onPlayerInteract(event) {
// get inventory
// analyze active structure items if not analyzed -- there needs to be a way to refresh this that is efficient
// find items in inventory that match items in structure
// transfer to player
}
+3 -7
View File
@@ -1,8 +1,8 @@
{
"format_version": 2,
"header": {
"name": "StrucTool [RP] v1.0.0",
"description": "Survival building extension for §l§aCanopy§r by §aForestOfLight§r.",
"name": "Construct [RP] v1.0.0",
"description": "Survival building addon by §aForestOfLight§r.",
"uuid": "375ec465-3dc1-429f-8b4c-a337889e1ed4",
"version": [1, 0, 0],
"min_engine_version": [1,17,0]
@@ -16,12 +16,8 @@
],
"dependencies": [
{
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", // StrucTool BP
"uuid": "8c0c0153-d8b9-482a-889f-aef922b8fe58", // Construct BP
"version": [1, 0, 0]
},
{
"uuid": "7f6b23df-a583-476b-b0e4-87457e65f7c0", // Canopy BP
"version": [1, 3, 9]
}
],
"metadata": {
@@ -2,7 +2,7 @@
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "structool:blockoverlay_xy",
"identifier": "construct:blockoverlay_xy",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particle/white"
@@ -2,7 +2,7 @@
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "structool:blockoverlay_xz",
"identifier": "construct:blockoverlay_xz",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particle/white"
@@ -2,7 +2,7 @@
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "structool:blockoverlay_yz",
"identifier": "construct:blockoverlay_yz",
"basic_render_parameters": {
"material": "particles_blend",
"texture": "textures/particle/white"
@@ -2,7 +2,7 @@
"format_version": "1.10.0",
"particle_effect": {
"description": {
"identifier": "structool:outline",
"identifier": "construct:outline",
"basic_render_parameters": {
"material": "particles_alpha",
"texture": "textures/particle/particles"
+27 -12
View File
@@ -12,33 +12,48 @@
---
Intro paragraph.
The closest thing Bedrock has to **Litematica**. This addon allows you to create and manage ghost structures in your world, making it easier to build them in survival. Construct offers these convenient features:
- **Block Validation**: Highlights incorrectly placed blocks.
- **Easy Place**: Always places blocks correctly.
- **Structure Management**: Create, edit, and delete structures.
[IMAGE HERE]
> [!IMPORTANT]
> This addon is a **Canopy Extension**, which means **Canopy** must be installed in your world for it to work.
> This is a standalone addon, but it is also a **Canopy Extension**, which means **Canopy** can be installed in your world for a few extra features.
> [Download **Canopy** here!](https://github.com/ForestOfLight/Canopy)
## Usage
All commands are prefixed with `./`. Do `./help` for more information.
### Construct Menu
**Usage: `./struct add <name>`**
Adds a new structure.
The main Construct menu can be accessed using a `paper` item. Using it will open a GUI where you can manage your structures instances. You can create new instances of structures, edit existing ones, and delete them when you no longer need them. There are plenty options available to customize your building style, including the ability build in layers.
**Usage: `./struct remove <name>`**
Removes a structure.
*Pro tip: You can name the paper after a structure instance to automagically select it when you open the menu!*
### Adding New Structures
Construct uses Minecraft's vanilla structure system so that you can easily create and build new structures.
- The simplest way to add a new structure to the structure list is to save it using a structure block.
- Importing structures from other worlds is as simple as dropping the `.mcstructure` file in Construct's `Construct [BP]/structures` folder. Then, to select your structure in the Construct menu, choose "Other" when presented with the structures and enter the filename (without including ".mcstructure"). You only need to do this once with new structures, since Construct will save them for future use!
### Commands
**Usage: `./construct`**
Shows the construct form. Only available when **Canopy** is installed.
## Roadmap
- [x] Form to manage structure
- [x] Form to manage structures
- [x] Structure naming & movement
- [x] easyPlace rule
- [ ] Automatic Armor stand posing
- [x] Easyplace
- [x] Correct block placement checking
- [ ] Automatic material gathering from inventories
- [ ] Correct block placement checking
- [ ] Structure Mirroring & Rotation
- [ ] Structure Merging into SuperStructures
## Issues & Suggestions
If you have any issues or suggestions, please open an issue on this repo. Additionally, if you're interested in contributing to the project, feel free to open a pull request!
If you have any issues or suggestions, please don't hesitate to open an issue on this repo. Additionally, if you're interested in contributing to the project, feel free to open a pull request!