feat(addons): hemp plant, wild cherry tree, naturalist-lite
New addons: - hemp-addon: silverlabs:hemp_crop (5 ages, indoor sun-lamp grown vs outdoor sky-lit), shears harvest, cauldron tincture, brownie food, bonemeal, sun-lamp redstone-lit block (light_dampening: 0 so crops beneath stay lit), grass-seed bootstrap, wandering-trader buyback, pillager raid stealing. - trees-features-addon: ods_orch wild cherry tree — log/leaves/planks/ stripped/sapling/fruit blocks with seasonal fruit states, structure- spawn worldgen. - naturalist-lite-addon: 13-mob subset of Naturalist (deer, fox, owl, skunk, snake, hedgehog, red panda, capybara, elephant, kangaroo, moose, tiger, firefly), trimmed for Switch joinability. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
686
hemp-addon/hemp_BP/scripts/main.js
Normal file
686
hemp-addon/hemp_BP/scripts/main.js
Normal file
@@ -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.");
|
||||
});
|
||||
Reference in New Issue
Block a user