From b28e4d0610e9132a0798d8f0d859e4b627a3ca35 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Tue, 28 Apr 2026 01:07:27 +0100 Subject: [PATCH] fix(hemp): trader scope to held stack + chest loot guard + wild scatter bridge Three mid-session fixes to the hemp progression mechanics: - Trader buyback now operates on the held stack only. Earlier code walked the entire inventory to count and consume hemp items, so right-clicking with a stack of 64 buds while having more in other slots traded everything. Reproduced by user holding 64, ended up losing all stacks. Fixed: count = stack.amount, consume modifies only the selected hotbar slot. Other stacks of the same item stay intact. - Chest loot handler removed the `if (stack) return` guard. It was meant to skip placement events but blocked nearly every legitimate chest open (players almost always hold an item). Now every fresh chest open rolls; per-chest seeded set still prevents repeats. Added a chat ping when seeds drop so players notice. - Added a script-side wild hemp scatter on a 60s tick to bridge the worldgen gap. Bedrock features only fire on FRESH chunk generation, so legacy worlds where players have already explored never see the feature_rule patches. Scatter picks a random online overworld player every minute (40% chance), finds a grass surface within 32 blocks, places 2-4 mature hemp_crop in a small cluster. Runs in parallel with the worldgen feature_rule for new chunks. Co-Authored-By: Claude Opus 4.7 (1M context) --- hemp-addon/hemp_BP/scripts/main.js | 116 +++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 23 deletions(-) diff --git a/hemp-addon/hemp_BP/scripts/main.js b/hemp-addon/hemp_BP/scripts/main.js index 46415d7..2f96107 100644 --- a/hemp-addon/hemp_BP/scripts/main.js +++ b/hemp-addon/hemp_BP/scripts/main.js @@ -379,32 +379,35 @@ world.beforeEvents.playerInteractWithEntity.subscribe((event) => { if (!deal) return; event.cancel = true; // suppress the vanilla trade window for this interaction const player = event.player; + const held = stack.amount; 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.`); + 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 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 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 actualTrades = Math.floor(consumed / deal.perTrade); - if (actualTrades <= 0) return; - giveItem(player, "minecraft:emerald", actualTrades * deal.emeralds); + 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${actualTrades * deal.perTrade}§f for §a${actualTrades * deal.emeralds} emerald${actualTrades * deal.emeralds === 1 ? "" : "s"}§f.`); + player.sendMessage(`§a[Trader] §fTraded §e${consumed}§f for §a${trades * deal.emeralds} emerald${trades * deal.emeralds === 1 ? "" : "s"}§f.`); }); }); @@ -715,9 +718,9 @@ function saveSeededChests(set) { 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; + // 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; @@ -734,9 +737,76 @@ world.afterEvents.playerInteractWithBlock.subscribe((event) => { 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. + 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."); });