Files
minecraft-aiworld/hemp-addon/hemp_BP/scripts/main.js
SysAdmin eb82c8307b feat(hemp): wild patches in plains/forest + chest seed injection
Two new ways to find hemp seeds without already having any:

1. Worldgen: minecraft:scatter_feature spawns 1-3 mature
   silverlabs:hemp_crop blocks on grass/dirt in plains/forest/birch/
   flower_forest biomes (~14% scatter chance per chunk surface pass).
2. Chest injection: 8% chance per chest first-open to plant 1-3 seeds
   in a random empty slot. Tracked per-chest via world dynamic property
   (rolling cap of 500 entries) so each chest only contributes once.

Bedrock has no loot-table merge mechanism so we can't add seeds to
vanilla village chests without losing their vanilla loot — script
injection sidesteps that and stays version-independent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:14:54 +01:00

743 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { world, system, ItemStack, BlockPermutation } from "@minecraft/server";
const CROP = "silverlabs:hemp_crop";
const SUN_LAMP = "silverlabs:sun_lamp";
const SEEDS = "silverlabs:hemp_seeds";
const BUD = "silverlabs:hemp_bud";
const TINCTURE = "silverlabs:hemp_tincture";
const BROWNIE = "silverlabs:hemp_brownie";
const AGE = "silverlabs:hemp_age";
const TOP = "silverlabs:hemp_top";
const GROWTH_INTERVAL_TICKS = 100; // 5s between growth ticks
const RABBIT_INTERVAL_TICKS = 100; // 5s between rabbit checks
const SCAN_RADIUS = 5; // ±5 horizontal — dense scan, every block looked at
const SCAN_VERT = 2; // ±2 vertical
const SUN_LAMP_RANGE = 4; // blocks searched for a lamp around indoor crop
const GROWTH_CHANCE_OUTDOOR = 0.017; // per 5s tick — ~20 min for 0→4 (prime)
const GROWTH_CHANCE_INDOOR = 0.011; // per 5s tick — ~30 min for 0→4 (sun lamp required)
const OVERRIPE_CHANCE_MULT = 0.33; // age 4→5 transition is 3x slower so prime lingers
function rand(n) { return Math.floor(Math.random() * n); }
function chance(p) { return Math.random() < p; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function getInv(player) {
return player.getComponent("minecraft:inventory")?.container ?? null;
}
function consumeOneOfType(player, typeId) {
const inv = getInv(player);
if (!inv) return false;
const sel = player.selectedSlotIndex;
const cur = inv.getItem(sel);
if (cur && cur.typeId === typeId) {
if (cur.amount > 1) { cur.amount -= 1; inv.setItem(sel, cur); }
else inv.setItem(sel, undefined);
return true;
}
for (let i = 0; i < inv.size; i++) {
const it = inv.getItem(i);
if (it && it.typeId === typeId) {
if (it.amount > 1) { it.amount -= 1; inv.setItem(i, it); }
else inv.setItem(i, undefined);
return true;
}
}
return false;
}
function giveItem(player, typeId, count = 1) {
const inv = getInv(player);
if (!inv) return false;
try {
inv.addItem(new ItemStack(typeId, count));
return true;
} catch (_) {
return false;
}
}
// Block ids that don't block sky access — the plant grows around these like
// a vanilla tree pushes through leaves. Anything else above stops growth.
function isSkyTransparent(typeId) {
if (!typeId) return true;
if (typeId === CROP) return true; // ignore our own top half
if (typeId === "minecraft:air") return true;
// Glass / panes / bars
if (typeId.includes("glass")) return true;
if (typeId === "minecraft:iron_bars") return true;
// Leaves and saplings (canopy-style obstructions)
if (typeId.includes("leaves")) return true;
if (typeId.includes("sapling")) return true;
// Other plants / vines / hanging stuff
if (typeId === "minecraft:vine" || typeId === "minecraft:weeping_vines"
|| typeId === "minecraft:twisting_vines") return true;
return false;
}
function isAirAbove(dim, loc) {
// Walk up from y+1; sky-transparent blocks don't count as obstructions.
for (let dy = 1; dy < 64; dy++) {
let b;
try { b = dim.getBlock({ x: loc.x, y: loc.y + dy, z: loc.z }); } catch (_) { return true; }
if (!b) return true;
if (b.isAir || b.isLiquid) continue;
if (isSkyTransparent(b.typeId)) continue;
return false;
}
return true;
}
function findSunLampNear(dim, loc) {
const r = SUN_LAMP_RANGE;
for (let dy = -1; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
for (let dz = -r; dz <= r; dz++) {
let b;
try { b = dim.getBlock({ x: loc.x + dx, y: loc.y + dy, z: loc.z + dz }); } catch (_) { continue; }
if (b && b.typeId === SUN_LAMP) {
try {
if (b.permutation.getState("silverlabs:powered") === true) return true;
} catch (_) {}
}
}
}
}
return false;
}
function setAge(block, newAge, outdoor) {
try {
const perm = BlockPermutation.resolve(CROP, {
[AGE]: clamp(newAge, 0, 5),
[TOP]: false,
});
block.setPermutation(perm);
if (outdoor && newAge >= 3) {
// Place top half above for the visual "tall outdoor crop"
const above = block.dimension.getBlock({ x: block.location.x, y: block.location.y + 1, z: block.location.z });
if (above && above.isAir) {
try {
const topPerm = BlockPermutation.resolve(CROP, { [AGE]: clamp(newAge, 0, 5), [TOP]: true });
above.setPermutation(topPerm);
} catch (_) {}
}
}
} catch (_) {}
}
function clearTopAbove(block) {
const above = block.dimension.getBlock({ x: block.location.x, y: block.location.y + 1, z: block.location.z });
if (above && above.typeId === CROP) {
try { above.setType("minecraft:air"); } catch (_) {}
}
}
// --- Growth: dense scan of an 11×11×5 box around each player ---
// Every hemp_crop base in range gets a roll on every tick — no random sampling.
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const dim = player.dimension;
const px = Math.floor(player.location.x);
const py = Math.floor(player.location.y);
const pz = Math.floor(player.location.z);
for (let dy = -SCAN_VERT; dy <= SCAN_VERT; dy++) {
for (let dx = -SCAN_RADIUS; dx <= SCAN_RADIUS; dx++) {
for (let dz = -SCAN_RADIUS; dz <= SCAN_RADIUS; dz++) {
let b;
try { b = dim.getBlock({ x: px + dx, y: py + dy, z: pz + dz }); } catch (_) { continue; }
if (!b || b.typeId !== CROP) continue;
if (b.permutation.getState(TOP) === true) continue; // only base ticks
const age = b.permutation.getState(AGE) ?? 0;
if (age >= 5) continue; // overripe stops
const outdoor = isAirAbove(dim, b.location);
let baseChance = 0;
if (outdoor) baseChance = GROWTH_CHANCE_OUTDOOR;
else if (findSunLampNear(dim, b.location)) baseChance = GROWTH_CHANCE_INDOOR;
else continue;
// Prime (age 4) lingers — slow the transition into overripe
const c = age === 4 ? baseChance * OVERRIPE_CHANCE_MULT : baseChance;
if (chance(c)) setAge(b, age + 1, outdoor);
}
}
}
}
}, GROWTH_INTERVAL_TICKS);
// --- Redstone power: scan known sources adjacent to a sun_lamp ---
const REDSTONE_FACES = [
[1, 0, 0], [-1, 0, 0],
[0, 1, 0], [0, -1, 0],
[0, 0, 1], [0, 0, -1],
];
function isPowerSource(b) {
if (!b) return false;
const t = b.typeId;
if (t === "minecraft:redstone_block") return true;
if (t === "minecraft:lit_redstone_torch" || t === "minecraft:redstone_torch") {
// torch lit state
try { return b.permutation.getState("toggle_bit") !== false; } catch (_) {}
return t === "minecraft:lit_redstone_torch";
}
if (t === "minecraft:powered_repeater") return true;
if (t === "minecraft:powered_comparator") return true;
if (t === "minecraft:lever") {
try { return b.permutation.getState("open_bit") === true; } catch (_) { return false; }
}
if (t.endsWith("_button") || t.includes(":wooden_button") || t.includes(":stone_button")) {
try { return b.permutation.getState("button_pressed_bit") === true; } catch (_) { return false; }
}
if (t === "minecraft:redstone_wire") {
try {
const p = b.permutation.getState("redstone_signal") ?? 0;
return p > 0;
} catch (_) { return false; }
}
if (t === "minecraft:daylight_detector_inverted") return true;
return false;
}
function lampShouldBePowered(lamp) {
// Try the script API redstone power first (newer servers expose it)
try {
const p = lamp.getRedstonePower();
if (typeof p === "number" && p > 0) return true;
} catch (_) {}
const dim = lamp.dimension;
for (const [dx, dy, dz] of REDSTONE_FACES) {
let n;
try { n = dim.getBlock({ x: lamp.location.x + dx, y: lamp.location.y + dy, z: lamp.location.z + dz }); } catch (_) { continue; }
if (isPowerSource(n)) return true;
}
return false;
}
const LAMP_SCAN_INTERVAL_TICKS = 20; // 1s
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const dim = player.dimension;
const px = Math.floor(player.location.x);
const py = Math.floor(player.location.y);
const pz = Math.floor(player.location.z);
for (let dy = -SCAN_VERT; dy <= SCAN_VERT; dy++) {
for (let dx = -SCAN_RADIUS; dx <= SCAN_RADIUS; dx++) {
for (let dz = -SCAN_RADIUS; dz <= SCAN_RADIUS; dz++) {
let b;
try { b = dim.getBlock({ x: px + dx, y: py + dy, z: pz + dz }); } catch (_) { continue; }
if (!b || b.typeId !== SUN_LAMP) continue;
let cur = false;
try { cur = b.permutation.getState("silverlabs:powered") === true; } catch (_) {}
const want = lampShouldBePowered(b);
if (cur !== want) {
try {
const perm = BlockPermutation.resolve(SUN_LAMP, { "silverlabs:powered": want });
b.setPermutation(perm);
} catch (_) {}
}
}
}
}
}
}, LAMP_SCAN_INTERVAL_TICKS);
// --- Rabbit threat: damage outdoor hemp ---
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const dim = player.dimension;
let rabbits;
try {
rabbits = dim.getEntities({ type: "minecraft:rabbit", location: player.location, maxDistance: 48 });
} catch (_) { continue; }
for (const rabbit of rabbits) {
// For each rabbit, look at a small box around its feet for hemp_crop.
const rx = Math.floor(rabbit.location.x);
const ry = Math.floor(rabbit.location.y);
const rz = Math.floor(rabbit.location.z);
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
let b;
try { b = dim.getBlock({ x: rx + dx, y: ry, z: rz + dz }); } catch (_) { continue; }
if (!b || b.typeId !== CROP) continue;
const isTop = b.permutation.getState(TOP) === true;
if (isTop) continue;
if (chance(0.35)) {
const age = b.permutation.getState(AGE) ?? 0;
if (age <= 0) {
clearTopAbove(b);
try { b.setType("minecraft:air"); } catch (_) {}
} else {
clearTopAbove(b);
setAge(b, age - 1, isAirAbove(dim, b.location));
}
try { dim.runCommand(`particle minecraft:crit_particle ${rx + dx + 0.5} ${ry + 0.5} ${rz + dz + 0.5}`); } catch (_) {}
}
}
}
}
}
}, RABBIT_INTERVAL_TICKS);
// --- Pillager raid threat: illagers carrying a banner steal hemp during raids ---
// Gate on either an active raid (detected via a nearby ominous_banner) or a
// pillager_captain so isolated tower pillagers don't constantly cull farms
// in render distance. When triggered, drops the crop's age by 1 (or removes
// it if age 0) and spawns 1 hemp_bud at the illager's feet so the player
// can recover the loot by killing the raider.
const RAID_INTERVAL_TICKS = 100;
const ILLAGER_TYPES = new Set([
"minecraft:pillager",
"minecraft:vindicator",
"minecraft:evocation_illager",
]);
const ILLAGER_HEMP_STEAL_CHANCE = 0.30;
function illagerIsRaiding(dim, ill) {
// Cheapest: check if the illager is wearing/carrying an ominous_banner
// (raid captain marker). Also accept any nearby ominous_banner block within
// 8 blocks as evidence of an ongoing raid the illager is participating in.
const equip = (() => { try { return ill.getComponent("minecraft:equippable"); } catch (_) { return null; } })();
if (equip) {
try {
const head = equip.getEquipmentSlot("Head")?.getItem();
if (head && head.typeId === "minecraft:ominous_banner") return true;
} catch (_) {}
}
const ix = Math.floor(ill.location.x);
const iy = Math.floor(ill.location.y);
const iz = Math.floor(ill.location.z);
for (let dx = -8; dx <= 8; dx += 4) {
for (let dz = -8; dz <= 8; dz += 4) {
let b;
try { b = dim.getBlock({ x: ix + dx, y: iy, z: iz + dz }); } catch (_) { continue; }
if (b && (b.typeId === "minecraft:standing_banner" || b.typeId === "minecraft:wall_banner")) {
// Banners can be ominous via their NBT; we can't read it, so any banner
// within 8 blocks of an illager is treated as raid evidence. Still rare.
return true;
}
}
}
return false;
}
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
const dim = player.dimension;
let illagers;
try {
illagers = dim.getEntities({ families: [], location: player.location, maxDistance: 48 });
} catch (_) { continue; }
for (const ill of illagers) {
if (!ILLAGER_TYPES.has(ill.typeId)) continue;
if (!illagerIsRaiding(dim, ill)) continue;
const ix = Math.floor(ill.location.x);
const iy = Math.floor(ill.location.y);
const iz = Math.floor(ill.location.z);
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
let b;
try { b = dim.getBlock({ x: ix + dx, y: iy, z: iz + dz }); } catch (_) { continue; }
if (!b || b.typeId !== CROP) continue;
if (b.permutation.getState(TOP) === true) continue;
if (!chance(ILLAGER_HEMP_STEAL_CHANCE)) continue;
const age = b.permutation.getState(AGE) ?? 0;
const outdoor = isAirAbove(dim, b.location);
if (age <= 0) {
clearTopAbove(b);
try { b.setType("minecraft:air"); } catch (_) {}
} else {
clearTopAbove(b);
setAge(b, age - 1, outdoor);
}
// Drop a hemp_bud at the illager's feet — recoverable when killed.
spawnDrop(dim, ill.location, BUD, 1);
try { dim.runCommand(`particle minecraft:villager_angry ${ix + 0.5} ${iy + 1.0} ${iz + 0.5}`); } catch (_) {}
}
}
}
}
}, RAID_INTERVAL_TICKS);
// --- Wandering trader buyback: hand off hemp products for emeralds ---
// Right-click a wandering_trader while holding hemp_bud / hemp_tincture /
// hemp_seeds. Avoids overriding vanilla trade tables (which would pin us to
// one Bedrock version) by acting as an off-menu interaction. Trader animates
// a "happy villager" particle and the player's inventory swaps the items
// for emeralds at the rates below.
const TRADER_BUYBACK = {
[BUD]: { perTrade: 2, emeralds: 1 },
[TINCTURE]: { perTrade: 1, emeralds: 3 },
[SEEDS]: { perTrade: 8, emeralds: 1 },
};
world.beforeEvents.playerInteractWithEntity.subscribe((event) => {
const target = event.target;
if (!target || target.typeId !== "minecraft:wandering_trader") return;
const stack = event.itemStack;
if (!stack) return;
const deal = TRADER_BUYBACK[stack.typeId];
if (!deal) return;
event.cancel = true; // suppress the vanilla trade window for this interaction
const player = event.player;
system.run(() => {
// Count what the player has of this item across the inventory
const inv = getInv(player);
if (!inv) return;
let have = 0;
for (let i = 0; i < inv.size; i++) {
const it = inv.getItem(i);
if (it && it.typeId === stack.typeId) have += it.amount;
}
if (have < deal.perTrade) {
player.sendMessage(`§7[Trader] Brings me at least §f${deal.perTrade}§7 of those and I'll pay you in emeralds.`);
return;
}
const trades = Math.floor(have / deal.perTrade);
let consumed = 0;
for (let n = 0; n < trades * deal.perTrade; n++) {
if (!consumeOneOfType(player, stack.typeId)) break;
consumed++;
}
const actualTrades = Math.floor(consumed / deal.perTrade);
if (actualTrades <= 0) return;
giveItem(player, "minecraft:emerald", actualTrades * deal.emeralds);
const loc = target.location;
try { target.dimension.runCommand(`particle minecraft:villager_happy ${loc.x} ${loc.y + 1.5} ${loc.z}`); } catch (_) {}
try { target.dimension.runCommand(`playsound mob.villager.yes @a ${loc.x} ${loc.y} ${loc.z} 0.8 1.1`); } catch (_) {}
player.sendMessage(`§a[Trader] §fTraded §e${actualTrades * deal.perTrade}§f for §a${actualTrades * deal.emeralds} emerald${actualTrades * deal.emeralds === 1 ? "" : "s"}§f.`);
});
});
// --- Outdoor detection on placement ---
world.afterEvents.playerPlaceBlock.subscribe((event) => {
const block = event.block;
if (!block || block.typeId !== CROP) return;
// newly-placed crop is age 0 by default — we don't need to set anything,
// outdoor status is computed live during ticks.
});
// --- Cleanup: breaking either half of a tall plant removes the other ---
// Also: rare hemp_seeds drop from breaking vanilla grass / tall_grass /
// short_grass — gives players a way to bootstrap into hemp without
// already having seeds. Mirrors how vanilla wheat seeds work.
const GRASS_BLOCK_IDS = new Set([
"minecraft:short_grass",
"minecraft:tall_grass",
"minecraft:fern",
"minecraft:large_fern",
// Pre-1.21 alias kept for safety on mixed-version worlds
"minecraft:grass",
]);
const GRASS_HEMP_SEED_CHANCE = 0.04; // ~1 in 25 grass tufts
world.afterEvents.playerBreakBlock.subscribe((event) => {
const brokenType = event.brokenBlockPermutation?.type?.id;
// Hemp_crop tall-plant cleanup
if (brokenType === CROP) {
const block = event.block;
const dim = block.dimension;
const wasTop = event.brokenBlockPermutation.getState(TOP) === true;
const dy = wasTop ? -1 : 1;
let neighbor;
try { neighbor = dim.getBlock({ x: block.location.x, y: block.location.y + dy, z: block.location.z }); } catch (_) { return; }
if (!neighbor || neighbor.typeId !== CROP) return;
const neighborIsTop = neighbor.permutation.getState(TOP) === true;
if (wasTop === neighborIsTop) return;
try { neighbor.setType("minecraft:air"); } catch (_) {}
return;
}
// Hemp seed bootstrap from grass blocks
if (brokenType && GRASS_BLOCK_IDS.has(brokenType)) {
if (!chance(GRASS_HEMP_SEED_CHANCE)) return;
const block = event.block;
const loc = block.location;
spawnDrop(block.dimension, loc, SEEDS, 1);
}
});
// Per-player debounce so a single click that fires the event twice
// (a known Bedrock quirk for some interact paths) only does work once.
const recentInteract = new Map(); // key: playerId|x|y|z -> system.currentTick
function consumeInteractToken(player, block) {
const key = `${player.id}|${block.location.x}|${block.location.y}|${block.location.z}`;
const last = recentInteract.get(key) ?? -999;
const now = system.currentTick;
if (now - last < 4) return false; // <200ms — treat as duplicate
recentInteract.set(key, now);
// Lazy cleanup: prune any entries older than ~4s on each successful claim
if (recentInteract.size > 64) {
for (const [k, t] of recentInteract) {
if (now - t > 80) recentInteract.delete(k);
}
}
return true;
}
// --- Bonemeal on hemp_crop bumps growth ---
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
const stack = event.itemStack;
const player = event.player;
if (!block || block.typeId !== CROP) return;
// Sheers harvest path
if (stack && stack.typeId === "minecraft:shears") {
event.cancel = true;
if (!consumeInteractToken(player, block)) return;
system.run(() => harvestWithShears(player, block, stack));
return;
}
// Bone meal path
if (stack && stack.typeId === "minecraft:bone_meal") {
event.cancel = true;
if (!consumeInteractToken(player, block)) return;
system.run(() => {
// If the player clicked the top half, redirect to the base
let target = block;
if (block.permutation.getState(TOP) === true) {
const below = block.dimension.getBlock({ x: block.location.x, y: block.location.y - 1, z: block.location.z });
if (below && below.typeId === CROP) target = below;
else return;
}
const age = target.permutation.getState(AGE) ?? 0;
// Cap bone meal at age 4 (prime) — overripe (5) only via neglect
if (age >= 4) {
player.sendMessage("§e[Hemp] Already mature — harvest with shears.");
return;
}
// Always consume the bone meal, but only 50% chance to advance
consumeOneOfType(player, "minecraft:bone_meal");
const loc = target.location;
try { target.dimension.runCommand(`particle minecraft:crop_growth_emitter ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5}`); } catch (_) {}
if (!chance(0.5)) return;
const outdoor = isAirAbove(target.dimension, loc);
setAge(target, age + 1, outdoor);
try { target.dimension.runCommand(`playsound random.fizz @a ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5} 0.6 1.6`); } catch (_) {}
});
return;
}
});
function harvestWithShears(player, block, shearsStack) {
if (!block || block.typeId !== CROP) return; // already harvested / replaced
// Read state on the BASE block (not top)
let base = block;
if (block.permutation.getState(TOP) === true) {
const below = block.dimension.getBlock({ x: block.location.x, y: block.location.y - 1, z: block.location.z });
if (below && below.typeId === CROP) base = below;
}
const age = base.permutation.getState(AGE) ?? 0;
const dim = base.dimension;
const loc = base.location;
const outdoor = isAirAbove(dim, loc);
// Too early — leave the plant alone, don't damage shears
if (age <= 1) {
player.sendMessage("§7[Hemp] Too early — leave it to grow.");
return;
}
const yields = computeYield(age, outdoor);
if (yields.msg) player.sendMessage(yields.msg);
if (yields.bud > 0) spawnDrop(dim, loc, BUD, yields.bud);
if (yields.seeds > 0) spawnDrop(dim, loc, SEEDS, yields.seeds);
// Fully remove the plant — player replants from seeds
clearTopAbove(base);
try { base.setType("minecraft:air"); } catch (_) {}
damageShears(player, shearsStack);
try { dim.runCommand(`playsound mob.sheep.shear @a ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5}`); } catch (_) {}
}
function computeYield(age, outdoor) {
// Outdoor gets a bud bonus; indoor (sun-lamp grown) is baseline.
const out = outdoor ? 1 : 0;
switch (age) {
case 2: return { bud: 1, seeds: 1 + out };
case 3: return { bud: 2 + out, seeds: 1 + out };
case 4: return { bud: 3 + out * 2, seeds: 1 + out, msg: "§a[Hemp] Prime harvest." }; // peak
case 5: return { bud: rand(3), seeds: 3 + rand(3), msg: "§7[Hemp] Overripe — mostly seeds now." };
default: return { bud: 0, seeds: 0 };
}
}
function spawnDrop(dim, loc, typeId, count) {
try {
const it = new ItemStack(typeId, count);
dim.spawnItem(it, { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
} catch (_) {}
}
function damageShears(player, shearsStack) {
try {
const equippable = player.getComponent("minecraft:equippable");
if (!equippable) return;
const slot = equippable.getEquipmentSlot("Mainhand");
if (!slot) return;
const item = slot.getItem();
if (!item || item.typeId !== "minecraft:shears") return;
const dur = item.getComponent("minecraft:durability");
if (!dur) return;
const damage = dur.damage + 1;
if (damage >= dur.maxDurability) {
slot.setItem(undefined);
try { player.dimension.runCommand(`playsound random.break @a ${player.location.x} ${player.location.y} ${player.location.z}`); } catch (_) {}
} else {
dur.damage = damage;
slot.setItem(item);
}
} catch (_) {}
}
// --- Cauldron tincture brewing: hemp_bud + water cauldron + glass bottle => tincture ---
world.afterEvents.itemUse.subscribe((event) => {
const player = event.source;
const stack = event.itemStack;
if (!stack || stack.typeId !== BUD) return;
system.run(() => {
const target = player.getBlockFromViewDirection({ maxDistance: 6 });
const block = target?.block;
if (!block) return;
const isCauldron = block.typeId === "minecraft:cauldron" || block.typeId === "minecraft:water_cauldron";
if (!isCauldron) return;
// Need at least 3 buds total in inventory and 1 glass bottle
const inv = getInv(player);
if (!inv) return;
let budCount = 0, bottleCount = 0;
for (let i = 0; i < inv.size; i++) {
const it = inv.getItem(i);
if (!it) continue;
if (it.typeId === BUD) budCount += it.amount;
else if (it.typeId === "minecraft:glass_bottle") bottleCount += it.amount;
}
if (budCount < 3) {
player.sendMessage("§c[Hemp] Need 3 hemp buds to brew tincture.");
return;
}
if (bottleCount < 1) {
player.sendMessage("§c[Hemp] Need an empty glass bottle.");
return;
}
// Check water level (water cauldron has fill_level state 1-3; vanilla cauldron = empty)
let level = 0;
try { level = block.permutation.getState("fill_level") ?? 0; } catch (_) {}
if (block.typeId === "minecraft:cauldron") {
player.sendMessage("§c[Hemp] Cauldron must contain water.");
return;
}
if (level <= 0) {
player.sendMessage("§c[Hemp] Cauldron is empty.");
return;
}
// Consume 3 buds, 1 bottle
for (let i = 0; i < 3; i++) consumeOneOfType(player, BUD);
consumeOneOfType(player, "minecraft:glass_bottle");
// Decrement water level
try {
const newLevel = level - 1;
if (newLevel <= 0) {
block.setType("minecraft:cauldron");
} else {
const perm = BlockPermutation.resolve("minecraft:water_cauldron", { fill_level: newLevel });
block.setPermutation(perm);
}
} catch (_) {}
// Give tincture
giveItem(player, TINCTURE, 1);
const loc = block.location;
try { block.dimension.runCommand(`particle minecraft:water_splash_particle_manual ${loc.x + 0.5} ${loc.y + 1.0} ${loc.z + 0.5}`); } catch (_) {}
try { block.dimension.runCommand(`playsound bucket.fill_water @a ${loc.x + 0.5} ${loc.y + 0.5} ${loc.z + 0.5}`); } catch (_) {}
player.sendMessage("§a[Hemp] Hemp tincture brewed.");
});
});
// --- Consumption effects ---
world.afterEvents.itemCompleteUse.subscribe((event) => {
const player = event.source;
const stack = event.itemStack;
if (!stack) return;
if (stack.typeId === TINCTURE) {
try {
player.addEffect("regeneration", 100, { amplifier: 1, showParticles: true });
player.addEffect("slowness", 200, { amplifier: 0, showParticles: true });
} catch (_) {}
player.sendMessage("§a[Hemp] You feel a warm, calming wave.");
} else if (stack.typeId === BROWNIE) {
try {
player.addEffect("regeneration", 200, { amplifier: 0, showParticles: true });
player.addEffect("slowness", 400, { amplifier: 0, showParticles: true });
} catch (_) {}
player.sendMessage("§a[Hemp] Mmm. You feel relaxed.");
}
});
// --- Chest loot injection: first-time-open chests sometimes contain seeds ---
// Bedrock has no merge-loot-table mechanism, so we hook chest opens and
// deposit hemp_seeds with low probability into a random empty slot.
// Tracking is per-chest via a world dynamic property holding a JSON list
// of "dim:x:y:z" keys; pruned to a rolling cap to bound storage.
const CHEST_TYPES = new Set(["minecraft:chest", "minecraft:trapped_chest"]);
const CHEST_SEED_CHANCE = 0.08;
const SEEDED_PROP = "hemp_seeded_chests_v1";
const SEEDED_CAP = 500;
function chestKey(block) {
const l = block.location;
return `${block.dimension.id}:${l.x}:${l.y}:${l.z}`;
}
function loadSeededChests() {
try {
const raw = world.getDynamicProperty(SEEDED_PROP);
return new Set(raw ? JSON.parse(raw) : []);
} catch (_) { return new Set(); }
}
function saveSeededChests(set) {
// Prune oldest if over cap (Set preserves insertion order in JS)
if (set.size > SEEDED_CAP) {
const arr = Array.from(set).slice(set.size - SEEDED_CAP);
set = new Set(arr);
}
try {
world.setDynamicProperty(SEEDED_PROP, JSON.stringify(Array.from(set)));
} catch (_) {}
}
world.afterEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (!block || !CHEST_TYPES.has(block.typeId)) return;
// Only on the open hand (not while holding an item, to avoid placing-into events firing twice)
const stack = event.itemStack;
if (stack) return;
const key = chestKey(block);
const seeded = loadSeededChests();
if (seeded.has(key)) return;
seeded.add(key);
saveSeededChests(seeded);
if (!chance(CHEST_SEED_CHANCE)) return;
// Try to insert into a random empty slot
let inv;
try { inv = block.getComponent("minecraft:inventory")?.container; } catch (_) { return; }
if (!inv) return;
const empties = [];
for (let i = 0; i < inv.size; i++) if (!inv.getItem(i)) empties.push(i);
if (empties.length === 0) return;
const slot = empties[rand(empties.length)];
const count = 1 + rand(3); // 1-3 seeds
try { inv.setItem(slot, new ItemStack(SEEDS, count)); } catch (_) {}
// No chat ping — let the player discover the seeds organically.
});
system.run(() => {
world.sendMessage("§a[Hemp] §7Hemp pack loaded.");
});