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; const held = stack.amount; system.run(() => { if (held < deal.perTrade) { player.sendMessage(`§7[Trader] §fBring me at least §a${deal.perTrade}§f of those and I'll pay in emeralds.`); return; } const inv = getInv(player); if (!inv) return; // Trade scope is the held stack only — never walk the rest of the inventory const sel = player.selectedSlotIndex; const cur = inv.getItem(sel); if (!cur || cur.typeId !== stack.typeId) { // Player swapped item out between the click and this run() — abort safely return; } const trades = Math.floor(held / deal.perTrade); const consumed = trades * deal.perTrade; const remaining = held - consumed; if (remaining > 0) { cur.amount = remaining; inv.setItem(sel, cur); } else { inv.setItem(sel, undefined); } giveItem(player, "minecraft:emerald", trades * 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${consumed}§f for §a${trades * deal.emeralds} emerald${trades * 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; // Run on every chest interaction; the per-chest seeded set prevents repeats. // Earlier `if (stack) return` was over-aggressive — players almost always // hold an item when opening chests, so it suppressed nearly all opens. 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 (_) {} try { event.player.sendMessage("§a[Hemp] §7You spot some §dhemp seeds§7 tucked inside the chest."); } catch (_) {} }); // --- Wild hemp scatter: bridge until worldgen patches show up --- // Bedrock features only fire on FRESH chunk generation. Pre-existing // chunks (where the player has already been) won't have patches even // after we ship the feature_rule. This periodic scatter seeds mature // hemp_crop on suitable grass blocks in the player's vicinity so the // mechanic is discoverable in legacy worlds. Cap one patch per minute // per player to avoid flooding. const SCATTER_INTERVAL_TICKS = 1200; // 60 s const SCATTER_CHANCE_PER_TICK = 0.4; // 40% per minute → ~1 patch every 2.5 min const SCATTER_RADIUS = 32; const SCATTER_PATCH_SIZE = [2, 4]; // 2-4 plants per patch function tryScatterHempNearPlayer(player) { const dim = player.dimension; if (dim.id !== "minecraft:overworld") return; const px = Math.floor(player.location.x); const py = Math.floor(player.location.y); const pz = Math.floor(player.location.z); // Sample up to 12 candidate locations; pick the first viable grass block for (let attempt = 0; attempt < 12; attempt++) { const dx = rand(SCATTER_RADIUS * 2 + 1) - SCATTER_RADIUS; const dz = rand(SCATTER_RADIUS * 2 + 1) - SCATTER_RADIUS; // Search vertically: top-down within ±6 of player y for the surface let surface = null; for (let dy = 6; dy >= -6; dy--) { let b; try { b = dim.getBlock({ x: px + dx, y: py + dy, z: pz + dz }); } catch (_) { continue; } if (!b) continue; if (b.typeId === "minecraft:grass_block") { // Check above is air (and skylit-ish — not a cave with grass_path glitches) let above; try { above = dim.getBlock({ x: px + dx, y: py + dy + 1, z: pz + dz }); } catch (_) { continue; } if (above && above.isAir) { surface = { x: px + dx, y: py + dy + 1, z: pz + dz }; break; } } } if (!surface) continue; // Place 2-4 mature hemp_crop in a small cluster around surface const count = SCATTER_PATCH_SIZE[0] + rand(SCATTER_PATCH_SIZE[1] - SCATTER_PATCH_SIZE[0] + 1); let placed = 0; for (let i = 0; i < count * 3 && placed < count; i++) { const ox = rand(5) - 2; const oz = rand(5) - 2; const tx = surface.x + ox, ty = surface.y, tz = surface.z + oz; let target, ground; try { target = dim.getBlock({ x: tx, y: ty, z: tz }); ground = dim.getBlock({ x: tx, y: ty - 1, z: tz }); } catch (_) { continue; } if (!target || !ground) continue; if (!target.isAir) continue; if (ground.typeId !== "minecraft:grass_block" && ground.typeId !== "minecraft:dirt") continue; try { const perm = BlockPermutation.resolve(CROP, { [AGE]: 4, [TOP]: false }); target.setPermutation(perm); placed++; } catch (_) {} } return; // one patch per tick } } system.runInterval(() => { const players = world.getAllPlayers(); if (players.length === 0) return; const player = players[rand(players.length)]; if (!chance(SCATTER_CHANCE_PER_TICK)) return; tryScatterHempNearPlayer(player); }, SCATTER_INTERVAL_TICKS); system.run(() => { world.sendMessage("§a[Hemp] §7Hemp pack loaded."); });