Bedrock Wiki
  • Discord
  • Contribute
  • bedrock.dev
  • MS Learn
Beginner's Guide
  • Guide
    • 1. Introduction
      guide
    • 2. Add-Ons Explained
    • 3. Software & Preparation
    • 4. Project Setup
    • 5. Create a Custom Item
    • 6. Create a Custom Entity
    • 7. Blockbench: Modeling, Texturing & Animating
    • 8. Adding a Loot Table, Spawn Rule & Crafting Recipe
  • Extra
    • a. Understanding JSON
    • b. Download Example Packs
    • c. Troubleshooting
      help
    • d. Advanced Manifest
    • e. Format Versions
    • f. Project Setup Android
Animation Controllers
  • Intro to Animation Controllers
    guide
  • Entity Commands
  • AFK Detector
  • Death Commands
  • Molang into Scoreboard
  • Respawn Commands
Blocks
  • General
    • Intro to Blocks
      guide
    • Block Components
    • Block Tags
    • Block States
    • Block Traits
    • Block Permutations
    • Block Events
      Scripts
    • Block Event Migration
      help
    • Blocks as Items
    • Troubleshooting Blocks
      help
  • Visuals
    • Block Culling
    • Block Models
      guide
    • Block Texture Animation
    • Block Texture Variation
    • Block Tinting
  • Tutorials
    • Applying Constant Effects
      Scripts
    • Avoiding State Limit
    • Fake Blocks
      Scripts
    • Ore Loot Tables
      Scripts
    • Precise Interaction
      Scripts
    • Precise Rotation
      Scripts
    • Rotatable Blocks
  • Vanilla Re-Creations
    • Custom Crops
      Scripts
    • Custom Glass
    • Custom Glazed Terracotta
    • Custom Trapdoors
      Scripts
  • Documentation
    • Block Format History
    • Block Shapes
    • Block Sounds
    • Vanilla Block Models
Commands
  • General
    • Intro to Command Blocks
    • Functions
    • Block States
    • Coordinate System
    • NBT Commands
    • Scoreboard Operations
    • Understanding Selectors
  • Commands
    • Damage
    • Execute
    • Playanimation
    • Playsound
  • On Event Systems
    • On Player First Join
    • On Player Join
    • On Player Leave
    • On Player Death
    • On Player Respawn
    • On First World Load
  • Scoreboard Systems
    • Entity Counter
    • Scoreboard Timers
    • Comparing And Retrieving Scores
  • Techniques
    • Execute Logic Gates
    • MBE - Max's Block Entity
    • FMBE - A New Way to Create Display Entities
    • Look Detection
    • Movement Detections
    • Orbital Camera
  • Useful Creations
    • Custom Crafter
    • Multiplayer Position Rearrangement
      function
Concepts
  • contents.json
  • Emojis & Symbols
  • Molang
  • Namespaces
  • Overwriting Assets
  • Raw Text
  • Shaders
  • Sounds
  • Subpacks
  • Text and Localization
  • Texture Atlases
  • textures_list.json
Documentation
  • Shared Constructs
  • Advanced Molang
  • File Types
  • Fog IDs
  • Material Configuration Description
  • Menu Categories
  • Molang Queries
  • Pack Folder Structure
  • Sound Definitions
  • Vanilla Materials
Entities
  • General
    • Intro to Entities BP
      guide
    • Intro to Entities RP
      guide
    • Troubleshooting Entities
      help
    • Entity Events
    • Entity Properties
    • NPC Dialogues
    • Render Controllers
    • Spawn Rules
  • Tutorials
    • Convert Points Between Any Space (World, Entity, Bones)
    • Creating Boats
    • Detecting Other Entities
    • Disabling Team Damage
    • Dummy Entities
    • Entity Attacks
    • Entity Holds Item
    • Entity Movement
    • Entity Timers
    • Flying Entities
    • Introduction to AOE Clouds
    • Invulnerable Entities
    • Look at Entity
    • Sleeping Entities
    • Solid Entities
    • Spawning Tamed Entities
      Scripts
    • Village Mechanic
  • Documentation
    • Dummy Components
    • Non-Mob Runtime Identifiers
    • Projectiles
    • Runtime Identifiers
    • Vanilla Usage Components
    • Vanilla Usage Spawn Rules
Items
  • General
    • Intro to Items
      guide
    • Item Components
    • Item Tags
    • Item Events
      Scripts
    • Item Event Migration
      help
    • Troubleshooting Items
      help
  • Tutorials
    • Custom Armor
    • Custom Food
      Scripts
    • Custom Pottery Sherds
    • Custom Weapons
    • Equipment-Based Commands
    • High Resolution Items
    • Spawning Items
    • Throwable Items
  • Documentation
    • Enchantments
    • Attachables
    • Item Format History
    • Numerical Item IDs
    • Vanilla Item Identifiers
    • Vanilla Usage Components
JSON UI
  • General
    • Intro to JSON UI
      guide
    • Best Practices
      guide
  • Tutorials
    • Adding HUD Elements
    • Aseprite Animations
    • Buttons and Toggles
    • Modifying Server Forms
    • Preserve Title Texts
    • String to Number
  • Documentation
    • JSON UI Documentation
Loot, Recipes & Trading
  • General
    • Trading Behavior
  • Documentation
    • Loot Tables
    • Trade Tables
    • Recipes
    • Item Functions
  • Tutorials
    • Randomized Structure Loot
Meta
  • Add-On Performance
  • Style Guide
  • Useful Links
  • Using Schemas
  • Version Control
  • Q&A
    • Blocks and Items Q&A 2024/08/30
    • Deferred Technical Preview Q&A 2024/02/23
    • GameTest Q&A 2021/08/06
    • Scripting and Editor Q&A 2023/09/22
    • World Generation Q&A 2024/11/15
NBT
  • General
    • .mcstructure
  • Tutorials
    • Experiments in Minecraft Education
    • Extending Structure Limits
  • NBT in Depth
    • About NBT (Named Binary Tag)
    • NBT Libraries
    • Reading NBT Example
Particles
  • General
    • Intro to Particles
      guide
  • Tutorials
    • Disabling Particles
  • Documentation
    • Vanilla Particles
Scripting
  • General
    • Intro to Scripting
    • What is Script API?
    • API Modules
  • Tutorials
    • Block Placement Prevention
    • GameTests
    • Script Core Features
    • Script Forms
    • Script Requests API
    • Simple Chat Commands
  • Documentation
    • Engine Environment
    • Script Resources
    • Script Watchdog
    • Troubleshooting JavaScript
    • TypeScript
Servers
  • Software
    • Bedrock Server Software
  • Protocols
    • Bedrock Protocol
    • NetherNet Protocol
    • RakNet Protocol
Visuals
  • General
    • Introduction to Entity Visuals
      guide
    • Bedrock Modeling
    • Custom Death Animations
    • Effects in Animations
    • Material Creations
    • Materials
    • Math-Based Animations
    • Skin Packs
  • Tutorials
    • Entity Texture Animation
    • Glowing Entity Texture
    • Hurt Animations
    • Leash Position
    • Player Geometry
    • Remove Entity Shadows
    • Retexturing Spawn Eggs
  • Ideas
    • Structure Presentation
World Generation
  • General
    • Intro to World Generation
      guide
    • Biomes
      guide
    • Feature Types
    • Jigsaw Structures
  • Tutorials
    • Block Conditions for Features
    • Generating Custom Ores
    • Generating Custom Structures
    • Generating Patches
    • Heightmap Noise
  • Documentation
    • Biome Tags

Precise Interaction

expert
scripting
Precise Interaction
  • How it Works
  • FaceSelectionPlains Class
    • Methods
    • Usage
  • SelectionBoxes Class
    • Methods
    • Usage
  • Pigeonholes Example
  • Double Flower Pot Example
  • Importing Scripts
  • Download Example Pack

FORMAT & MIN ENGINE VERSION 1.21.70

This tutorial assumes an advanced understanding of blocks and scripting. Check out the blocks and scripting guides before starting.

The ability to create custom blocks that the player can interact with can be very basic to implement, yet still allow for complex functionality. However, sometimes the default interaction mode, which is based on simply right-clicking or tapping the block without location-specific conditions, is not enough to achieve the desired functionality.

For example, what if you want to create a block that has multiple buttons on one side, and each one triggers a different action? Or a segment display, where multiple individually-lit lamps can be contained within one block.

This is where precise interaction comes in! The following methods of precise interaction allow you to define multiple areas in a block that can be interacted with separately, and assign different functions to each area. In this tutorial, we will show you how to add precise interaction to your blocks using scripts, with examples of each method.

Note: Precise interaction does not enable blocks to have multiple/custom-shaped minecraft:selection_box components. The selection box must be within all defined areas for precise interaction to function properly.

Showcase image displaying example Pigeonholes and Double Flower Pot blocks

How it Works ​

The provided methods of precise interaction use faceLocation, a property of the player interact event.

This value tells us where on the block's minecraft:selection_box was selected/hit, which is what precise interaction relies on.

MCPE-216825

The faceLocation property is supposed to be relative to the bottom north-west corner of the interacted block, however it is currently relative to the world origin, meaning we will have to perform an additional calculation to make it relative. When this issue is resolved, this calculation will no longer be needed.

FaceSelectionPlains Class ​

This class allows you to define 2D areas on a block's face and get the selected plain.

To use this method for precise interaction, create the file BP/scripts/utilities/face_selection_plains.js and paste the below code into it.

FaceSelectionPlains Code
BP/scripts/utilities/face_selection_plains.js
js
import { Direction } from "@minecraft/server";

const isInRange = (value, min, max) => value >= min && value <= max;

export default class FaceSelectionPlains {
    /**
     * Allows you to define 2D areas on a block's face and get the selected plain.
     *
     * @param {Object[]} plains Spread array defining the 2D areas on a block's face which may be selected.
     * @param {[number, number]} plains[].origin [U, V] array defining the offset of the plain from the top left of the block face in pixels.
     * @param {[number, number]} plains[].size [U, V] array defining the size of the plain, extending from the top left in pixels.
     * @param {string} [plains[].name] Custom name to easily identify this plain when it is selected.
     */
    constructor(...plains) {
        this.plains = plains;
    }
    /**
     * @param {Object} selection
     * @param {Direction} selection.face
     * @param {import("@minecraft/server").Vector3} selection.faceLocation
     *
     * @param {Object} [options]
     * @param {boolean} [options.invertU] Horizontal axis extends `right -> left` rather than `left -> right` if true.
     * @param {boolean} [options.invertV] Vertical axis extends `bottom -> top` rather than `top -> bottom` if true.
     *
     * @returns Selected plain ID, or plain index if an ID is not provided. If no plains apply to the selection, `undefined` is returned.
     */
    getSelected(selection, options) {
        const { face, faceLocation } = selection;

        // Create a new object so the original is not mutated
        let location = { ...faceLocation };

        const horizontalAxis = face === Direction.East || face === Direction.West ? "z" : "x";
        const verticalAxis = face === Direction.Up || face === Direction.Down ? "z" : "y";

        if (face !== Direction.Down) location[verticalAxis] = 1 - location[verticalAxis];
        if (face !== Direction.South && face !== Direction.West)
            location[horizontalAxis] = 1 - location[horizontalAxis];

        if (options?.invertU) location[horizontalAxis] = 1 - location[horizontalAxis];
        if (options?.invertV) location[verticalAxis] = 1 - location[verticalAxis];

        for (let i = 0; i < this.plains.length; i++) {
            const plain = this.plains[i];

            const inHorizontalRange = isInRange(
                location[horizontalAxis],
                plain.origin[0] / 16,
                (plain.origin[0] + plain.size[0]) / 16
            );
            const inVerticalRange = isInRange(
                location[verticalAxis],
                plain.origin[1] / 16,
                (plain.origin[1] + plain.size[1]) / 16
            );

            if (inHorizontalRange && inVerticalRange) return plain.name ?? i;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

Methods ​

  • constructor ​

    ts
    new FaceSelectionPlains(...plains: { origin: [number, number]; size: [number, number]; name?: string }[])
    1

    Creates a new FaceSelectionPlains instance.

    Parameters
    • plains: Object[]

      Array defining the 2D areas on a block's face which may be selected.

      • origin: [number, number]

        [U, V] array defining the offset of the plain from the top left of the block face.

      • size: [number, number]

        [U, V] array defining the size of the plain, extending from the top left.

      • name?: string

        Custom name to easily identify this plain when it is selected.

  • getSelected ​

    ts
    getSelected(selection: { face: Direction; faceLocation: Vector3 }, options?: { invertU?: boolean; invertV?: boolean }): number | string | undefined
    1

    Returns the involved plain's array index, or name if provided. If no plain is selected, undefined is returned.

    Parameters
    • selection: Object

      Object containing details about the selection.

      • face: Direction

        The selected face of the block.

      • faceLocation: Vector3

        Selection location relative to the bottom north-west corner of the block.

    • options?: Object

      Optionally configure how the selected plain is calculated.

      • invertU?: boolean

        Horizontal axis goes right -> left rather than left -> right if true.

      • invertV?: boolean

        Vertical axis goes bottom -> top rather than top -> bottom if true.

Usage ​

The below example would split the targeted block face into quarters:

BP/scripts/blocks/example.js
js
import { world } from "@minecraft/server";
import FaceSelectionPlains from "../utilities/face_selection_plains";

const quadrants = new FaceSelectionPlains(
    { origin: [0, 0], size: [8, 8] },
    { origin: [8, 0], size: [8, 8] },
    { origin: [0, 8], size: [8, 8] },
    { origin: [8, 8], size: [8, 8] }
);
1
2
3
4
5
6
7
8
9

Additionally, names can be provided to easily identify each plain:

js
const quadrants = new FaceSelectionPlains(
    { origin: [0, 0], size: [8, 8], name: "top_left" },
    { origin: [8, 0], size: [8, 8], name: "top_right" },
    { origin: [0, 8], size: [8, 8], name: "bottom_left" },
    { origin: [8, 8], size: [8, 8], name: "bottom_right" }
);
1
2
3
4
5
6

This could be used in a custom component to get the selected quadrant:

js
const BlockQuadrantInteractionComponent = {
    onPlayerInteract({ block, face, faceLocation }) {
        // Work around the faceLocation bug - get the location relative to the block
        const relativeFaceLocation = {
            x: faceLocation.x - block.location.x,
            y: faceLocation.y - block.location.y,
            z: faceLocation.z - block.location.z,
        };

        // Returns the selected area's index (0, 1, 2 or 3), or name if provided (e.g. "top_left").
        // If no plain was selected, `undefined` is retured.
        const selectedQuadrant = quadrants.getSelected({
            face,
            faceLocation: relativeFaceLocation,
        });

        world.sendMessage(`Quadrant ${selectedQuadrant} was selected!`);
    },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

SelectionBoxes Class ​

WARNING

As with the minecraft:selection_box and minecraft:collision_box components, do not use Blockbench's displayed position values when setting up your SelectionBoxes as they are measured from the north-west, rather than the north-east. Instead, use the origin value from the exported .geo.json file.

If you wish to use Blockbench's values, the invertX option should be set to true in getSelected.

This class allows you to define 3D areas in a block and get the box which the face selection lies within.

To use this method for precise interaction, create the file BP/scripts/utilities/selection_boxes.js and paste the below code into it.

SelectionBoxes Code
BP/scripts/utilities/selection_boxes.js
js
const isInRange = (value, min, max) => value >= min && value <= max;

export default class SelectionBoxes {
    /**
     * Allows you to define 3D areas in a block and get the box which the face selection lies within.
     *
     * @param {Object[]} boxes Array defining the 3D areas within a block which may be selected.
     * @param {[number, number, number]} boxes[].origin [X, Y, Z] array defining the offset of the box from the block's horizontal middle and vertical bottom in pixels, extending from the north-east.
     * @param {[number, number, number]} boxes[].size [X, Y, Z] array defining the size of the box in pixels, extending from the north-east.
     * @param {string} [boxes[].name] Custom name to easily identify this box when it is selected.
     */
    constructor(...boxes) {
        this.boxes = boxes;
    }
    /**
     * Get the box which the `faceLocation` lies within.
     *
     * @param {import("@minecraft/server").Vector3} faceLocation Selection location relative to the bottom north-west corner of the block.
     *
     * @param {Object} [options] Optionally configure how the selected box is calculated.
     * @param {boolean} [options.invertX] X axis extends `west -> east` rather than `east -> west` if true, following [Blockbench](https://blockbench.net)'s displayed positions.
     * @param {boolean} [options.invertY] Y axis extends `up -> down` rather than `down -> up` if true.
     * @param {boolean} [options.invertZ] Z axis extends `south -> north` rather than `north -> south` if true.
     *
     * @returns {(string|number|undefined)} Selected box name, or box index if a name is not provided. If no boxes apply to the selection, `undefined` is returned.
     */
    getSelected(faceLocation, options) {
        // Create a new object so the original is not mutated
        let location = { ...faceLocation };

        // X is inverted to ensure measurements are relative to the bottom north-east.
        if (!options?.invertX) location.x = 1 - location.x;
        if (options?.invertY) location.y = 1 - location.y;
        if (options?.invertZ) location.z = 1 - location.z;

        for (let i = 0; i < this.boxes.length; i++) {
            const box = this.boxes[i];

            const from = {
                x: box.origin[0] + 8,
                y: box.origin[1],
                z: box.origin[2] + 8,
            };
            const to = {
                x: from.x + box.size[0],
                y: from.y + box.size[1],
                z: from.z + box.size[2],
            };

            const inXRange = isInRange(location.x, from.x / 16, to.x / 16);
            const inYRange = isInRange(location.y, from.y / 16, to.y / 16);
            const inZRange = isInRange(location.z, from.z / 16, to.z / 16);

            if (inXRange && inYRange && inZRange) return box.name ?? i;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

Methods ​

  • constructor ​

    ts
    new SelectionBoxes(...boxes: { origin: [number, number, number]; size: [number, number, number]; name?: string }[])
    1

    Creates a new SelectionBoxes instance.

    Parameters
    • boxes: Object[]

      Array defining the 3D areas within a block which may be selected.

      • origin: [number, number, number]

        [X, Y, Z] array defining the offset of the box from the block's horizontal middle and vertical bottom in pixels, extending from the north-east.

      • size: [number, number, number]

        [X, Y, Z] array defining the size of the box in pixels, extending from the north-east.br>

      • name?: string

        Custom name to easily identify this box when it is selected.

  • getSelected ​

    ts
    getSelected(faceLocation: Vector3, options?: { invertX?: boolean; invertY?: boolean; invertZ?: boolean }): number | string | undefined
    1

    Get the box which the faceLocation lies within.

    Returns the involved box's array index, or name if provided. If no box is selected, undefined is returned.

    Parameters
    • faceLocation: Vector3

      Selection location relative to the bottom north-west corner of the block.

    • options?: Object

      Optionally configure how the selected box is calculated.

      • invertX?: boolean

        X axis extends west -> east rather than east -> west if true, following Blockbench's displayed positions.

      • invertY?: boolean

        Y axis extends up -> down rather than down -> up if true.

      • invertZ?: boolean

        Z axis extends south -> north rather than north -> south if true.

Usage ​

The below example would split the targeted block into its vertical halves:

BP/scripts/blocks/example.js
js
import { world } from "@minecraft/server";
import SelectionBoxes from "../utilities/selection_boxes";

const verticalHalves = new SelectionBoxes(
    { origin: [-8, 8, -8], size: [16, 8, 16], name: "top" },
    { origin: [-8, 0, -8], size: [16, 8, 16], name: "bottom" }
);
1
2
3
4
5
6
7

This could be used with an itemUseOn after event to get the selected box:

js
world.afterEvents.itemUseOn.subscribe((e) => {
    // Do nothing if the targeted block is not "wiki:example_block"
    if (e.block.typeId !== "wiki:example_block") return;

    // Returns the selected vertical half ("top" or "bottom").
    const selectedVerticalHalf = verticalHalves.getSelected(e.faceLocation);

    world.sendMessage(`The ${selectedVerticalHalf} of the block was selected!`);
});
1
2
3
4
5
6
7
8
9

Pigeonholes Example ​

Using our FaceSelectionPlains class, we can create a block which functions similarly to a Chiselled Bookshelf. Other assets (textures etc.) are included in the example pack.

Interacting with paper will fill the selected slot. Destroying the block releases all of the stored paper items.

Pigeonholes Showcase

Download Pigeonholes Model
Block JSON
BP/blocks/pigeonholes.json
json
{
    "format_version": "1.21.70",
    "minecraft:block": {
        "description": {
            "identifier": "wiki:pigeonholes",
            "menu_category": {
                "category": "items"
            },
            "states": {
                "wiki:slot_0_occupied": [false, true],
                "wiki:slot_1_occupied": [false, true],
                "wiki:slot_2_occupied": [false, true],
                "wiki:slot_3_occupied": [false, true],
                "wiki:slot_4_occupied": [false, true],
                "wiki:slot_5_occupied": [false, true]
            },
            "traits": {
                "minecraft:placement_direction": {
                    "enabled_states": ["minecraft:cardinal_direction"],
                    "y_rotation_offset": 180
                }
            }
        },
        "components": {
            "minecraft:custom_components": ["wiki:pigeonholes_storage"],
            "minecraft:destructible_by_mining": {
                "seconds_to_destroy": 1.5
            },
            "minecraft:geometry": {
                "identifier": "geometry.pigeonholes",
                "culling": "wiki:pigeonholes_culling",
                "bone_visibility": {
                    // Display each slot as empty/occupied
                    "empty_slot_0": "!q.block_state('wiki:slot_0_occupied')",
                    "empty_slot_1": "!q.block_state('wiki:slot_1_occupied')",
                    "empty_slot_2": "!q.block_state('wiki:slot_2_occupied')",
                    "empty_slot_3": "!q.block_state('wiki:slot_3_occupied')",
                    "empty_slot_4": "!q.block_state('wiki:slot_4_occupied')",
                    "empty_slot_5": "!q.block_state('wiki:slot_5_occupied')",
                    "occupied_slot_0": "q.block_state('wiki:slot_0_occupied')",
                    "occupied_slot_1": "q.block_state('wiki:slot_1_occupied')",
                    "occupied_slot_2": "q.block_state('wiki:slot_2_occupied')",
                    "occupied_slot_3": "q.block_state('wiki:slot_3_occupied')",
                    "occupied_slot_4": "q.block_state('wiki:slot_4_occupied')",
                    "occupied_slot_5": "q.block_state('wiki:slot_5_occupied')"
                }
            },
            "minecraft:material_instances": {
                "*": {
                    "texture": "stripped_bamboo_block_top"
                },
                // Material instances defined in model:
                "side": {
                    "texture": "stripped_bamboo_block"
                },
                "empty_slot": {
                    "texture": "wiki:pigeonholes_empty"
                },
                "occupied_slot": {
                    "texture": "wiki:pigeonholes_occupied"
                }
            }
        },
        "permutations": [
            // Facing north
            {
                "condition": "q.block_state('minecraft:cardinal_direction') == 'north'",
                "components": {
                    "minecraft:transformation": { "rotation": [0, 0, 0] }
                }
            },
            // Facing west
            {
                "condition": "q.block_state('minecraft:cardinal_direction') == 'west'",
                "components": {
                    "minecraft:transformation": { "rotation": [0, 90, 0] }
                }
            },
            // Facing south
            {
                "condition": "q.block_state('minecraft:cardinal_direction') == 'south'",
                "components": {
                    "minecraft:transformation": { "rotation": [0, 180, 0] }
                }
            },
            // Facing east
            {
                "condition": "q.block_state('minecraft:cardinal_direction') == 'east'",
                "components": {
                    "minecraft:transformation": { "rotation": [0, -90, 0] }
                }
            }
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
Precise Interaction Script
BP/scripts/blocks/pigeonholes.js
js
import { world, EquipmentSlot, GameMode, ItemStack } from "@minecraft/server";
import FaceSelectionPlains from "../utilities/face_selection_plains"; // Import the FaceSelectionPlains class to use it

// Slot bounds
const slots = new FaceSelectionPlains(
    { origin: [0, 0], size: [6, 8] },
    { origin: [6, 0], size: [5, 8] },
    { origin: [11, 0], size: [5, 8] },
    { origin: [0, 8], size: [6, 8] },
    { origin: [6, 8], size: [5, 8] },
    { origin: [11, 8], size: [5, 8] }
);

const isFrontFace = (block, face) =>
    block.permutation.getState("minecraft:cardinal_direction") === face.toLowerCase();

const isSlotOccupied = (block, slot) => block.permutation.getState(`wiki:slot_${slot}_occupied`);

const occupySlot = (block, slot) =>
    block.setPermutation(block.permutation.withState(`wiki:slot_${slot}_occupied`, true));

const emptySlot = (block, slot) =>
    block.setPermutation(block.permutation.withState(`wiki:slot_${slot}_occupied`, false));

function handleInteract({ block, face, faceLocation, dimension, player }) {
    if (!player || !isFrontFace(block, face)) return;

    const equippable = player.getComponent("minecraft:equippable");
    if (!equippable) return;

    const relativeFaceLocation = {
        x: faceLocation.x - block.location.x,
        y: faceLocation.y - block.location.y,
        z: faceLocation.z - block.location.z,
    };

    const selectedSlot = slots.getSelected({ face, faceLocation: relativeFaceLocation });
    if (selectedSlot === undefined) return;

    const mainhand = equippable.getEquipmentSlot(EquipmentSlot.Mainhand);
    const isHoldingPaper = mainhand.hasItem() && mainhand.typeId === "minecraft:paper";

    if (isHoldingPaper && !isSlotOccupied(block, selectedSlot)) {
        if (player.getGameMode() !== GameMode.creative) {
            if (mainhand.amount > 1) mainhand.amount--;
            else mainhand.setItem(undefined);
        }

        occupySlot(block, selectedSlot);
        dimension.playSound("insert.chiseled_bookshelf", block.center());
    } else if (isSlotOccupied(block, selectedSlot)) {
        emptySlot(block, selectedSlot);

        const itemLocation = { ...faceLocation };
        itemLocation.y -= 0.5;
        dimension.spawnItem(new ItemStack("minecraft:paper"), itemLocation).clearVelocity();

        dimension.playSound("pickup.chiseled_bookshelf", block.center());
    }
}

// ------------------------------
//  Release paper on destruction
// ------------------------------
function releasePaper({ block, destroyedBlockPermutation, dimension }) {
    const states = destroyedBlockPermutation.getAllStates();

    for (const state in states) {
        const value = states[state];
        const isPaper = value === true;

        if (!isPaper) continue;

        dimension.spawnItem(new ItemStack("minecraft:paper"), block.center());
    }
}

/** @type {import("@minecraft/server").BlockCustomComponent} */
const BlockPigeonholesStorageComponent = {
    onPlayerInteract: handleInteract,
    onPlayerDestroy: releasePaper,
};

world.beforeEvents.worldInitialize.subscribe(({ blockComponentRegistry }) => {
    blockComponentRegistry.registerCustomComponent(
        "wiki:pigeonholes_storage",
        BlockPigeonholesStorageComponent
    );
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

Double Flower Pot Example ​

Using our SelectionBoxes class, the player can interact with each pot separately. The following files are the basis for implementing a new Double Flower Pot block, other assets (textures etc.) are included in the example pack.

Note: The pots in this example only support the planting of dandelions and cacti for simplicity - you could expand this further yourself.

Double Flower Pot Showcase

Download Double Flower Pot Model
Block JSON
BP/blocks/double_flower_pot.json
json
{
    "format_version": "1.21.70",
    "minecraft:block": {
        "description": {
            "identifier": "wiki:double_flower_pot",
            "menu_category": {
                "category": "items"
            },
            "states": {
                "wiki:pot_0_plant": ["none", "dandelion", "cactus"],
                "wiki:pot_1_plant": ["none", "dandelion", "cactus"]
            },
            "traits": {
                "minecraft:placement_direction": {
                    "enabled_states": ["minecraft:cardinal_direction"]
                }
            }
        },
        "components": {
            "minecraft:custom_components": ["wiki:double_flower_pot"],
            "minecraft:collision_box": {
                "origin": [-7, 0, -3],
                "size": [14, 6, 6]
            },
            // This must cover all boxes in the precise interaction script
            "minecraft:selection_box": {
                "origin": [-7, 0, -3],
                "size": [14, 6, 6]
            },
            "minecraft:geometry": {
                "identifier": "geometry.double_flower_pot",
                // Conditionally display plants in their pots
                "bone_visibility": {
                    "dandelion_0": "q.block_state('wiki:pot_0_plant') == 'dandelion'",
                    "dandelion_1": "q.block_state('wiki:pot_1_plant') == 'dandelion'",
                    "cactus_0": "q.block_state('wiki:pot_0_plant') == 'cactus'",
                    "cactus_1": "q.block_state('wiki:pot_1_plant') == 'cactus'"
                }
            },
            "minecraft:material_instances": {
                "*": {
                    "texture": "flower_pot",
                    "render_method": "alpha_test",
                    "ambient_occlusion": false
                },
                // Material instances defined in model:
                "dirt": {
                    "texture": "wiki:double_flower_pot_dirt", // Apply a darker tint to the dirt texture to replicate vanilla potted dirt
                    "render_method": "alpha_test",
                    "ambient_occlusion": false
                },
                "handle": {
                    "texture": "wiki:double_flower_pot_handle",
                    "render_method": "alpha_test"
                },
                "dandelion": {
                    "texture": "yellow_flower",
                    "render_method": "alpha_test",
                    "face_dimming": false,
                    "ambient_occlusion": false
                },
                "cactus_side": {
                    "texture": "cactus_side",
                    "render_method": "alpha_test"
                },
                "cactus_top": {
                    "texture": "cactus_top",
                    "render_method": "alpha_test"
                }
            }
        },
        "permutations": [
            {
                "condition": "q.block_state('minecraft:cardinal_direction') == 'west' || q.block_state('minecraft:cardinal_direction') == 'east'",
                "components": {
                    "minecraft:transformation": { "rotation": [0, 90, 0] } // Front of model facing east
                }
            }
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
Precise Interaction Script
BP/scripts/blocks/double_flower_pot.js
js
import { world, ItemStack } from "@minecraft/server";
import SelectionBoxes from "../utilities/selection_boxes"; // Import the SelectionBoxes class to use it

// Support orientation along both horizontal axes
const pots = {
    x: new SelectionBoxes(
        { origin: [-7, 0, -3], size: [6, 6, 6] },
        { origin: [1, 0, -3], size: [6, 6, 6] }
    ),
    z: new SelectionBoxes(
        { origin: [-3, 0, -7], size: [6, 6, 6] },
        { origin: [-3, 0, 1], size: [6, 6, 6] }
    ),
};

// The state value and sound associated with each plant
const plants = {
    "minecraft:yellow_flower": {
        value: "dandelion",
        sound: "dig.grass",
    },
    "minecraft:cactus": {
        value: "cactus",
        sound: "dig.cloth",
    },
};

// Get the selected pot for the appropriate axis
const getSelectedPot = (e) =>
    pots[e.block.permutation.getState("wiki:axis")].getSelected(e.faceLocation);

const isPotOccupied = (block, pot) =>
    block.permutation.getState(`wiki:pot_${pot}_plant`) !== "none";

const setPotPlant = (block, pot, plant) =>
    block.setPermutation(block.permutation.withState(`wiki:pot_${pot}_plant`, plant));

// Our flower pots even have sound effects!
const playPlantSound = (dimension, location, sound) =>
    dimension.runCommand(`playsound ${sound} @a ${location.x} ${location.y} ${location.z} 0.5`);

// If a pot is not selected (the inbetween area is targeted) or is already filled, the item use is cancelled.
world.beforeEvents.itemUseOn.subscribe((e) => {
    if (e.block.typeId !== "wiki:double_flower_pot" || !plants[e.itemStack.typeId]) return;

    const selectedPot = getSelectedPot(e);

    if (selectedPot === undefined || isPotOccupied(e.block, selectedPot)) e.cancel = true;
});

// -------------------------------
//    Plant in the selected pot
// -------------------------------
world.afterEvents.itemUseOn.subscribe((e) => {
    if (
        e.block.typeId !== "wiki:double_flower_pot" ||
        !plants[e.itemStack.typeId] ||
        e.source.isSneaking
    )
        return;

    const selectedPot = getSelectedPot(e);
    const plant = plants[e.itemStack.typeId];

    setPotPlant(e.block, selectedPot, plant.value);
    playPlantSound(e.block.dimension, e.block.location, plant.sound);
});

// -------------------------------
//  Release plants on destruction
// -------------------------------
function releasePlants(e) {
    const states = (e.brokenBlockPermutation ?? e.explodedBlockPermutation).getAllStates();

    // Array of plant state values e.g. ["cactus", "dandelion"]
    const storedPlants = Object.entries(states)
        .filter(([state, value]) => state.startsWith("wiki:pot") && value !== "none")
        .map(([state, value]) => value);

    if (storedPlants.length === 0) return;

    // Centre loot in block
    const lootLocation = {
        x: e.block.location.x + 0.5,
        y: e.block.location.y + 0.5,
        z: e.block.location.z + 0.5,
    };

    // Create an item entity for every potted plant
    for (const plant of storedPlants) {
        const plantId = Object.keys(plants).find((key) => plants[key].value === plant);

        e.dimension.spawnItem(new ItemStack(plantId), lootLocation);
        playPlantSound(e.dimension, e.block.location, plants[plantId].sound);
    }
}

world.afterEvents.playerBreakBlock.subscribe((e) => {
    if (e.brokenBlockPermutation.type.id === "wiki:double_flower_pot") releasePlants(e);
});
world.afterEvents.blockExplode.subscribe((e) => {
    if (e.explodedBlockPermutation.type.id === "wiki:double_flower_pot") releasePlants(e);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

Importing Scripts ​

Don't forget to import your scripts into your pack's entry file!

BP/manifest.json
json
{
    "modules": [
        {
            "type": "script",
            "language": "javascript",
            "entry": "index.js", // Your defined entry file
            "uuid": "...",
            "version": "1.0.0"
        }
    ],
    "dependencies": [
        {
            "module_name": "@minecraft/server",
            "version": "1.15.0"
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BP/scripts/index.js
js
// Import your precise interaction scripts here...
import "./blocks/pigeonholes";
import "./blocks/double_flower_pot";
1
2
3

Download Example Pack ​

Template pack made according to this tutorial, adding the Pigeonholes and Double Flower Pot blocks into the Items tab.

Download MCADDON

If you require extra assistance with precise interaction, feel free to ask in the Bedrock Add-Ons Discord! Remember to include a link to this page in your question, as the classes provided here are not built into Minecraft.

Contributors

Edit Precise Interaction on GitHub

Text and image content on this page is licensed under the Creative Commons Attribution 4.0 International License

Code samples on this page are licensed under the MIT License

Bedrock Wiki by Bedrock OSS

"Minecraft" is a trademark of Mojang AB.

Bedrock OSS, Bedrock Wiki and bedrock.dev are not affiliated in any way with Microsoft or Mojang AB.

  • Privacy Policy
  • Join our Discord
  • Learn how to Contribute
  • Visit our Repository