feat: A-frame tent + portal walk-through field + texture polish
All checks were successful
Deploy Addons / deploy (push) Successful in 14s

- camping: replace cube tent with A-frame slope panels (tent_panel_l/r) +
  cardinal_direction permutations; vote-skip sleep that mixes
  player.isSleeping bed sleepers with tent occupants and respects the
  playersSleepingPercentage gamerule; new weathered-canvas texture.
- lobby: walk-through silverlabs:portal_field block (no collision,
  translucent swirl, cross-plane geo) auto-placed above each portal frame;
  invisible silverlabs:portal_label entity floats above each portal with
  the destination world name; transfer detection now scans down through
  the field to find the destination frame.
- postal: regenerate post_office and mailbox block textures so they fill
  the full block face (brick + POST plaque, full red panel with slot/latch
  /flag/rivets) instead of small sprites floating on transparent.
- dynamite + tow-boat: ship the addons (volumes wired into all four
  worlds; enabled_packs registers them into Mya's world).
- art: build-textures.py extended; build-art-catalog.py added to project.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 23:17:31 +01:00
parent fce15ac801
commit 3e08a59972
58 changed files with 1815 additions and 67 deletions

View File

@@ -0,0 +1,95 @@
{
"format_version": "1.21.0",
"minecraft:entity": {
"description": {
"identifier": "silverlabs:tow_boat",
"is_spawnable": false,
"is_summonable": true,
"is_experimental": false
},
"components": {
"minecraft:type_family": {
"family": ["tow_boat", "boat"]
},
"minecraft:health": {
"value": 20,
"max": 20
},
"minecraft:collision_box": {
"width": 1.4,
"height": 0.55
},
"minecraft:physics": {
"has_gravity": true,
"has_collision": true
},
"minecraft:pushable": {
"is_pushable": true,
"is_pushable_by_piston": false
},
"minecraft:persistent": {},
"minecraft:nameable": {},
"minecraft:movement": {
"value": 0.18
},
"minecraft:movement.basic": {},
"minecraft:navigation.float": {
"can_path_over_water": true,
"can_breach": false
},
"minecraft:underwater_movement": {
"value": 0.0
},
"minecraft:can_climb": {},
"minecraft:rideable": {
"seat_count": 1,
"family_types": ["player"],
"interact_text": "action.interact.mount",
"pull_in_entities": false,
"seats": [
{
"position": [0, 0.4, 0.0],
"lock_rider_rotation": 90,
"min_rider_count": 0,
"max_rider_count": 1
}
]
},
"minecraft:input_ground_controlled": {},
"minecraft:leashable": {
"soft_distance": 4.0,
"hard_distance": 6.0,
"max_distance": 12.0
},
"minecraft:damage_sensor": {
"triggers": [
{
"cause": "drowning",
"deals_damage": "no"
},
{
"cause": "fall",
"deals_damage": "no"
},
{
"cause": "fire",
"deals_damage": "no"
},
{
"cause": "fire_tick",
"deals_damage": "no"
},
{
"cause": "lava",
"deals_damage": "no"
},
{
"cause": "suffocation",
"deals_damage": "no"
}
]
}
},
"events": {}
}
}

View File

@@ -0,0 +1,26 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs:tow_boat",
"menu_category": {
"category": "equipment",
"group": "itemGroup.name.boat"
}
},
"components": {
"minecraft:max_stack_size": 1,
"minecraft:icon": "tow_boat",
"minecraft:display_name": {
"value": "Tow Boat"
},
"minecraft:entity_placer": {
"entity": "silverlabs:tow_boat",
"use_on": [
"minecraft:water",
"minecraft:flowing_water"
]
}
}
}
}

View File

@@ -0,0 +1,33 @@
{
"format_version": 2,
"header": {
"name": "Tow Boat",
"description": "A flatbed boat that can be leashed and chained for towing.",
"uuid": "a35d89fd-99e1-497a-9e78-35443c0cd59f",
"version": [1, 0, 0],
"min_engine_version": [1, 21, 0]
},
"modules": [
{
"type": "data",
"uuid": "b29d967c-8a9a-491d-867d-2866890780f0",
"version": [1, 0, 0]
},
{
"type": "script",
"uuid": "06b92593-0c15-41a0-8f39-bc6bba516ebb",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"uuid": "c6193eda-f160-475b-ab65-fca17741e41b",
"version": [1, 0, 0]
},
{
"module_name": "@minecraft/server",
"version": "1.17.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

@@ -0,0 +1,24 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shaped": {
"description": {
"identifier": "silverlabs:tow_boat_recipe"
},
"tags": ["crafting_table"],
"unlock": [
{ "item": "minecraft:lead" }
],
"pattern": [
"PLP",
"PPP"
],
"key": {
"P": { "item": "minecraft:planks" },
"L": { "item": "minecraft:lead" }
},
"result": {
"item": "silverlabs:tow_boat",
"count": 1
}
}
}

View File

@@ -0,0 +1,150 @@
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);
}
}
});