From 21ec447e0e2f073904611bb282469e26b002c43b Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sat, 15 Mar 2025 04:43:11 -0700 Subject: [PATCH] easyPlace initial --- scripts/classes/Structure.js | 92 ++++++++++++++++---------- scripts/classes/StructureCollection.js | 7 ++ scripts/commands/struct.js | 26 ++++++-- scripts/data.js | 39 +++++++++++ scripts/main.js | 3 +- scripts/rules/easyPlace.js | 77 +++++++++++++++++++++ 6 files changed, 204 insertions(+), 40 deletions(-) create mode 100644 scripts/data.js create mode 100644 scripts/rules/easyPlace.js diff --git a/scripts/classes/Structure.js b/scripts/classes/Structure.js index c743e51..be9735b 100644 --- a/scripts/classes/Structure.js +++ b/scripts/classes/Structure.js @@ -6,7 +6,7 @@ export class Structure { #options = { isPlaced: false, dimensionId: MinecraftDimensionTypes.overworld, - location: { x: 0, y: 0, z: 0 }, + worldLocation: { x: 0, y: 0, z: 0 }, rotation: 0, mirror: false, currentLayer: 0 @@ -39,7 +39,7 @@ export class Structure { } getLocation() { - return this.#options.location; + return this.#options.worldLocation; } getHeight() { @@ -70,51 +70,34 @@ export class Structure { } } - getBlock(location) { - return this.#structure.getBlockPermutation(location); + getBlock(structureLocation) { + return this.#structure.getBlockPermutation(structureLocation); } getBounds() { + return { + min: { x: 0, y: 0, z: 0 }, + max: this.#structure.size + }; + } + + getLayeredBounds() { if (!this.#options.isPlaced) throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); return { - min: { - x: this.#options.location.x, - y: this.#options.location.y, - z: this.#options.location.z - }, - max: { - x: this.#options.location.x + this.#structure.size.x, - y: this.#options.location.y + this.#structure.size.y, - z: this.#options.location.z + this.#structure.size.z - } - } + 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 } + }; } - getLayeredBounds(useUnder = false) { - if (!this.#options.isPlaced) - throw new Error(`[StrucTool] Structure '${this.name}' is not placed.`); - if (useUnder) { - return { - min: { x: this.#options.location.x, y: this.#options.location.y, z: this.#options.location.z }, - max: { x: this.#options.location.x + this.#structure.size.x, y: this.#options.location.y + this.#options.currentLayer, z: this.#options.location.z + this.#structure.size.z } - } - } else { - return { - min: { x: this.#options.location.x, y: this.#options.location.y + this.#options.currentLayer - 1,z: this.#options.location.z }, - max: { x: this.#options.location.x + this.#structure.size.x, y: this.#options.location.y + this.#options.currentLayer, z: this.#options.location.z + this.#structure.size.z } - } - } - } - - place(dimensionId, location) { + place(dimensionId, worldLocation) { this.#options = { isPlaced: true, dimensionId, - location: { x: Math.floor(location.x), y: Math.floor(location.y), z: Math.floor(location.z) }, + worldLocation: { x: Math.floor(worldLocation.x), y: Math.floor(worldLocation.y), z: Math.floor(worldLocation.z) }, }; this.updateOptions(); - this.outliner = new Outliner(dimensionId, this.getBounds().min, this.getBounds().max); + this.outliner = new Outliner(dimensionId, this.toGlobalCoords(this.getBounds().min), this.toGlobalCoords(this.getBounds().max)); } remove() { @@ -131,6 +114,45 @@ export class Structure { this.#options.currentLayer = layer; this.updateOptions(); this.outliner.stopDraw(); - this.outliner = new Outliner(this.#options.dimensionId, this.getLayeredBounds().min, this.getLayeredBounds().max); + const { min, max } = this.getLayeredBounds(); + this.outliner = new Outliner(this.#options.dimensionId, this.toGlobalCoords(min), this.toGlobalCoords(max)); + } + + isLocationInStructure(structureLocation) { + const { min, max } = this.getBounds(); + return structureLocation.x >= min.x && structureLocation.x < max.x + && structureLocation.y >= min.y && structureLocation.y < max.y + && structureLocation.z >= min.z && structureLocation.z < max.z; + } + + isLocationInLayer(structureLocation) { + const { min, max } = this.getLayeredBounds(true); + return structureLocation.x >= min.x && structureLocation.x < max.x + && structureLocation.y >= min.y && structureLocation.y < max.y + && structureLocation.z >= min.z && structureLocation.z < max.z; + } + + isLocationActive(structureLocation) { + if (!this.#options.isPlaced) + return false + if (this.#options.currentLayer > 0) + return this.isLocationInLayer(structureLocation); + return this.isLocationInStructure(structureLocation); + } + + toGlobalCoords(structureLocation) { + return { + x: this.#options.worldLocation.x + structureLocation.x, + y: this.#options.worldLocation.y + structureLocation.y, + z: this.#options.worldLocation.z + structureLocation.z + }; + } + + toStructureCoords(worldLocation) { + return { + x: worldLocation.x - this.#options.worldLocation.x, + y: worldLocation.y - this.#options.worldLocation.y, + z: worldLocation.z - this.#options.worldLocation.z + }; } } \ No newline at end of file diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index c393b19..10b838c 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -8,6 +8,9 @@ class StructureCollection { } add(name) { + if (this.#structures[name]) { + throw new Error(`Structure ${name} already exists.`); + } const structure = new Structure(name); this.#structures[name] = structure; return structure; @@ -26,6 +29,10 @@ class StructureCollection { struct.remove(); delete this.#structures[name]; } + + getStructuresAtLocation(location) { + return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); + } } export const structureCollection = new StructureCollection(); \ No newline at end of file diff --git a/scripts/commands/struct.js b/scripts/commands/struct.js index d4cf73e..91a58af 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -7,7 +7,7 @@ import { MaterialCounter } from '../classes/MaterialCounter'; const structCmd = new Command({ name: 'struct', description: { text: 'Manages current StrucTool structures.' }, - usage: 'struct', + usage: 'struct [args...]', callback: structCommand, args: [ { type: 'string', name: 'name' }, @@ -41,7 +41,17 @@ function structCommand(sender, args) { } function addStructure(sender, name) { - structureCollection.add(name); + try { + structureCollection.add(name); + } catch (e) { + if (e.message.includes('already exists')) { + sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); + return; + } else { + sender.sendMessage({ text: `§cFailed to add structure '${name}'.` }); + throw e; + } + } sender.sendMessage({ text: `§7Added structure '${name}'` }); } @@ -63,8 +73,16 @@ function placeStructure(sender, name) { try { structure = structureCollection.add(name); } catch (e) { - sender.sendMessage({ text: `§cStructure '${name}' not found.` }); - return; + if (e.message.includes('already exists')) { + sender.sendMessage({ text: `§cStructure '${name}' already exists.` }); + return; + } else if (e.message.includes('not found')) { + sender.sendMessage({ text: `§cStructure '${name}' not found.` }); + return; + } else { + sender.sendMessage({ text: `§cFailed to place structure '${name}'.` }); + throw e; + } } } structure.place(sender.dimension.id, sender.location); diff --git a/scripts/data.js b/scripts/data.js new file mode 100644 index 0000000..5c8eb22 --- /dev/null +++ b/scripts/data.js @@ -0,0 +1,39 @@ +export const bannedEasyPlaceBlocks = [ + 'air', 'bed', + 'wooden_door', 'spruce_door', 'birch_door', 'jungle_door', 'acacia_door', 'dark_oak_door', 'mangrove_door', 'cherry_door', 'pale_oak_door', + 'bamboo_door', 'iron_door', 'crimson_door', 'warped_door', 'copper_door', 'exposed_copper_door', 'weathered_copper_door', 'oxidized_copper_door', + 'waxed_copper_door', 'waxed_exposed_copper_door', 'waxed_weathered_copper_door', 'waxed_oxidized_copper_door', + 'piston_arm_collision', 'sticky_piston_arm_collision', "skeleton_skull" +]; + +export const bannedToValidBlockMap = { + 'lit_furnace': 'furnace', + 'lit_smoker': 'smoker', + 'lit_blast_furnace': 'blast_furnace', + 'lit_redstone_ore': 'redstone_ore', + 'unlit_redstone_torch': 'redstone_torch', + 'bubble_column': 'water', + // 'seagrass'? 'tall_seagrass'? kelp? +}; + +export const defaultStates = { + growth: 0, + age: 0, + height: 0, + bite_counter: 0, + fill_level: 0, + redstone_signal: 0, + cluster_count: 0, + respawn_anchor_charge: 0, + turtle_egg_count: "one_egg", + cauldron_liquid: "water" +}; + +export const allowedBlockStates = { + water: { depth_level: 0 }, + lava: { depth_level: 0 } +}; + +export const bannedDimensionBlocks = { + 'nether': ['water', 'bubble_column'] +}; \ No newline at end of file diff --git a/scripts/main.js b/scripts/main.js index f03bc75..47755ac 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,4 +1,5 @@ // Rules +import './rules/easyPlace'; // Commands -import './commands/struct.js'; +import './commands/struct'; diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js new file mode 100644 index 0000000..94185fe --- /dev/null +++ b/scripts/rules/easyPlace.js @@ -0,0 +1,77 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { EntityComponentTypes, GameMode, system, world } from '@minecraft/server'; +import { structureCollection } from '../classes/StructureCollection'; +import { bannedEasyPlaceBlocks } from '../data'; + +const easyPlace = new Rule({ + identifier: 'easyPlace', + description: { text: 'Places a structure with ease.' }, + onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); }, + onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } +}) +extension.addRule(easyPlace); + +function onPlayerPlaceBlock(event) { + const { player, block } = event; + if (!player || !block) return; + const structureBlock = fetchStructureBlock(block.location); + if (!structureBlock || isBannedBlock(structureBlock)) + return; + const states = structureBlock.getAllStates(); + tryPlaceBlock(event, player, block, structureBlock); +} + +function fetchStructureBlock(location) { + const locatedStructures = structureCollection.getStructuresAtLocation(location); + if (locatedStructures.length === 0) + return void 0; + const structure = locatedStructures[0]; + return structure.getBlock(structure.toStructureCoords(location)); +} + +function isBannedBlock(structureBlock) { + return bannedEasyPlaceBlocks.some(bannedId => structureBlock.type.id.replace('minecraft:', '') === bannedId); +} + +function tryPlaceBlock(event, player, block, structureBlock) { + if (player.getGameMode() === GameMode.creative) { + placeBlock(block, structureBlock); + } else if (player.getGameMode() === GameMode.survival) { + tryPlaceBlockSurvival(event, player, block, structureBlock); + } +} + +function tryPlaceBlockSurvival(event, player, block, structureBlock) { + const itemSlotToUse = fetchMatchingItemSlot(player, structureBlock); + if (itemSlotToUse) { + event.cancel = true; + placeBlock(block, structureBlock, itemSlotToUse); + } +} + +function fetchMatchingItemSlot(player, structureBlock) { + const itemToMatch = structureBlock.getItemStack(); + if (!itemToMatch) + return void 0; + const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; + if (!inventory) + return void 0; + for (let index = 0; index < inventory.size; index++) { + const itemSlot = inventory.getSlot(index); + if (itemSlot.hasItem() && itemSlot?.typeId === itemToMatch.typeId) + return itemSlot; + } +} + +function placeBlock(block, structureBlock, itemSlot) { + system.run(() => { + if (itemSlot) { + if (itemSlot.amount === 1) + itemSlot.setItem(void 0); + else + itemSlot.amount--; + } + block.setPermutation(structureBlock); + }); +} \ No newline at end of file