import { world, system } from "@minecraft/server"; const BOAT = "silverlabs:tow_boat"; const TOW_PROP = "silverlabs:tow_target"; const SOFT_DIST = 3.0; const HARD_DIST = 8.0; const PULL_GAIN = 0.20; const TICK_RATE = 4; function dist(a, b) { const dx = a.x - b.x; const dy = a.y - b.y; const dz = a.z - b.z; return Math.sqrt(dx * dx + dy * dy + dz * dz); } function unitVec(from, to, len) { return { x: (to.x - from.x) / len, y: (to.y - from.y) / len, z: (to.z - from.z) / len }; } function getTowTarget(boat) { const id = boat.getDynamicProperty(TOW_PROP); return typeof id === "string" && id.length > 0 ? id : null; } function setTowTarget(boat, targetId) { boat.setDynamicProperty(TOW_PROP, targetId); } function clearTowTarget(boat) { boat.setDynamicProperty(TOW_PROP, ""); } function findLeashedBoat(player) { const overworld = world.getDimension(player.dimension.id); const nearby = overworld.getEntities({ type: BOAT, location: player.location, maxDistance: 16 }); for (const boat of nearby) { try { const holder = boat.getComponent("leashable")?.leashHolder; if (holder && holder.id === player.id) return boat; } catch { /* ignore — component may be unavailable mid-load */ } } return null; } world.beforeEvents.playerInteractWithEntity.subscribe((ev) => { const { player, target } = ev; if (target.typeId !== BOAT) return; if (!player.isSneaking) return; const inv = player.getComponent("inventory")?.container; const held = inv?.getItem(player.selectedSlotIndex); if (held) return; // empty hand only if (getTowTarget(target)) { ev.cancel = true; system.run(() => { clearTowTarget(target); player.sendMessage("§eTow link removed."); }); return; } const leader = findLeashedBoat(player); if (!leader || leader.id === target.id) return; ev.cancel = true; system.run(() => { setTowTarget(target, leader.id); try { leader.getComponent("leashable")?.unleash(); } catch { /* unleash may not exist on all versions; safe to ignore */ } player.sendMessage("§aTow link created — boat will follow the leader."); }); }); function tickFollowers() { for (const dim of ["overworld", "nether", "the_end"]) { let boats; try { boats = world.getDimension(dim).getEntities({ type: BOAT }); } catch { continue; } for (const boat of boats) { const targetId = getTowTarget(boat); if (!targetId) continue; const target = world.getEntity(targetId); if (!target || !target.isValid()) { clearTowTarget(boat); continue; } const d = dist(boat.location, target.location); if (d < SOFT_DIST) continue; if (d > HARD_DIST) { const u = unitVec(target.location, boat.location, d); try { boat.teleport({ x: target.location.x + u.x * (SOFT_DIST - 0.5), y: target.location.y, z: target.location.z + u.z * (SOFT_DIST - 0.5) }); } catch { /* chunk unloaded */ } continue; } const u = unitVec(boat.location, target.location, d); try { boat.applyImpulse({ x: u.x * PULL_GAIN, y: 0, z: u.z * PULL_GAIN }); } catch { /* boat may not support impulse if unloaded */ } } } } system.runInterval(tickFollowers, TICK_RATE); world.afterEvents.entityRemove.subscribe((ev) => { const removedId = ev.removedEntityId; for (const dim of ["overworld", "nether", "the_end"]) { let boats; try { boats = world.getDimension(dim).getEntities({ type: BOAT }); } catch { continue; } for (const boat of boats) { if (getTowTarget(boat) === removedId) clearTowTarget(boat); } } });