From fb3deea181412c60ca6767193336285c4e3e2a5d Mon Sep 17 00:00:00 2001 From: ForestOfLight Date: Sun, 16 Mar 2025 22:46:20 -0700 Subject: [PATCH] add showStructBlockInfo, fastEasyPlace, and add arrow slot for easyPlace --- scripts/classes/Raycaster.js | 48 +++++++++ scripts/classes/StructureCollection.js | 8 ++ scripts/commands/struct.js | 1 - scripts/data.js | 31 ------ scripts/main.js | 2 + scripts/rules/easyPlace.js | 16 ++- scripts/rules/fastEasyPlace.js | 140 +++++++++++++++++++++++++ scripts/rules/showStructBlockInfo.js | 35 +++++++ 8 files changed, 246 insertions(+), 35 deletions(-) create mode 100644 scripts/classes/Raycaster.js create mode 100644 scripts/rules/fastEasyPlace.js create mode 100644 scripts/rules/showStructBlockInfo.js diff --git a/scripts/classes/Raycaster.js b/scripts/classes/Raycaster.js new file mode 100644 index 0000000..bbe64e9 --- /dev/null +++ b/scripts/classes/Raycaster.js @@ -0,0 +1,48 @@ +import { world } from "@minecraft/server"; +import { structureCollection } from "./StructureCollection"; + +export class Raycaster { + static STEP_SIZE = 0.2; + + static getStructureBlocks(dimension, startLocation, direction, { maxDistance, getFirst = true, collideWithWorldBlocks = true }) { + // Can probably be optimized by the fact that we only need full blocks and aren't checking for partial blocks + const blocks = []; + let location = startLocation; + let distance = 0; + while (distance < maxDistance) { + const locatedStructures = structureCollection.getStructuresAtLocation(location); + if (locatedStructures.length !== 0) { + const structure = locatedStructures[0]; + const block = structure.getBlock(structure.toStructureCoords(location)); + if (collideWithWorldBlocks && !dimension.getBlock(location)?.isAir) + break; + if (block?.type.id !== 'minecraft:air') { + blocks.push({ + permutation: block, + location: location + }); + if (getFirst) + break; + } + } + location = { + x: location.x + (direction.x*this.STEP_SIZE), + y: location.y + (direction.y*this.STEP_SIZE), + z: location.z + (direction.z*this.STEP_SIZE) + }; + distance += this.STEP_SIZE; + } + // world.getDimension('minecraft:overworld').spawnParticle('minecraft:villager_happy', location); + return blocks; + } + + static getTargetedStructureBlock(player, { isFirst = true, collideWithWorldBlocks = true }) { + const startLocation = player.getHeadLocation(); + const direction = player.getViewDirection(); + const maxDistance = 7; + const blocks = this.getStructureBlocks(player.dimension, startLocation, direction, { maxDistance, getFirst: isFirst, collideWithWorldBlocks }); + if (blocks.length === 0) + return void 0; + return isFirst ? blocks[0] : blocks[blocks.length - 1]; + } +} \ No newline at end of file diff --git a/scripts/classes/StructureCollection.js b/scripts/classes/StructureCollection.js index 10b838c..c558a04 100644 --- a/scripts/classes/StructureCollection.js +++ b/scripts/classes/StructureCollection.js @@ -33,6 +33,14 @@ class StructureCollection { getStructuresAtLocation(location) { return Object.values(this.#structures).filter(structure => structure.isLocationActive(structure.toStructureCoords(location))); } + + fetchStructureBlock(location) { + const locatedStructures = this.getStructuresAtLocation(location); + if (locatedStructures.length === 0) + return void 0; + const structure = locatedStructures[0]; + return structure.getBlock(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 91a58af..b3664d0 100644 --- a/scripts/commands/struct.js +++ b/scripts/commands/struct.js @@ -1,7 +1,6 @@ import { Command } from '../lib/canopy/CanopyExtension'; import { extension } from '../config'; import { structureCollection } from '../classes/StructureCollection'; -import { system } from '@minecraft/server'; import { MaterialCounter } from '../classes/MaterialCounter'; const structCmd = new Command({ diff --git a/scripts/data.js b/scripts/data.js index e5f45c9..ccb7b80 100644 --- a/scripts/data.js +++ b/scripts/data.js @@ -39,37 +39,6 @@ export const resetToBlockStates = { cluster_count: 0 }; -export const chainedStatePlacements = { - cauldron: { - cauldron_liquid: { - water: ['water_bucket'], - lava: ['lava_bucket'] - } - }, - sea_pickle: { - cluster_count: { - 1: ['sea_pickle'], - 2: ['sea_pickle', 'sea_pickle'], - 3: ['sea_pickle', 'sea_pickle', 'sea_pickle'] - } - }, - turtle_egg: { - turtle_egg_count: { - two_eggs: ['turtle_egg'], - three_eggs: ['turtle_egg', 'turtle_egg'], - four_eggs: ['turtle_egg', 'turtle_egg', 'turtle_egg'] - } - }, - respawn_anchor: { - respawn_anchor_charge: { - 1: ['glowstone'], - 2: ['glowstone', 'glowstone'], - 3: ['glowstone', 'glowstone', 'glowstone'], - 4: ['glowstone', 'glowstone', 'glowstone', 'glowstone'] - } - } -}; - export const blockIdToItemStackMap = { 'water': 'water_bucket', 'lava': 'lava_bucket', diff --git a/scripts/main.js b/scripts/main.js index 47755ac..da757bb 100644 --- a/scripts/main.js +++ b/scripts/main.js @@ -1,5 +1,7 @@ // Rules import './rules/easyPlace'; +import './rules/fastEasyPlace'; +import './rules/showStructBlockInfo'; // Commands import './commands/struct'; diff --git a/scripts/rules/easyPlace.js b/scripts/rules/easyPlace.js index 0bfe8dd..3a983da 100644 --- a/scripts/rules/easyPlace.js +++ b/scripts/rules/easyPlace.js @@ -5,9 +5,11 @@ import { structureCollection } from '../classes/StructureCollection'; import { bannedBlocks, bannedToValidBlockMap, whitelistedBlockStates, resetToBlockStates, bannedDimensionBlocks, specialItemPlacementConversions, blockIdToItemStackMap } from '../data'; +const ARROW_SLOT = 35; + const easyPlace = new Rule({ identifier: 'easyPlace', - description: { text: 'Places a structure with ease.' }, + description: { text: 'Simplifies placing blocks in a structure (arrow in bottom right inventory slot).' }, onEnableCallback: () => { world.beforeEvents.playerPlaceBlock.subscribe(onPlayerPlaceBlock); }, onDisableCallback: () => { world.beforeEvents.playerPlaceBlock.unsubscribe(onPlayerPlaceBlock); } }) @@ -15,13 +17,21 @@ extension.addRule(easyPlace); function onPlayerPlaceBlock(event) { const { player, block } = event; - if (!player || !block) return; + if (!player || !block || !hasArrowInCorrectSlot(player)) return; const structureBlock = fetchStructureBlock(block.location); if (!structureBlock) return; tryPlaceBlock(event, player, block, structureBlock); } +function hasArrowInCorrectSlot(player) { + const inventory = player.getComponent(EntityComponentTypes.Inventory)?.container; + if (!inventory) + return false; + const arrowSlot = inventory.getSlot(ARROW_SLOT); + return arrowSlot.hasItem() && arrowSlot.typeId === 'minecraft:arrow'; +} + function fetchStructureBlock(location) { const locatedStructures = structureCollection.getStructuresAtLocation(location); if (locatedStructures.length === 0) @@ -77,7 +87,7 @@ function tryConvertToDefaultState(structureBlock) { function tryPlaceBlockSurvival(event, player, block, structureBlock) { const placeableItemStack = getPlaceableItemStack(structureBlock); - console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); + // console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); if (itemSlotToUse) { event.cancel = true; diff --git a/scripts/rules/fastEasyPlace.js b/scripts/rules/fastEasyPlace.js new file mode 100644 index 0000000..c7c15de --- /dev/null +++ b/scripts/rules/fastEasyPlace.js @@ -0,0 +1,140 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +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'; + +let runner = void 0; +const easyPlace = new Rule({ + identifier: 'fastEasyPlace', + description: { text: 'Looking at structure blocks with an arrow in your hand will place them.' }, + onEnableCallback: () => { runner = system.runInterval(onTick, 2); }, + onDisableCallback: () => { system.clearRun(runner); } +}) +extension.addRule(easyPlace); + +function onTick() { + for (const player of world.getAllPlayers()) { + if (!player) + continue; + processEasyPlace(player); + } +} + +function processEasyPlace(player) { + if (!player || !isHoldingArrow(player)) return; + const structureBlock = Raycaster.getTargetedStructureBlock(player, { isFirst: true }); + if (!structureBlock) + return; + const worldBlock = player.dimension.getBlock(structureBlock.location); + tryPlaceBlock(player, worldBlock, structureBlock.permutation); +} + +function isHoldingArrow(player) { + const mainhandItemStack = player.getComponent(EntityComponentTypes.Equippable).getEquipment(EquipmentSlot.Mainhand); + if (!mainhandItemStack) + return false; + return mainhandItemStack.typeId === 'minecraft:arrow'; +} + +function tryPlaceBlock(player, worldBlock, structureBlock) { + if (isBannedBlock(player, structureBlock) || !locationIsPlaceable(worldBlock)) return; + structureBlock = tryConvertBannedToValidBlock(structureBlock); + if (player.getGameMode() === GameMode.creative) { + placeBlock(worldBlock, structureBlock); + } else if (player.getGameMode() === GameMode.survival) { + structureBlock = tryConvertToDefaultState(structureBlock); + tryPlaceBlockSurvival(player, worldBlock, structureBlock); + } +} + +function locationIsPlaceable(worldBlock) { + return worldBlock.isAir; +} + +function isBannedBlock(player, structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + if (bannedBlocks.includes(blockId)) + return true; + if (bannedDimensionBlocks[player.dimension.id.replace('minecraft:', '')]?.includes(blockId)) + return true; + const allowedStates = whitelistedBlockStates[blockId]; + if (allowedStates) { + for (const [stateKey, stateValue] of Object.entries(allowedStates)) { + if (structureBlock.getState(stateKey) !== stateValue) + return true; + } + } + return false; +} + +function tryConvertBannedToValidBlock(structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + if (Object.keys(bannedToValidBlockMap).includes(blockId)) + return BlockPermutation.resolve(bannedToValidBlockMap[blockId], structureBlock.getAllStates()); + return structureBlock; +} + +function tryConvertToDefaultState(structureBlock) { + const newStates = {}; + for (const [stateKey, stateValue] of Object.entries(structureBlock.getAllStates())) { + if (resetToBlockStates[stateKey] !== void 0 && stateValue !== resetToBlockStates[stateKey]) + newStates[stateKey] = resetToBlockStates[stateKey]; + else + newStates[stateKey] = stateValue; + } + return BlockPermutation.resolve(structureBlock.type.id, newStates); +} + +function tryPlaceBlockSurvival(player, block, structureBlock) { + const placeableItemStack = getPlaceableItemStack(structureBlock); + // console.warn(`Looking for item to place ${structureBlock?.type.id} (${placeableItemStack?.typeId})...`); + const itemSlotToUse = fetchMatchingItemSlot(player, placeableItemStack?.typeId); + if (itemSlotToUse) { + placeBlock(block, structureBlock, itemSlotToUse); + } +} + +function getPlaceableItemStack(structureBlock) { + const blockId = structureBlock.type.id.replace('minecraft:', ''); + const newItemId = blockIdToItemStackMap[blockId]; + return newItemId ? new ItemStack(newItemId) : structureBlock.getItemStack(); +} + +function fetchMatchingItemSlot(player, itemToMatchId) { + if (!itemToMatchId) + 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 === itemToMatchId) + return itemSlot; + } +} + +function placeBlock(block, structureBlock, itemSlot) { + system.run(() => { + if (itemSlot) { + consumeItem(itemSlot); + } + block.setPermutation(structureBlock); + }); +} + +function consumeItem(itemSlot) { + if (specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')]) { + consumeSpecial(itemSlot); + } else { + if (itemSlot.amount === 1) + itemSlot.setItem(void 0); + else + itemSlot.amount--; + } +} + +function consumeSpecial(itemSlot) { + itemSlot.setItem(new ItemStack(specialItemPlacementConversions[itemSlot.typeId.replace('minecraft:', '')])); +} \ No newline at end of file diff --git a/scripts/rules/showStructBlockInfo.js b/scripts/rules/showStructBlockInfo.js new file mode 100644 index 0000000..208ce2a --- /dev/null +++ b/scripts/rules/showStructBlockInfo.js @@ -0,0 +1,35 @@ +import { Rule } from '../lib/canopy/CanopyExtension'; +import { extension } from '../config'; +import { system, world } from '@minecraft/server'; +import { Raycaster } from '../classes/Raycaster'; + +let runner = void 0; +const showStructBlockInfo = new Rule({ + identifier: 'showStructBlockInfo', + description: { text: 'Shows block and state info for structure blocks you are looking at.' }, + onEnableCallback: () => { runner = system.runInterval(onTick); }, + onDisableCallback: () => { system.clearRun(runner); } +}) +extension.addRule(showStructBlockInfo); + +function onTick() { + for (const player of world.getAllPlayers()) { + if (!player) + continue; + showStructureBlockInfo(player); + } +} + +function showStructureBlockInfo(player) { + const block = Raycaster.getTargetedStructureBlock(player, { isFirst: true, collideWithWorldBlocks: true }); + if (!block) + return; + player.onScreenDisplay.setActionBar({ text: getFormattedBlockInfo(block.permutation) }); +} + +function getFormattedBlockInfo(block) { + const states = block.getAllStates(); + if (Object.keys(states).length === 0) + return `Structure:\n§a${block.type.id}`; + return `Structure:\n§a${block.type.id}\n§7${JSON.stringify(block.getAllStates())}`; +} \ No newline at end of file