@@ -0,0 +1,686 @@
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." ) ;
}
} ) ;
system . run ( ( ) => {
world . sendMessage ( "§a[Hemp] §7Hemp pack loaded." ) ;
} ) ;