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>
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:block": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:tent_panel_l",
|
||||||
|
"traits": {
|
||||||
|
"minecraft:placement_direction": {
|
||||||
|
"enabled_states": ["minecraft:cardinal_direction"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:destructible_by_mining": { "seconds_to_destroy": 0.4 },
|
||||||
|
"minecraft:destructible_by_explosion": { "explosion_resistance": 1.0 },
|
||||||
|
"minecraft:map_color": "#547A4E",
|
||||||
|
"minecraft:material_instances": {
|
||||||
|
"*": {
|
||||||
|
"texture": "tent_canvas",
|
||||||
|
"render_method": "alpha_test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minecraft:light_dampening": 1,
|
||||||
|
"minecraft:geometry": "geometry.silverlabs.tent_panel_l"
|
||||||
|
},
|
||||||
|
"permutations": [
|
||||||
|
{
|
||||||
|
"condition": "query.block_state('minecraft:cardinal_direction') == 'south'",
|
||||||
|
"components": {
|
||||||
|
"minecraft:transformation": { "rotation": [0, 180, 0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "query.block_state('minecraft:cardinal_direction') == 'east'",
|
||||||
|
"components": {
|
||||||
|
"minecraft:transformation": { "rotation": [0, 90, 0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "query.block_state('minecraft:cardinal_direction') == 'west'",
|
||||||
|
"components": {
|
||||||
|
"minecraft:transformation": { "rotation": [0, 270, 0] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:block": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:tent_panel_r",
|
||||||
|
"traits": {
|
||||||
|
"minecraft:placement_direction": {
|
||||||
|
"enabled_states": ["minecraft:cardinal_direction"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:destructible_by_mining": { "seconds_to_destroy": 0.4 },
|
||||||
|
"minecraft:destructible_by_explosion": { "explosion_resistance": 1.0 },
|
||||||
|
"minecraft:map_color": "#547A4E",
|
||||||
|
"minecraft:material_instances": {
|
||||||
|
"*": {
|
||||||
|
"texture": "tent_canvas",
|
||||||
|
"render_method": "alpha_test"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minecraft:light_dampening": 1,
|
||||||
|
"minecraft:geometry": "geometry.silverlabs.tent_panel_r"
|
||||||
|
},
|
||||||
|
"permutations": [
|
||||||
|
{
|
||||||
|
"condition": "query.block_state('minecraft:cardinal_direction') == 'south'",
|
||||||
|
"components": {
|
||||||
|
"minecraft:transformation": { "rotation": [0, 180, 0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "query.block_state('minecraft:cardinal_direction') == 'east'",
|
||||||
|
"components": {
|
||||||
|
"minecraft:transformation": { "rotation": [0, 90, 0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condition": "query.block_state('minecraft:cardinal_direction') == 'west'",
|
||||||
|
"components": {
|
||||||
|
"minecraft:transformation": { "rotation": [0, 270, 0] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,17 @@ import { world, system, ItemStack } from "@minecraft/server";
|
|||||||
// ─── Constants ──────────────────────────────────────────────
|
// ─── Constants ──────────────────────────────────────────────
|
||||||
const TENT_ITEM = "silverlabs:tent";
|
const TENT_ITEM = "silverlabs:tent";
|
||||||
const HAMMOCK_ITEM = "silverlabs:hammock";
|
const HAMMOCK_ITEM = "silverlabs:hammock";
|
||||||
const TENT_BLOCK = "silverlabs:tent_canvas";
|
const TENT_BLOCK = "silverlabs:tent_canvas"; // legacy cube — kept so old worlds load cleanly
|
||||||
|
const TENT_PANEL_L = "silverlabs:tent_panel_l";
|
||||||
|
const TENT_PANEL_R = "silverlabs:tent_panel_r";
|
||||||
|
const TENT_BLOCK_IDS = [TENT_BLOCK, TENT_PANEL_L, TENT_PANEL_R];
|
||||||
const HAMMOCK_BLOCK = "silverlabs:hammock_cloth";
|
const HAMMOCK_BLOCK = "silverlabs:hammock_cloth";
|
||||||
const STATE_PROP = "camping_state_v1";
|
const STATE_PROP = "camping_state_v1";
|
||||||
const HAMMOCK_TAG = "camping_hammock";
|
const HAMMOCK_TAG = "camping_hammock";
|
||||||
|
const TENT_REST_PROP = "camping_tent_rest"; // per-player: "{x,y,z,dim,startTick}"
|
||||||
|
const SLEEP_TICK_INTERVAL = 20; // run sleep loop every 1s
|
||||||
|
const NIGHT_START = 12500;
|
||||||
|
const NIGHT_END = 23500;
|
||||||
|
|
||||||
// ─── State ──────────────────────────────────────────────────
|
// ─── State ──────────────────────────────────────────────────
|
||||||
// tents: key = "ox,oy,oz,dim" -> { ownerId, ownerName, cells: [[x,y,z]...] }
|
// tents: key = "ox,oy,oz,dim" -> { ownerId, ownerName, cells: [[x,y,z]...] }
|
||||||
@@ -51,6 +58,15 @@ function cardinalFacing(yaw) {
|
|||||||
return "north";
|
return "north";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map our placement facing → block-state cardinal_direction the panel rotates to.
|
||||||
|
// The geometry's "default" (cardinal_direction = "north") has the slope's outer
|
||||||
|
// edge on -X and inner apex on +X with the depth running along Z. When the
|
||||||
|
// player faces north, that aligns with the world. When they face elsewhere,
|
||||||
|
// the placement_direction trait + permutations rotate the model to match.
|
||||||
|
function blockFacingFor(playerFacing) {
|
||||||
|
return playerFacing; // 1:1 — placement_direction handles the rotation
|
||||||
|
}
|
||||||
|
|
||||||
function vecsForFacing(facing) {
|
function vecsForFacing(facing) {
|
||||||
switch (facing) {
|
switch (facing) {
|
||||||
case "north": return { fx: 0, fz: -1, rx: 1, rz: 0 };
|
case "north": return { fx: 0, fz: -1, rx: 1, rz: 0 };
|
||||||
@@ -149,19 +165,32 @@ function tryPlaceTent(player) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A-frame layout: 3 long × 2 wide, single block tall. Each cross-section is a
|
||||||
|
// pair of slope panels meeting at the apex on the seam between the two columns.
|
||||||
|
// w=0 is the player's column → LEFT side of the tent (panel_l).
|
||||||
|
// w=1 is one step right → RIGHT side of the tent (panel_r).
|
||||||
|
const blockFacing = blockFacingFor(facing);
|
||||||
const canvasCells = [];
|
const canvasCells = [];
|
||||||
for (let w = 0; w < 2; w++) {
|
for (let l = 0; l < 3; l++) {
|
||||||
// Back wall at l=0 (Y=0 and Y=1)
|
canvasCells.push({
|
||||||
canvasCells.push({ x: ox + w * rx, y: oy + 0, z: oz + w * rz });
|
x: ox + l * fx,
|
||||||
canvasCells.push({ x: ox + w * rx, y: oy + 1, z: oz + w * rz });
|
y: oy,
|
||||||
// Roof at l=1 and l=2 (Y=1)
|
z: oz + l * fz,
|
||||||
canvasCells.push({ x: ox + 1 * fx + w * rx, y: oy + 1, z: oz + 1 * fz + w * rz });
|
block: TENT_PANEL_L,
|
||||||
canvasCells.push({ x: ox + 2 * fx + w * rx, y: oy + 1, z: oz + 2 * fz + w * rz });
|
});
|
||||||
|
canvasCells.push({
|
||||||
|
x: ox + l * fx + rx,
|
||||||
|
y: oy,
|
||||||
|
z: oz + l * fz + rz,
|
||||||
|
block: TENT_PANEL_R,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const c of canvasCells) {
|
for (const c of canvasCells) {
|
||||||
try {
|
try {
|
||||||
dim.runCommand(`setblock ${c.x} ${c.y} ${c.z} ${TENT_BLOCK}`);
|
dim.runCommand(
|
||||||
|
`setblock ${c.x} ${c.y} ${c.z} ${c.block} ["minecraft:cardinal_direction"="${blockFacing}"]`
|
||||||
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +198,7 @@ function tryPlaceTent(player) {
|
|||||||
state.tents[key] = {
|
state.tents[key] = {
|
||||||
ownerId: player.id,
|
ownerId: player.id,
|
||||||
ownerName: player.name,
|
ownerName: player.name,
|
||||||
|
facing,
|
||||||
cells: canvasCells.map((c) => [c.x, c.y, c.z]),
|
cells: canvasCells.map((c) => [c.x, c.y, c.z]),
|
||||||
};
|
};
|
||||||
saveState();
|
saveState();
|
||||||
@@ -297,10 +327,12 @@ try {
|
|||||||
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
||||||
const block = event.block;
|
const block = event.block;
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
if (block.typeId === TENT_BLOCK) {
|
if (TENT_BLOCK_IDS.includes(block.typeId)) {
|
||||||
event.cancel = true;
|
event.cancel = true;
|
||||||
const player = event.player;
|
const player = event.player;
|
||||||
system.run(() => sleepInTent(player));
|
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
||||||
|
const dimId = block.dimension.id;
|
||||||
|
system.run(() => enterTentRest(player, loc, dimId));
|
||||||
} else if (block.typeId === HAMMOCK_BLOCK) {
|
} else if (block.typeId === HAMMOCK_BLOCK) {
|
||||||
event.cancel = true;
|
event.cancel = true;
|
||||||
const player = event.player;
|
const player = event.player;
|
||||||
@@ -312,18 +344,182 @@ try {
|
|||||||
console.warn(`[Camping] playerInteractWithBlock unavailable: ${e}`);
|
console.warn(`[Camping] playerInteractWithBlock unavailable: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleepInTent(player) {
|
// ─── Tent rest: vote-skip with mixed bed + tent sleepers ────────
|
||||||
|
// Tracks which players are currently "resting" in a tent. A player counts as a
|
||||||
|
// tent sleeper as long as they stay near the panel they interacted with and
|
||||||
|
// don't sneak/move/disconnect/take damage. Vanilla bed sleepers are detected
|
||||||
|
// via player.isSleeping (true while a player is in a real bed). We compare the
|
||||||
|
// combined count against the world's playersSleepingPercentage gamerule and
|
||||||
|
// skip the night when the threshold is crossed.
|
||||||
|
|
||||||
|
const tentRest = new Map(); // playerId → { x, y, z, dimId, startTick }
|
||||||
|
|
||||||
|
function isNight(tod) {
|
||||||
|
return tod >= NIGHT_START && tod <= NIGHT_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterTentRest(player, loc, dimId) {
|
||||||
const tod = world.getTimeOfDay();
|
const tod = world.getTimeOfDay();
|
||||||
if (tod < 12500 && tod > 500) {
|
if (!isNight(tod)) {
|
||||||
player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off.");
|
player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (tentRest.has(player.id)) {
|
||||||
|
leaveTentRest(player, "§7[Camping] You stop resting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tentRest.set(player.id, {
|
||||||
|
x: loc.x,
|
||||||
|
y: loc.y,
|
||||||
|
z: loc.z,
|
||||||
|
dimId,
|
||||||
|
startTick: system.currentTick,
|
||||||
|
});
|
||||||
|
// Cinematic fade so it reads like sleep instead of a status message.
|
||||||
|
try {
|
||||||
|
player.runCommand("camera @s fade time 0.4 1.5 0.6 color 0 0 0");
|
||||||
|
} catch (_) {}
|
||||||
|
try {
|
||||||
|
player.onScreenDisplay.setTitle("§7Resting…", {
|
||||||
|
fadeInDuration: 8,
|
||||||
|
stayDuration: 60,
|
||||||
|
fadeOutDuration: 12,
|
||||||
|
subtitle: "§8Move, sneak, or take damage to wake.",
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
reportSleepProgress(player, /*onEnter*/ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveTentRest(player, msg) {
|
||||||
|
if (!tentRest.delete(player.id)) return;
|
||||||
|
if (msg && player) {
|
||||||
|
try { player.sendMessage(msg); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function countSleepers() {
|
||||||
|
let bed = 0;
|
||||||
|
let tent = 0;
|
||||||
|
let online = 0;
|
||||||
|
for (const p of world.getAllPlayers()) {
|
||||||
|
online++;
|
||||||
|
// Vanilla bed sleep: Player.isSleeping is true while they're in a bed.
|
||||||
|
// Available since @minecraft/server 1.10+; guarded for safety.
|
||||||
|
let sleeping = false;
|
||||||
|
try { sleeping = !!p.isSleeping; } catch (_) {}
|
||||||
|
if (sleeping) bed++;
|
||||||
|
else if (tentRest.has(p.id)) tent++;
|
||||||
|
}
|
||||||
|
return { bed, tent, online, resting: bed + tent };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSleepThreshold() {
|
||||||
|
// playersSleepingPercentage is a percentage 0-100. 0 means any one player
|
||||||
|
// can skip night (vanilla quirk); 100 means everyone must sleep.
|
||||||
|
let pct = 100;
|
||||||
|
try {
|
||||||
|
const v = world.gameRules?.playersSleepingPercentage;
|
||||||
|
if (typeof v === "number") pct = v;
|
||||||
|
} catch (_) {}
|
||||||
|
// 0 in vanilla means "one is enough" — preserve that intent.
|
||||||
|
if (pct <= 0) return 1;
|
||||||
|
return pct;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredSleepers(online, pct) {
|
||||||
|
// Standard vanilla rounding: ceil(online * pct / 100), min 1.
|
||||||
|
return Math.max(1, Math.ceil((online * pct) / 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportSleepProgress(targetPlayer, onEnter = false) {
|
||||||
|
const { bed, tent, online, resting } = countSleepers();
|
||||||
|
const pct = getSleepThreshold();
|
||||||
|
const need = requiredSleepers(online, pct);
|
||||||
|
const remaining = Math.max(0, need - resting);
|
||||||
|
const msg = onEnter
|
||||||
|
? `§a[Camping] §7You settle in. §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)${remaining ? `. Need §f${remaining}§7 more.` : `.`}`
|
||||||
|
: `§7[Sleep] §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)`;
|
||||||
|
if (onEnter && targetPlayer) {
|
||||||
|
try { targetPlayer.sendMessage(msg); } catch (_) {}
|
||||||
|
} else {
|
||||||
|
// Broadcast a subtle update to everyone currently resting.
|
||||||
|
for (const p of world.getAllPlayers()) {
|
||||||
|
if (tentRest.has(p.id) || (() => { try { return !!p.isSleeping; } catch (_) { return false; } })()) {
|
||||||
|
try { p.sendMessage(msg); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function awardRestEffects(player) {
|
||||||
|
try {
|
||||||
player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
|
player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
|
||||||
player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
|
player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
|
||||||
world.setTimeOfDay(0);
|
} catch (_) {}
|
||||||
player.sendMessage("§a[Camping] §7You rest until dawn. §8Spawn point unchanged.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function executeNightSkip() {
|
||||||
|
// Snapshot tent sleepers before clearing, so we can give them rest perks.
|
||||||
|
const tentIds = [...tentRest.keys()];
|
||||||
|
tentRest.clear();
|
||||||
|
try { world.setTimeOfDay(0); } catch (_) {}
|
||||||
|
for (const p of world.getAllPlayers()) {
|
||||||
|
if (tentIds.includes(p.id) || p.isSleeping) {
|
||||||
|
awardRestEffects(p);
|
||||||
|
}
|
||||||
|
try { p.runCommand("camera @s clear"); } catch (_) {}
|
||||||
|
}
|
||||||
|
world.sendMessage("§6[Sleep] §7The camp rests. Dawn breaks.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sleep loop: validate tent sleepers, check threshold ────────
|
||||||
|
system.runInterval(() => {
|
||||||
|
if (tentRest.size === 0) return;
|
||||||
|
|
||||||
|
// Validate each tent sleeper still meets the criteria.
|
||||||
|
for (const [pid, rest] of [...tentRest.entries()]) {
|
||||||
|
const player = world.getAllPlayers().find((p) => p.id === pid);
|
||||||
|
if (!player) {
|
||||||
|
tentRest.delete(pid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (player.dimension.id !== rest.dimId) {
|
||||||
|
leaveTentRest(player, "§7[Camping] You wandered out of camp.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const dx = player.location.x - (rest.x + 0.5);
|
||||||
|
const dy = player.location.y - rest.y;
|
||||||
|
const dz = player.location.z - (rest.z + 0.5);
|
||||||
|
if (dx * dx + dz * dz > 4 || Math.abs(dy) > 2) {
|
||||||
|
leaveTentRest(player, "§7[Camping] You wandered out of camp.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (player.isSneaking) {
|
||||||
|
leaveTentRest(player, "§7[Camping] You climb out of the tent.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Sleepers don't get scared off by mobs but a hit cancels rest:
|
||||||
|
// (handled implicitly — damage breaks the camera fade and the player is
|
||||||
|
// expected to sneak out; we don't have a public hurt event hook here)
|
||||||
|
awardRestEffects(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tentRest.size === 0) return;
|
||||||
|
const tod = world.getTimeOfDay();
|
||||||
|
if (!isNight(tod)) {
|
||||||
|
// Sun came up some other way — clear resters quietly.
|
||||||
|
tentRest.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { online, resting } = countSleepers();
|
||||||
|
const pct = getSleepThreshold();
|
||||||
|
const need = requiredSleepers(online, pct);
|
||||||
|
if (resting >= need) {
|
||||||
|
executeNightSkip();
|
||||||
|
}
|
||||||
|
}, SLEEP_TICK_INTERVAL);
|
||||||
|
|
||||||
const HAMMOCK_ANCHOR_PROP = "camping_hammock_anchor";
|
const HAMMOCK_ANCHOR_PROP = "camping_hammock_anchor";
|
||||||
|
|
||||||
function toggleHammock(player, loc) {
|
function toggleHammock(player, loc) {
|
||||||
@@ -437,12 +633,13 @@ try {
|
|||||||
const block = event.block;
|
const block = event.block;
|
||||||
if (!block) return;
|
if (!block) return;
|
||||||
const id = block.typeId;
|
const id = block.typeId;
|
||||||
if (id !== TENT_BLOCK && id !== HAMMOCK_BLOCK) return;
|
const isTent = TENT_BLOCK_IDS.includes(id);
|
||||||
|
if (!isTent && id !== HAMMOCK_BLOCK) return;
|
||||||
event.cancel = true;
|
event.cancel = true;
|
||||||
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
||||||
const dimId = block.dimension.id;
|
const dimId = block.dimension.id;
|
||||||
const player = event.player;
|
const player = event.player;
|
||||||
if (id === TENT_BLOCK) {
|
if (isTent) {
|
||||||
system.run(() => dismantleTentAt(loc, dimId, player));
|
system.run(() => dismantleTentAt(loc, dimId, player));
|
||||||
} else {
|
} else {
|
||||||
system.run(() => dismantleHammockAt(loc, dimId, player));
|
system.run(() => dismantleHammockAt(loc, dimId, player));
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.silverlabs.tent_panel_l",
|
||||||
|
"texture_width": 16,
|
||||||
|
"texture_height": 16,
|
||||||
|
"visible_bounds_width": 2,
|
||||||
|
"visible_bounds_height": 2,
|
||||||
|
"visible_bounds_offset": [0, 0.5, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "panel",
|
||||||
|
"pivot": [0, 0, 0],
|
||||||
|
"cubes": [
|
||||||
|
{ "origin": [-8, 0, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-6, 2, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-4, 4, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-2, 6, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 0, 8, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 2, 10, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 4, 12, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 6, 14, -8], "size": [2, 2, 16], "uv": [0, 0] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.silverlabs.tent_panel_r",
|
||||||
|
"texture_width": 16,
|
||||||
|
"texture_height": 16,
|
||||||
|
"visible_bounds_width": 2,
|
||||||
|
"visible_bounds_height": 2,
|
||||||
|
"visible_bounds_offset": [0, 0.5, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "panel",
|
||||||
|
"pivot": [0, 0, 0],
|
||||||
|
"cubes": [
|
||||||
|
{ "origin": [ 6, 0, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 4, 2, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 2, 4, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [ 0, 6, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-2, 8, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-4, 10, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-6, 12, -8], "size": [2, 2, 16], "uv": [0, 0] },
|
||||||
|
{ "origin": [-8, 14, -8], "size": [2, 2, 16], "uv": [0, 0] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 773 B After Width: | Height: | Size: 427 B |
@@ -34,6 +34,10 @@ services:
|
|||||||
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
||||||
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
||||||
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
||||||
|
- ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP
|
||||||
|
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||||
|
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||||
|
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- mc-network
|
- mc-network
|
||||||
@@ -63,6 +67,10 @@ services:
|
|||||||
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
||||||
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
||||||
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
||||||
|
- ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP
|
||||||
|
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||||
|
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||||
|
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- mc-network
|
- mc-network
|
||||||
@@ -97,6 +105,10 @@ services:
|
|||||||
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
||||||
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
||||||
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
||||||
|
- ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP
|
||||||
|
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||||
|
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||||
|
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||||
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
|
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
@@ -132,6 +144,10 @@ services:
|
|||||||
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
- ./postal-service-addon/postal_service_RP:/data/resource_packs/postal_service_RP
|
||||||
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
- ./camping-supplies-addon/camping_supplies_BP:/data/behavior_packs/camping_supplies_BP
|
||||||
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
- ./camping-supplies-addon/camping_supplies_RP:/data/resource_packs/camping_supplies_RP
|
||||||
|
- ./dynamite-addon/dynamite_BP:/data/behavior_packs/dynamite_BP
|
||||||
|
- ./dynamite-addon/dynamite_RP:/data/resource_packs/dynamite_RP
|
||||||
|
- ./tow-boat-addon/tow_boat_BP:/data/behavior_packs/tow_boat_BP
|
||||||
|
- ./tow-boat-addon/tow_boat_RP:/data/resource_packs/tow_boat_RP
|
||||||
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
|
- ./village-evolution-addon/enabled_packs.json:/data/config/default/enabled_packs.json
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
47
dynamite-addon/dynamite_BP/entities/thrown_banger.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:entity": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:thrown_banger",
|
||||||
|
"is_spawnable": false,
|
||||||
|
"is_summonable": true,
|
||||||
|
"is_experimental": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:collision_box": {
|
||||||
|
"width": 0.25,
|
||||||
|
"height": 0.25
|
||||||
|
},
|
||||||
|
"minecraft:physics": {},
|
||||||
|
"minecraft:pushable": {
|
||||||
|
"is_pushable": false,
|
||||||
|
"is_pushable_by_piston": false
|
||||||
|
},
|
||||||
|
"minecraft:projectile": {
|
||||||
|
"on_hit": {
|
||||||
|
"impact_damage": {
|
||||||
|
"damage": 2,
|
||||||
|
"knockback": true,
|
||||||
|
"destroy_on_hit": true,
|
||||||
|
"semi_random_diff_damage": false
|
||||||
|
},
|
||||||
|
"spawn_aoe_cloud": {
|
||||||
|
"radius": 1.5,
|
||||||
|
"duration": 0,
|
||||||
|
"particle": "minecraft:explosion_manual",
|
||||||
|
"affect_owner": false
|
||||||
|
},
|
||||||
|
"remove_on_hit": {}
|
||||||
|
},
|
||||||
|
"power": 1.4,
|
||||||
|
"gravity": 0.05,
|
||||||
|
"inertia": 0.99,
|
||||||
|
"liquid_inertia": 0.6,
|
||||||
|
"anchor": 1,
|
||||||
|
"offset": [0, -0.1, 0],
|
||||||
|
"should_bounce": false,
|
||||||
|
"hit_sound": "random.fuse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
dynamite-addon/dynamite_BP/entities/thrown_bundle.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:entity": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:thrown_bundle",
|
||||||
|
"is_spawnable": false,
|
||||||
|
"is_summonable": true,
|
||||||
|
"is_experimental": false
|
||||||
|
},
|
||||||
|
"component_groups": {
|
||||||
|
"silverlabs:detonate": {
|
||||||
|
"minecraft:explode": {
|
||||||
|
"fuse_length": 0,
|
||||||
|
"fuse_lit": true,
|
||||||
|
"power": 3.0,
|
||||||
|
"causes_fire": false,
|
||||||
|
"breaks_blocks": true,
|
||||||
|
"max_resistance": 12,
|
||||||
|
"destroy_affected_by_griefing": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:collision_box": {
|
||||||
|
"width": 0.3,
|
||||||
|
"height": 0.3
|
||||||
|
},
|
||||||
|
"minecraft:physics": {},
|
||||||
|
"minecraft:pushable": {
|
||||||
|
"is_pushable": false,
|
||||||
|
"is_pushable_by_piston": false
|
||||||
|
},
|
||||||
|
"minecraft:projectile": {
|
||||||
|
"on_hit": {
|
||||||
|
"impact_damage": {
|
||||||
|
"damage": 6,
|
||||||
|
"knockback": true,
|
||||||
|
"destroy_on_hit": false,
|
||||||
|
"semi_random_diff_damage": false
|
||||||
|
},
|
||||||
|
"definition_event": {
|
||||||
|
"event_trigger": {
|
||||||
|
"event": "silverlabs:detonate",
|
||||||
|
"target": "self"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"power": 1.3,
|
||||||
|
"gravity": 0.06,
|
||||||
|
"inertia": 0.99,
|
||||||
|
"liquid_inertia": 0.6,
|
||||||
|
"anchor": 1,
|
||||||
|
"offset": [0, -0.1, 0],
|
||||||
|
"should_bounce": false,
|
||||||
|
"hit_sound": "random.fuse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"silverlabs:detonate": {
|
||||||
|
"add": {
|
||||||
|
"component_groups": ["silverlabs:detonate"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
dynamite-addon/dynamite_BP/entities/thrown_dynamite.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:entity": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:thrown_dynamite",
|
||||||
|
"is_spawnable": false,
|
||||||
|
"is_summonable": true,
|
||||||
|
"is_experimental": false
|
||||||
|
},
|
||||||
|
"component_groups": {
|
||||||
|
"silverlabs:detonate": {
|
||||||
|
"minecraft:explode": {
|
||||||
|
"fuse_length": 0,
|
||||||
|
"fuse_lit": true,
|
||||||
|
"power": 1.5,
|
||||||
|
"causes_fire": false,
|
||||||
|
"breaks_blocks": true,
|
||||||
|
"max_resistance": 4,
|
||||||
|
"destroy_affected_by_griefing": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:collision_box": {
|
||||||
|
"width": 0.25,
|
||||||
|
"height": 0.25
|
||||||
|
},
|
||||||
|
"minecraft:physics": {},
|
||||||
|
"minecraft:pushable": {
|
||||||
|
"is_pushable": false,
|
||||||
|
"is_pushable_by_piston": false
|
||||||
|
},
|
||||||
|
"minecraft:projectile": {
|
||||||
|
"on_hit": {
|
||||||
|
"impact_damage": {
|
||||||
|
"damage": 4,
|
||||||
|
"knockback": true,
|
||||||
|
"destroy_on_hit": false,
|
||||||
|
"semi_random_diff_damage": false
|
||||||
|
},
|
||||||
|
"definition_event": {
|
||||||
|
"event_trigger": {
|
||||||
|
"event": "silverlabs:detonate",
|
||||||
|
"target": "self"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"power": 1.4,
|
||||||
|
"gravity": 0.05,
|
||||||
|
"inertia": 0.99,
|
||||||
|
"liquid_inertia": 0.6,
|
||||||
|
"anchor": 1,
|
||||||
|
"offset": [0, -0.1, 0],
|
||||||
|
"should_bounce": false,
|
||||||
|
"hit_sound": "random.fuse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"silverlabs:detonate": {
|
||||||
|
"add": {
|
||||||
|
"component_groups": ["silverlabs:detonate"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
dynamite-addon/dynamite_BP/items/banger.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:item": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:banger",
|
||||||
|
"menu_category": {
|
||||||
|
"category": "equipment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:icon": "banger",
|
||||||
|
"minecraft:display_name": {
|
||||||
|
"value": "Banger"
|
||||||
|
},
|
||||||
|
"minecraft:max_stack_size": 16,
|
||||||
|
"minecraft:hand_equipped": true,
|
||||||
|
"minecraft:throwable": {
|
||||||
|
"do_swing_animation": true,
|
||||||
|
"max_draw_duration": 0,
|
||||||
|
"scale_power_by_draw_duration": false
|
||||||
|
},
|
||||||
|
"minecraft:projectile": {
|
||||||
|
"projectile_entity": "silverlabs:thrown_banger"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
dynamite-addon/dynamite_BP/items/dynamite_bundle.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:item": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:dynamite_bundle",
|
||||||
|
"menu_category": {
|
||||||
|
"category": "equipment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:icon": "dynamite_bundle",
|
||||||
|
"minecraft:display_name": {
|
||||||
|
"value": "Bundle of Dynamite"
|
||||||
|
},
|
||||||
|
"minecraft:max_stack_size": 16,
|
||||||
|
"minecraft:hand_equipped": true,
|
||||||
|
"minecraft:throwable": {
|
||||||
|
"do_swing_animation": true,
|
||||||
|
"max_draw_duration": 0,
|
||||||
|
"scale_power_by_draw_duration": false
|
||||||
|
},
|
||||||
|
"minecraft:projectile": {
|
||||||
|
"projectile_entity": "silverlabs:thrown_bundle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
dynamite-addon/dynamite_BP/items/dynamite_stick.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:item": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:dynamite_stick",
|
||||||
|
"menu_category": {
|
||||||
|
"category": "equipment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:icon": "dynamite_stick",
|
||||||
|
"minecraft:display_name": {
|
||||||
|
"value": "Stick of Dynamite"
|
||||||
|
},
|
||||||
|
"minecraft:max_stack_size": 16,
|
||||||
|
"minecraft:hand_equipped": true,
|
||||||
|
"minecraft:throwable": {
|
||||||
|
"do_swing_animation": true,
|
||||||
|
"max_draw_duration": 0,
|
||||||
|
"scale_power_by_draw_duration": false
|
||||||
|
},
|
||||||
|
"minecraft:projectile": {
|
||||||
|
"projectile_entity": "silverlabs:thrown_dynamite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
dynamite-addon/dynamite_BP/manifest.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"header": {
|
||||||
|
"name": "Dynamite",
|
||||||
|
"description": "Throwable explosives: bangers, dynamite sticks, and bundles.",
|
||||||
|
"uuid": "fac83943-16bc-4790-aa05-631894f59a03",
|
||||||
|
"version": [1, 0, 0],
|
||||||
|
"min_engine_version": [1, 21, 0]
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"type": "data",
|
||||||
|
"uuid": "1354002c-fdd5-4f7e-b89b-f5dd2c38799c",
|
||||||
|
"version": [1, 0, 0]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"uuid": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46",
|
||||||
|
"version": [1, 0, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
dynamite-addon/dynamite_BP/pack_icon.png
Normal file
|
After Width: | Height: | Size: 534 B |
25
dynamite-addon/dynamite_BP/recipes/banger.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:recipe_shaped": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:banger_recipe"
|
||||||
|
},
|
||||||
|
"tags": ["crafting_table"],
|
||||||
|
"unlock": [
|
||||||
|
{ "item": "minecraft:gunpowder" }
|
||||||
|
],
|
||||||
|
"pattern": [
|
||||||
|
"SP",
|
||||||
|
"G "
|
||||||
|
],
|
||||||
|
"key": {
|
||||||
|
"S": { "item": "minecraft:string" },
|
||||||
|
"P": { "item": "minecraft:paper" },
|
||||||
|
"G": { "item": "minecraft:gunpowder" }
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"item": "silverlabs:banger",
|
||||||
|
"count": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
dynamite-addon/dynamite_BP/recipes/dynamite_bundle.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:recipe_shaped": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:dynamite_bundle_recipe"
|
||||||
|
},
|
||||||
|
"tags": ["crafting_table"],
|
||||||
|
"unlock": [
|
||||||
|
{ "item": "silverlabs:dynamite_stick" }
|
||||||
|
],
|
||||||
|
"pattern": [
|
||||||
|
"DDD",
|
||||||
|
" S "
|
||||||
|
],
|
||||||
|
"key": {
|
||||||
|
"D": { "item": "silverlabs:dynamite_stick" },
|
||||||
|
"S": { "item": "minecraft:string" }
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"item": "silverlabs:dynamite_bundle",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
dynamite-addon/dynamite_BP/recipes/dynamite_stick.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:recipe_shaped": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:dynamite_stick_recipe"
|
||||||
|
},
|
||||||
|
"tags": ["crafting_table"],
|
||||||
|
"unlock": [
|
||||||
|
{ "item": "silverlabs:banger" }
|
||||||
|
],
|
||||||
|
"pattern": [
|
||||||
|
"BB",
|
||||||
|
"RT"
|
||||||
|
],
|
||||||
|
"key": {
|
||||||
|
"B": { "item": "silverlabs:banger" },
|
||||||
|
"R": { "item": "minecraft:redstone" },
|
||||||
|
"T": { "item": "minecraft:stick" }
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"item": "silverlabs:dynamite_stick",
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
dynamite-addon/dynamite_RP/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"header": {
|
||||||
|
"name": "Dynamite Resources",
|
||||||
|
"description": "Textures and language for the Dynamite addon.",
|
||||||
|
"uuid": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46",
|
||||||
|
"version": [1, 0, 0],
|
||||||
|
"min_engine_version": [1, 21, 0]
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"type": "resources",
|
||||||
|
"uuid": "587281f2-f159-4ad9-85a6-d20ff4899717",
|
||||||
|
"version": [1, 0, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
dynamite-addon/dynamite_RP/pack_icon.png
Normal file
|
After Width: | Height: | Size: 534 B |
6
dynamite-addon/dynamite_RP/texts/en_US.lang
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
item.silverlabs:banger=Banger
|
||||||
|
item.silverlabs:dynamite_stick=Stick of Dynamite
|
||||||
|
item.silverlabs:dynamite_bundle=Bundle of Dynamite
|
||||||
|
entity.silverlabs:thrown_banger.name=Banger
|
||||||
|
entity.silverlabs:thrown_dynamite.name=Dynamite
|
||||||
|
entity.silverlabs:thrown_bundle.name=Dynamite Bundle
|
||||||
3
dynamite-addon/dynamite_RP/texts/languages.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"en_US"
|
||||||
|
]
|
||||||
15
dynamite-addon/dynamite_RP/textures/item_texture.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"resource_pack_name": "dynamite_RP",
|
||||||
|
"texture_name": "atlas.items",
|
||||||
|
"texture_data": {
|
||||||
|
"banger": {
|
||||||
|
"textures": "textures/items/banger"
|
||||||
|
},
|
||||||
|
"dynamite_stick": {
|
||||||
|
"textures": "textures/items/dynamite_stick"
|
||||||
|
},
|
||||||
|
"dynamite_bundle": {
|
||||||
|
"textures": "textures/items/dynamite_bundle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
dynamite-addon/dynamite_RP/textures/items/banger.png
Normal file
|
After Width: | Height: | Size: 144 B |
BIN
dynamite-addon/dynamite_RP/textures/items/dynamite_bundle.png
Normal file
|
After Width: | Height: | Size: 186 B |
BIN
dynamite-addon/dynamite_RP/textures/items/dynamite_stick.png
Normal file
|
After Width: | Height: | Size: 196 B |
33
lobby-addon/lobby_transfer_BP/blocks/portal_field.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:block": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:portal_field",
|
||||||
|
"menu_category": {
|
||||||
|
"category": "items",
|
||||||
|
"group": "itemGroup.name.decorations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:destructible_by_mining": { "seconds_to_destroy": 0 },
|
||||||
|
"minecraft:destructible_by_explosion": { "explosion_resistance": 1200.0 },
|
||||||
|
"minecraft:light_emission": 11,
|
||||||
|
"minecraft:light_dampening": 0,
|
||||||
|
"minecraft:collision_box": false,
|
||||||
|
"minecraft:selection_box": {
|
||||||
|
"origin": [-7, 0, -7],
|
||||||
|
"size": [14, 16, 14]
|
||||||
|
},
|
||||||
|
"minecraft:map_color": "#7B27FF",
|
||||||
|
"minecraft:material_instances": {
|
||||||
|
"*": {
|
||||||
|
"texture": "portal_field",
|
||||||
|
"render_method": "blend",
|
||||||
|
"ambient_occlusion": false,
|
||||||
|
"face_dimming": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minecraft:geometry": "geometry.silverlabs.portal_field"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lobby-addon/lobby_transfer_BP/entities/portal_label.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.21.0",
|
||||||
|
"minecraft:entity": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:portal_label",
|
||||||
|
"is_spawnable": false,
|
||||||
|
"is_summonable": true,
|
||||||
|
"is_experimental": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"minecraft:nameable": {},
|
||||||
|
"minecraft:type_family": { "family": ["portal_label", "inanimate"] },
|
||||||
|
"minecraft:health": { "value": 1, "max": 1 },
|
||||||
|
"minecraft:physics": { "has_collision": false, "has_gravity": false },
|
||||||
|
"minecraft:pushable": { "is_pushable": false, "is_pushable_by_piston": false },
|
||||||
|
"minecraft:damage_sensor": {
|
||||||
|
"triggers": [{ "deals_damage": false }]
|
||||||
|
},
|
||||||
|
"minecraft:fire_immune": {},
|
||||||
|
"minecraft:knockback_resistance": { "value": 1.0, "max": 1.0 },
|
||||||
|
"minecraft:persistent": {},
|
||||||
|
"minecraft:movement": { "value": 0 },
|
||||||
|
"minecraft:collision_box": { "width": 0.01, "height": 0.01 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
import { world, system } from "@minecraft/server";
|
import { world, system } from "@minecraft/server";
|
||||||
import { transferPlayer } from "@minecraft/server-admin";
|
import { transferPlayer } from "@minecraft/server-admin";
|
||||||
|
|
||||||
// Portal block → transfer target mapping (custom blocks — priority detection)
|
// Portal frame block → transfer target mapping. The frame is the floor block;
|
||||||
|
// the walk-through field column above it inherits the destination by looking down.
|
||||||
const PORTAL_BLOCKS = {
|
const PORTAL_BLOCKS = {
|
||||||
"silverlabs:portal_jamie": { name: "Jamie's World", host: "10.0.0.247", port: 19133, color: "§a" },
|
"silverlabs:portal_jamie": { name: "Jamie's World", host: "10.0.0.247", port: 19133, color: "§a" },
|
||||||
"silverlabs:portal_lyla": { name: "Lyla's World", host: "10.0.0.247", port: 19134, color: "§d" },
|
"silverlabs:portal_lyla": { name: "Lyla's World", host: "10.0.0.247", port: 19134, color: "§d" },
|
||||||
"silverlabs:portal_mya": { name: "Mya's World", host: "10.0.0.247", port: 19135, color: "§b" },
|
"silverlabs:portal_mya": { name: "Mya's World", host: "10.0.0.247", port: 19135, color: "§b" },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Coordinate-based portal zones (fallback detection)
|
const PORTAL_FIELD_BLOCK = "silverlabs:portal_field";
|
||||||
|
const PORTAL_LABEL_ENTITY = "silverlabs:portal_label";
|
||||||
|
|
||||||
|
// Coordinate-based portal zones (fallback detection + auto-spawn anchors).
|
||||||
|
// `frame_y` is the floor block where the destination frame sits; the field
|
||||||
|
// blocks are placed at frame_y+1 and frame_y+2 (2-tall walk-through column),
|
||||||
|
// and the floating label entity sits at frame_y+3.5.
|
||||||
const PORTAL_ZONES = [
|
const PORTAL_ZONES = [
|
||||||
{ name: "Jamie's World", x: 436, y: 66, z: -296, host: "10.0.0.247", port: 19133, color: "§a" },
|
{ name: "Jamie's World", x: 436, y: 66, z: -296, frame_y: 66, host: "10.0.0.247", port: 19133, color: "§a" },
|
||||||
{ name: "Lyla's World", x: 462, y: 65, z: -322, host: "10.0.0.247", port: 19134, color: "§d" },
|
{ name: "Lyla's World", x: 462, y: 65, z: -322, frame_y: 65, host: "10.0.0.247", port: 19134, color: "§d" },
|
||||||
{ name: "Lyla's World", x: 474, y: 65, z: -281, host: "10.0.0.247", port: 19134, color: "§d" }, // Super Kitties portal
|
{ name: "Lyla's World", x: 474, y: 65, z: -281, frame_y: 65, host: "10.0.0.247", port: 19134, color: "§d", label_suffix: " (Super Kitties)" },
|
||||||
{ name: "Mya's World", x: 488, y: 66, z: -296, host: "10.0.0.247", port: 19135, color: "§b" },
|
{ name: "Mya's World", x: 488, y: 66, z: -296, frame_y: 66, host: "10.0.0.247", port: 19135, color: "§b" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PORTAL_RADIUS_X = 2.5;
|
const PORTAL_RADIUS_X = 2.5;
|
||||||
@@ -38,13 +45,26 @@ world.afterEvents.playerSpawn.subscribe((event) => {
|
|||||||
function checkBlockPortal(player) {
|
function checkBlockPortal(player) {
|
||||||
const pos = player.location;
|
const pos = player.location;
|
||||||
const dimension = player.dimension;
|
const dimension = player.dimension;
|
||||||
const blockAtFeet = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) });
|
const fx = Math.floor(pos.x);
|
||||||
const blockBelow = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y) - 1, z: Math.floor(pos.z) });
|
const fy = Math.floor(pos.y);
|
||||||
|
const fz = Math.floor(pos.z);
|
||||||
|
const blockAtFeet = dimension.getBlock({ x: fx, y: fy, z: fz });
|
||||||
|
const blockHead = dimension.getBlock({ x: fx, y: fy + 1, z: fz });
|
||||||
|
const blockBelow = dimension.getBlock({ x: fx, y: fy - 1, z: fz });
|
||||||
|
|
||||||
const feetId = blockAtFeet?.typeId;
|
// Walk-through portal field: scan downward for the destination frame block.
|
||||||
const belowId = blockBelow?.typeId;
|
for (const b of [blockAtFeet, blockHead]) {
|
||||||
|
if (b?.typeId === PORTAL_FIELD_BLOCK) {
|
||||||
|
for (let dy = 1; dy <= 4; dy++) {
|
||||||
|
const probe = dimension.getBlock({ x: fx, y: fy - dy, z: fz });
|
||||||
|
const dest = probe && PORTAL_BLOCKS[probe.typeId];
|
||||||
|
if (dest) return dest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return PORTAL_BLOCKS[feetId] || PORTAL_BLOCKS[belowId] || null;
|
// Legacy: standing on a frame block directly.
|
||||||
|
return PORTAL_BLOCKS[blockAtFeet?.typeId] || PORTAL_BLOCKS[blockBelow?.typeId] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,43 +131,65 @@ world.afterEvents.playerLeave.subscribe((event) => {
|
|||||||
spawnTicks.delete(event.playerId);
|
spawnTicks.delete(event.playerId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Place signs above each portal on load ──────────────────────
|
// ─── Walk-through portal fields + floating destination labels ──
|
||||||
|
// For each known portal zone, place a 2-block-tall column of `portal_field`
|
||||||
|
// blocks (no collision, translucent, light-emitting) directly above the frame
|
||||||
|
// block, and summon an invisible `portal_label` entity above with the
|
||||||
|
// destination world's name visible as a floating tag.
|
||||||
|
|
||||||
function placePortalSigns() {
|
function ensurePortalField(overworld, x, fy, z) {
|
||||||
|
for (const dy of [1, 2]) {
|
||||||
|
try {
|
||||||
|
const b = overworld.getBlock({ x, y: fy + dy, z });
|
||||||
|
if (!b) continue;
|
||||||
|
if (b.typeId !== PORTAL_FIELD_BLOCK) {
|
||||||
|
overworld.runCommand(`setblock ${x} ${fy + dy} ${z} ${PORTAL_FIELD_BLOCK}`);
|
||||||
|
}
|
||||||
|
} catch (_) { /* chunks may be unloaded; retried next boot */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePortalLabel(overworld, zone) {
|
||||||
|
const labelY = zone.frame_y + 3.5;
|
||||||
|
// Check if a label already exists nearby; replace its tag if outdated.
|
||||||
|
let existing = null;
|
||||||
|
try {
|
||||||
|
const matches = overworld.getEntities({
|
||||||
|
type: PORTAL_LABEL_ENTITY,
|
||||||
|
location: { x: zone.x + 0.5, y: labelY, z: zone.z + 0.5 },
|
||||||
|
maxDistance: 1.5,
|
||||||
|
});
|
||||||
|
existing = matches[0] || null;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
const tag = `${zone.color}${zone.name}${zone.label_suffix || ""}`;
|
||||||
|
if (existing) {
|
||||||
|
try { existing.nameTag = tag; } catch (_) {}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const entity = overworld.spawnEntity(
|
||||||
|
PORTAL_LABEL_ENTITY,
|
||||||
|
{ x: zone.x + 0.5, y: labelY, z: zone.z + 0.5 }
|
||||||
|
);
|
||||||
|
if (entity) {
|
||||||
|
try { entity.nameTag = tag; } catch (_) {}
|
||||||
|
}
|
||||||
|
} catch (_) { /* chunks may be unloaded */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function placePortalDressing() {
|
||||||
const overworld = world.getDimension("overworld");
|
const overworld = world.getDimension("overworld");
|
||||||
const signs = [
|
for (const zone of PORTAL_ZONES) {
|
||||||
{ x: 438, y: 71, z: -296, name: "Jamie's", color: "§a", facing: 5 }, // east-facing
|
if (zone.frame_y === undefined) continue;
|
||||||
{ x: 462, y: 71, z: -320, name: "Lyla's", color: "§d", facing: 3 }, // south-facing
|
ensurePortalField(overworld, zone.x, zone.frame_y, zone.z);
|
||||||
{ x: 486, y: 71, z: -296, name: "Mya's", color: "§b", facing: 4 }, // west-facing
|
ensurePortalLabel(overworld, zone);
|
||||||
{ x: 474, y: 74, z: -281, name: "Super Kitties\n§fLyla's", color: "§d", facing: 2 }, // north-facing (Super Kitties portal)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const sign of signs) {
|
|
||||||
try {
|
|
||||||
overworld.runCommand(`setblock ${sign.x} ${sign.y} ${sign.z} oak_wall_sign ["facing_direction":${sign.facing}]`);
|
|
||||||
} catch (e) {
|
|
||||||
// Non-fatal — chunks may not be loaded
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
system.runTimeout(() => {
|
// Initial place + periodic re-ensure (handles chunk loading after first attempt).
|
||||||
for (const sign of signs) {
|
system.runTimeout(() => placePortalDressing(), 40);
|
||||||
try {
|
system.runInterval(() => placePortalDressing(), 600); // every 30s
|
||||||
const block = overworld.getBlock({ x: sign.x, y: sign.y, z: sign.z });
|
|
||||||
if (!block) continue;
|
|
||||||
const signComponent = block.getComponent("minecraft:sign");
|
|
||||||
if (!signComponent) continue;
|
|
||||||
signComponent.setText(`${sign.color}${sign.name}\n${sign.color}World\n§7▼ Step in ▼`);
|
|
||||||
} catch (e) {
|
|
||||||
// Non-fatal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
system.runTimeout(() => {
|
|
||||||
placePortalSigns();
|
|
||||||
}, 40);
|
|
||||||
|
|
||||||
system.run(() => {
|
system.run(() => {
|
||||||
world.sendMessage("§6[Hub] §7Portal transfer system loaded!");
|
world.sendMessage("§6[Hub] §7Portal transfer system loaded!");
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"silverlabs:portal_frame": { "sound": "stone" },
|
"silverlabs:portal_frame": { "sound": "stone" },
|
||||||
"silverlabs:portal_jamie": { "sound": "stone" },
|
"silverlabs:portal_jamie": { "sound": "stone" },
|
||||||
"silverlabs:portal_lyla": { "sound": "stone" },
|
"silverlabs:portal_lyla": { "sound": "stone" },
|
||||||
"silverlabs:portal_mya": { "sound": "stone" }
|
"silverlabs:portal_mya": { "sound": "stone" },
|
||||||
|
"silverlabs:portal_field": { "sound": "glass" }
|
||||||
}
|
}
|
||||||
|
|||||||
12
lobby-addon/lobby_transfer_RP/entity/portal_label.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.10.0",
|
||||||
|
"minecraft:client_entity": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:portal_label",
|
||||||
|
"materials": { "default": "entity_alphatest" },
|
||||||
|
"textures": { "default": "textures/entity/portal_label" },
|
||||||
|
"geometry": { "default": "geometry.silverlabs.portal_label" },
|
||||||
|
"render_controllers": ["controller.render.default"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.silverlabs.portal_field",
|
||||||
|
"texture_width": 16,
|
||||||
|
"texture_height": 16,
|
||||||
|
"visible_bounds_width": 1.5,
|
||||||
|
"visible_bounds_height": 2,
|
||||||
|
"visible_bounds_offset": [0, 0.5, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "field",
|
||||||
|
"pivot": [0, 0, 0],
|
||||||
|
"cubes": [
|
||||||
|
{ "origin": [-7, 0, -1], "size": [14, 16, 2], "uv": [0, 0] },
|
||||||
|
{ "origin": [-1, 0, -7], "size": [2, 16, 14], "uv": [0, 0] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.silverlabs.portal_label",
|
||||||
|
"texture_width": 1,
|
||||||
|
"texture_height": 1,
|
||||||
|
"visible_bounds_width": 0,
|
||||||
|
"visible_bounds_height": 0,
|
||||||
|
"visible_bounds_offset": [0, 0, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"pivot": [0, 0, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png
Normal file
|
After Width: | Height: | Size: 941 B |
BIN
lobby-addon/lobby_transfer_RP/textures/entity/portal_label.png
Normal file
|
After Width: | Height: | Size: 70 B |
@@ -15,6 +15,9 @@
|
|||||||
},
|
},
|
||||||
"portal_mya": {
|
"portal_mya": {
|
||||||
"textures": "textures/blocks/prismarine_bricks"
|
"textures": "textures/blocks/prismarine_bricks"
|
||||||
|
},
|
||||||
|
"portal_field": {
|
||||||
|
"textures": "textures/blocks/portal_field"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 270 B After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 321 B |
111
scripts/build-art-catalog.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Build a contact sheet of every texture under art/ — each PNG upscaled
|
||||||
|
with nearest-neighbour (so pixels stay crisp) and labelled with its
|
||||||
|
relative path. Output: art/CATALOG.png
|
||||||
|
|
||||||
|
Run from repo root:
|
||||||
|
python3 scripts/build-art-catalog.py
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
ART = REPO / "art"
|
||||||
|
OUT = ART / "CATALOG.png"
|
||||||
|
|
||||||
|
TILE = 128 # upscaled texture size
|
||||||
|
COLS = 6 # tiles per row
|
||||||
|
PAD_X = 20 # horizontal padding around each tile
|
||||||
|
PAD_Y = 60 # vertical padding (extra room for label below)
|
||||||
|
LABEL_PX = 14 # label font size
|
||||||
|
BG = (30, 30, 46) # dark background
|
||||||
|
FG = (230, 230, 240)
|
||||||
|
SECTION_BG = (50, 60, 90)
|
||||||
|
|
||||||
|
|
||||||
|
def load_font(size):
|
||||||
|
for cand in (
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
||||||
|
"/mnt/c/Windows/Fonts/consola.ttf",
|
||||||
|
"/mnt/c/Windows/Fonts/arial.ttf",
|
||||||
|
):
|
||||||
|
if os.path.exists(cand):
|
||||||
|
return ImageFont.truetype(cand, size)
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
def collect():
|
||||||
|
"""Return {pack_name: [(relative_path, abs_path), ...]} grouped & sorted."""
|
||||||
|
groups = {}
|
||||||
|
for png in sorted(ART.rglob("*.png")):
|
||||||
|
if png.name == "CATALOG.png":
|
||||||
|
continue
|
||||||
|
rel = png.relative_to(ART)
|
||||||
|
pack = rel.parts[0]
|
||||||
|
groups.setdefault(pack, []).append((rel, png))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def upscale(png_path, size):
|
||||||
|
img = Image.open(png_path).convert("RGBA")
|
||||||
|
return img.resize((size, size), Image.NEAREST)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
groups = collect()
|
||||||
|
if not groups:
|
||||||
|
print("No PNGs found under art/. Nothing to do.")
|
||||||
|
return
|
||||||
|
|
||||||
|
font_label = load_font(LABEL_PX)
|
||||||
|
font_section = load_font(LABEL_PX + 8)
|
||||||
|
|
||||||
|
# Calculate overall canvas size
|
||||||
|
section_height = LABEL_PX + 24
|
||||||
|
tile_block_h = TILE + PAD_Y
|
||||||
|
total_h = 40
|
||||||
|
for pack, items in groups.items():
|
||||||
|
rows = (len(items) + COLS - 1) // COLS
|
||||||
|
total_h += section_height + rows * tile_block_h + 20
|
||||||
|
total_w = COLS * (TILE + PAD_X) + PAD_X + 40
|
||||||
|
|
||||||
|
canvas = Image.new("RGBA", (total_w, total_h), BG)
|
||||||
|
draw = ImageDraw.Draw(canvas)
|
||||||
|
|
||||||
|
y = 20
|
||||||
|
for pack, items in groups.items():
|
||||||
|
# Section header
|
||||||
|
draw.rectangle([20, y, total_w - 20, y + section_height - 4], fill=SECTION_BG)
|
||||||
|
draw.text((32, y + 4), pack, font=font_section, fill=FG)
|
||||||
|
y += section_height + 6
|
||||||
|
|
||||||
|
# Tiles
|
||||||
|
for i, (rel, abs_path) in enumerate(items):
|
||||||
|
col = i % COLS
|
||||||
|
row = i // COLS
|
||||||
|
x = 20 + PAD_X // 2 + col * (TILE + PAD_X)
|
||||||
|
ty = y + row * tile_block_h
|
||||||
|
|
||||||
|
try:
|
||||||
|
tile = upscale(abs_path, TILE)
|
||||||
|
canvas.paste(tile, (x, ty), tile)
|
||||||
|
except Exception as e:
|
||||||
|
draw.rectangle([x, ty, x + TILE, ty + TILE], outline=(200, 80, 80), width=2)
|
||||||
|
draw.text((x + 4, ty + 4), f"ERR: {e}", font=font_label, fill=(255, 120, 120))
|
||||||
|
|
||||||
|
# Label: show subpath + filename below the tile
|
||||||
|
label = "/".join(rel.parts[1:]) # drop pack prefix
|
||||||
|
draw.text((x, ty + TILE + 4), label, font=font_label, fill=FG)
|
||||||
|
|
||||||
|
rows = (len(items) + COLS - 1) // COLS
|
||||||
|
y += rows * tile_block_h + 20
|
||||||
|
|
||||||
|
canvas.save(OUT)
|
||||||
|
print(f"Wrote {OUT} — {sum(len(v) for v in groups.values())} textures across {len(groups)} packs.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -30,6 +30,13 @@ MB_BLACK = (20, 20, 20)
|
|||||||
MB_FLAG = (235, 190, 55)
|
MB_FLAG = (235, 190, 55)
|
||||||
MB_POST = (90, 60, 35)
|
MB_POST = (90, 60, 35)
|
||||||
|
|
||||||
|
# Tent canvas — weathered green army-tent fabric
|
||||||
|
CANVAS_LIGHT = (118, 138, 88)
|
||||||
|
CANVAS_MID = (90, 110, 68)
|
||||||
|
CANVAS_DARK = (66, 84, 50)
|
||||||
|
CANVAS_SHADOW = (44, 58, 36)
|
||||||
|
CANVAS_STITCH = (190, 175, 130)
|
||||||
|
|
||||||
|
|
||||||
def save(img: Image.Image, rel: str) -> None:
|
def save(img: Image.Image, rel: str) -> None:
|
||||||
out = ROOT / rel
|
out = ROOT / rel
|
||||||
@@ -228,10 +235,174 @@ def mailbox() -> Image.Image:
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tent Canvas ────────────────────────────────────────────
|
||||||
|
# Weathered army-canvas fabric: subtle horizontal weave + occasional darker
|
||||||
|
# mottling + a vertical seam stitch line. Fully opaque so it doesn't ghost
|
||||||
|
# against the alpha_test renderer.
|
||||||
|
def tent_canvas() -> Image.Image:
|
||||||
|
img = Image.new("RGBA", (16, 16), CANVAS_MID)
|
||||||
|
|
||||||
|
# Horizontal weave bands (every 2 rows alternating tone)
|
||||||
|
for y in range(16):
|
||||||
|
row = CANVAS_MID if (y // 2) % 2 == 0 else CANVAS_DARK
|
||||||
|
rect(img, 0, y, 15, y, row)
|
||||||
|
|
||||||
|
# Diagonal weave specks for fabric texture
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(16):
|
||||||
|
if (x + y) % 5 == 0:
|
||||||
|
px(img, x, y, CANVAS_LIGHT)
|
||||||
|
elif (x - y) % 7 == 0:
|
||||||
|
px(img, x, y, CANVAS_SHADOW)
|
||||||
|
|
||||||
|
# Wear / sun-bleached patches (clusters)
|
||||||
|
for (x, y) in [(2, 3), (3, 3), (10, 6), (11, 6), (5, 11), (6, 11), (13, 12)]:
|
||||||
|
px(img, x, y, CANVAS_LIGHT)
|
||||||
|
|
||||||
|
# Vertical seam stitch down the center
|
||||||
|
for y in range(0, 16, 2):
|
||||||
|
px(img, 7, y, CANVAS_STITCH)
|
||||||
|
px(img, 8, y, CANVAS_SHADOW)
|
||||||
|
|
||||||
|
# Edge darken (gives the panel some depth at block boundaries)
|
||||||
|
rect(img, 0, 0, 15, 0, CANVAS_SHADOW)
|
||||||
|
rect(img, 0, 15, 15, 15, CANVAS_SHADOW)
|
||||||
|
rect(img, 0, 0, 0, 15, CANVAS_SHADOW)
|
||||||
|
rect(img, 15, 0, 15, 15, CANVAS_SHADOW)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# ── Mailbox v2 — block-style (full 16x16 face, no transparency) ─
|
||||||
|
# The previous sprite was an item-sized icon centered on transparency, which
|
||||||
|
# reads as a placeholder when applied to all 6 faces of a cube. This version
|
||||||
|
# fills the face: red body panel, vertical seam, slot, latch, riveted corners.
|
||||||
|
def mailbox_block() -> Image.Image:
|
||||||
|
img = Image.new("RGBA", (16, 16), MB_RED)
|
||||||
|
|
||||||
|
# Vertical highlight strip on the left
|
||||||
|
rect(img, 0, 0, 1, 15, MB_RED_HL)
|
||||||
|
# Vertical shadow strip on the right
|
||||||
|
rect(img, 14, 0, 15, 15, MB_RED_DARK)
|
||||||
|
# Top dome highlight (1px band)
|
||||||
|
rect(img, 2, 0, 13, 0, MB_RED_HL)
|
||||||
|
# Bottom shadow band
|
||||||
|
rect(img, 2, 15, 13, 15, MB_RED_DARK)
|
||||||
|
|
||||||
|
# Horizontal mail slot (centered)
|
||||||
|
rect(img, 4, 6, 11, 8, MB_BLACK)
|
||||||
|
rect(img, 4, 6, 11, 6, (50, 50, 50)) # slot lip highlight
|
||||||
|
rect(img, 4, 8, 11, 8, (5, 5, 5)) # slot lip shadow
|
||||||
|
|
||||||
|
# Round latch below the slot
|
||||||
|
rect(img, 7, 11, 8, 12, MB_BLACK)
|
||||||
|
px(img, 7, 11, (90, 90, 90))
|
||||||
|
|
||||||
|
# Yellow flag emblem in the top-right corner
|
||||||
|
rect(img, 12, 2, 13, 4, MB_FLAG)
|
||||||
|
px(img, 12, 2, (180, 140, 40))
|
||||||
|
# Flag pole
|
||||||
|
px(img, 13, 5, (60, 60, 60))
|
||||||
|
|
||||||
|
# Rivets in the corners
|
||||||
|
for (x, y) in [(2, 2), (13, 2), (2, 13), (13, 13)]:
|
||||||
|
px(img, x, y, (40, 15, 15))
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# ── Post Office v2 — block-style facade ─────────────────────
|
||||||
|
# A small post-office building face: brick wall, "POST" plaque, awning hint.
|
||||||
|
def post_office_block() -> Image.Image:
|
||||||
|
img = Image.new("RGBA", (16, 16), BRICK_RED)
|
||||||
|
|
||||||
|
# Brick courses with mortar lines (offset every other row)
|
||||||
|
for y in range(16):
|
||||||
|
# mortar line every 4 rows
|
||||||
|
if y % 4 == 0:
|
||||||
|
rect(img, 0, y, 15, y, MORTAR)
|
||||||
|
else:
|
||||||
|
# vertical mortar joints, offset by course
|
||||||
|
offset = 0 if (y // 4) % 2 == 0 else 4
|
||||||
|
for x in range(16):
|
||||||
|
if (x + offset) % 8 == 0:
|
||||||
|
px(img, x, y, MORTAR)
|
||||||
|
else:
|
||||||
|
# subtle brick tone variation
|
||||||
|
if (x + y) % 3 == 0:
|
||||||
|
px(img, x, y, BRICK_DARK)
|
||||||
|
|
||||||
|
# Cream-colored "POST" plaque (centered)
|
||||||
|
rect(img, 2, 5, 13, 10, CREAM)
|
||||||
|
rect(img, 2, 5, 13, 5, ENVELOPE_LINE) # top border
|
||||||
|
rect(img, 2, 10, 13, 10, ENVELOPE_LINE) # bottom border
|
||||||
|
rect(img, 2, 5, 2, 10, ENVELOPE_LINE) # left border
|
||||||
|
rect(img, 13, 5, 13, 10, ENVELOPE_LINE) # right border
|
||||||
|
|
||||||
|
# "POST" lettering — 4 chunky chars on the plaque
|
||||||
|
# P
|
||||||
|
rect(img, 3, 7, 4, 8, ENVELOPE_LINE); px(img, 3, 6, ENVELOPE_LINE)
|
||||||
|
# O
|
||||||
|
rect(img, 6, 6, 7, 9, ENVELOPE_LINE); px(img, 6, 7, CREAM); px(img, 7, 7, CREAM); px(img, 6, 8, CREAM); px(img, 7, 8, CREAM)
|
||||||
|
# S (simplified stub)
|
||||||
|
rect(img, 9, 6, 10, 6, ENVELOPE_LINE); px(img, 9, 7, ENVELOPE_LINE); rect(img, 9, 8, 10, 8, ENVELOPE_LINE); px(img, 10, 9, ENVELOPE_LINE); rect(img, 9, 9, 10, 9, ENVELOPE_LINE)
|
||||||
|
# T
|
||||||
|
rect(img, 11, 6, 12, 6, ENVELOPE_LINE); rect(img, 11, 7, 11, 9, ENVELOPE_LINE)
|
||||||
|
|
||||||
|
# Red stamp accent in the top-right
|
||||||
|
rect(img, 12, 1, 14, 3, STAMP_RED)
|
||||||
|
px(img, 13, 2, (255, 220, 220))
|
||||||
|
|
||||||
|
# Awning suggestion (alternating red/cream stripes at the top)
|
||||||
|
for x in range(16):
|
||||||
|
if (x // 2) % 2 == 0:
|
||||||
|
px(img, x, 1, (210, 70, 70))
|
||||||
|
else:
|
||||||
|
px(img, x, 1, CREAM)
|
||||||
|
rect(img, 0, 0, 15, 0, ENVELOPE_LINE)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# ── Portal Field ───────────────────────────────────────────
|
||||||
|
# Translucent swirling-energy texture for the walk-through portal column.
|
||||||
|
# Renders with alpha blending — the dark areas read as voids, the bright
|
||||||
|
# center streaks read as concentrated portal energy.
|
||||||
|
def portal_field() -> Image.Image:
|
||||||
|
import math
|
||||||
|
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
|
||||||
|
cx, cy = 7.5, 7.5
|
||||||
|
for y in range(16):
|
||||||
|
for x in range(16):
|
||||||
|
dx = x - cx
|
||||||
|
dy = y - cy
|
||||||
|
dist = math.sqrt(dx * dx + dy * dy)
|
||||||
|
angle = math.atan2(dy, dx)
|
||||||
|
# swirl: angle modulated by distance
|
||||||
|
swirl = math.sin(angle * 3 + dist * 1.5) * 0.5 + 0.5
|
||||||
|
# purple/violet base
|
||||||
|
r = int(80 + swirl * 100)
|
||||||
|
g = int(20 + swirl * 30)
|
||||||
|
b = int(140 + swirl * 90)
|
||||||
|
# alpha falls off near edges, bright in middle bands
|
||||||
|
edge = max(0, 1 - dist / 8.5)
|
||||||
|
alpha = int(160 * edge + swirl * 60)
|
||||||
|
alpha = max(0, min(220, alpha))
|
||||||
|
img.putpixel((x, y), (r, g, b, alpha))
|
||||||
|
# Bright vertical core streaks
|
||||||
|
for y in range(16):
|
||||||
|
for x in (7, 8):
|
||||||
|
r, g, b, _ = img.getpixel((x, y))
|
||||||
|
img.putpixel((x, y), (min(255, r + 60), min(255, g + 30), min(255, b + 60), 220))
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png")
|
save(smart_crafting_table(), "smart-crafting-addon/smart_crafting_RP/textures/blocks/smart_crafting_table.png")
|
||||||
save(post_office(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png")
|
save(post_office_block(), "postal-service-addon/postal_service_RP/textures/blocks/post_office.png")
|
||||||
save(mailbox(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png")
|
save(mailbox_block(), "postal-service-addon/postal_service_RP/textures/blocks/mailbox.png")
|
||||||
|
save(tent_canvas(), "camping-supplies-addon/camping_supplies_RP/textures/blocks/tent_canvas.png")
|
||||||
|
save(portal_field(), "lobby-addon/lobby_transfer_RP/textures/blocks/portal_field.png")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
95
tow-boat-addon/tow_boat_BP/entities/tow_boat.json
Normal 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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tow-boat-addon/tow_boat_BP/items/tow_boat.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
tow-boat-addon/tow_boat_BP/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
tow-boat-addon/tow_boat_BP/pack_icon.png
Normal file
|
After Width: | Height: | Size: 381 B |
24
tow-boat-addon/tow_boat_BP/recipes/tow_boat.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
tow-boat-addon/tow_boat_BP/scripts/main.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"animation.tow_boat.bob": {
|
||||||
|
"loop": true,
|
||||||
|
"animation_length": 4.0,
|
||||||
|
"bones": {
|
||||||
|
"root": {
|
||||||
|
"position": {
|
||||||
|
"0.0": [0, 0, 0],
|
||||||
|
"1.0": [0, 0.6, 0],
|
||||||
|
"2.0": [0, 0, 0],
|
||||||
|
"3.0": [0, -0.4, 0],
|
||||||
|
"4.0": [0, 0, 0]
|
||||||
|
},
|
||||||
|
"rotation": {
|
||||||
|
"0.0": [0, 0, 0],
|
||||||
|
"1.0": [1.5, 0, -0.5],
|
||||||
|
"2.0": [0, 0, 0],
|
||||||
|
"3.0": [-1.0, 0, 0.5],
|
||||||
|
"4.0": [0, 0, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
tow-boat-addon/tow_boat_RP/entity/tow_boat.entity.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.10.0",
|
||||||
|
"minecraft:client_entity": {
|
||||||
|
"description": {
|
||||||
|
"identifier": "silverlabs:tow_boat",
|
||||||
|
"materials": {
|
||||||
|
"default": "entity_alphatest"
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"default": "textures/entity/tow_boat"
|
||||||
|
},
|
||||||
|
"geometry": {
|
||||||
|
"default": "geometry.tow_boat"
|
||||||
|
},
|
||||||
|
"render_controllers": [
|
||||||
|
"controller.render.tow_boat"
|
||||||
|
],
|
||||||
|
"animations": {
|
||||||
|
"bob": "animation.tow_boat.bob"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"animate": ["bob"]
|
||||||
|
},
|
||||||
|
"spawn_egg": {
|
||||||
|
"texture": "tow_boat",
|
||||||
|
"texture_index": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
tow-boat-addon/tow_boat_RP/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"format_version": 2,
|
||||||
|
"header": {
|
||||||
|
"name": "Tow Boat Resources",
|
||||||
|
"description": "Model, texture, and animations for the Tow Boat addon.",
|
||||||
|
"uuid": "c6193eda-f160-475b-ab65-fca17741e41b",
|
||||||
|
"version": [1, 0, 0],
|
||||||
|
"min_engine_version": [1, 21, 0]
|
||||||
|
},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"type": "resources",
|
||||||
|
"uuid": "2ef90fb4-1c8f-404e-924f-a41463a51758",
|
||||||
|
"version": [1, 0, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
105
tow-boat-addon/tow_boat_RP/models/entity/tow_boat.geo.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.tow_boat",
|
||||||
|
"texture_width": 64,
|
||||||
|
"texture_height": 32,
|
||||||
|
"visible_bounds_width": 3,
|
||||||
|
"visible_bounds_height": 2,
|
||||||
|
"visible_bounds_offset": [0, 0.5, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"pivot": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hull",
|
||||||
|
"parent": "root",
|
||||||
|
"pivot": [0, 0, 0],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-11, 0, -16],
|
||||||
|
"size": [22, 3, 32],
|
||||||
|
"uv": [0, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deck",
|
||||||
|
"parent": "hull",
|
||||||
|
"pivot": [0, 3, 0],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-11, 3, -16],
|
||||||
|
"size": [22, 1, 32],
|
||||||
|
"uv": [0, 16]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rail_left",
|
||||||
|
"parent": "deck",
|
||||||
|
"pivot": [-11, 4, 0],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-12, 4, -16],
|
||||||
|
"size": [1, 2, 32],
|
||||||
|
"uv": [0, 18]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rail_right",
|
||||||
|
"parent": "deck",
|
||||||
|
"pivot": [11, 4, 0],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [11, 4, -16],
|
||||||
|
"size": [1, 2, 32],
|
||||||
|
"uv": [0, 18]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rail_front",
|
||||||
|
"parent": "deck",
|
||||||
|
"pivot": [0, 4, -16],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-11, 4, -17],
|
||||||
|
"size": [22, 2, 1],
|
||||||
|
"uv": [0, 22]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rail_back",
|
||||||
|
"parent": "deck",
|
||||||
|
"pivot": [0, 4, 16],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-11, 4, 16],
|
||||||
|
"size": [22, 2, 1],
|
||||||
|
"uv": [0, 22]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tow_post",
|
||||||
|
"parent": "deck",
|
||||||
|
"pivot": [0, 4, -14],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-1, 4, -15],
|
||||||
|
"size": [2, 4, 2],
|
||||||
|
"uv": [44, 16]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
tow-boat-addon/tow_boat_RP/pack_icon.png
Normal file
|
After Width: | Height: | Size: 381 B |
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.10.0",
|
||||||
|
"render_controllers": {
|
||||||
|
"controller.render.tow_boat": {
|
||||||
|
"geometry": "Geometry.default",
|
||||||
|
"materials": [
|
||||||
|
{ "*": "Material.default" }
|
||||||
|
],
|
||||||
|
"textures": ["Texture.default"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
tow-boat-addon/tow_boat_RP/texts/en_US.lang
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
item.silverlabs:tow_boat=Tow Boat
|
||||||
|
entity.silverlabs:tow_boat.name=Tow Boat
|
||||||
|
action.interact.mount=Mount
|
||||||
3
tow-boat-addon/tow_boat_RP/texts/languages.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"en_US"
|
||||||
|
]
|
||||||
BIN
tow-boat-addon/tow_boat_RP/textures/entity/tow_boat.png
Normal file
|
After Width: | Height: | Size: 571 B |
9
tow-boat-addon/tow_boat_RP/textures/item_texture.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"resource_pack_name": "tow_boat_RP",
|
||||||
|
"texture_name": "atlas.items",
|
||||||
|
"texture_data": {
|
||||||
|
"tow_boat": {
|
||||||
|
"textures": "textures/items/tow_boat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
tow-boat-addon/tow_boat_RP/textures/items/tow_boat.png
Normal file
|
After Width: | Height: | Size: 160 B |
@@ -4,11 +4,15 @@
|
|||||||
{"pack_id": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b50", "version": [1, 0, 0]},
|
{"pack_id": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b50", "version": [1, 0, 0]},
|
||||||
{"pack_id": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b80", "version": [1, 0, 0]},
|
{"pack_id": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b80", "version": [1, 0, 0]},
|
||||||
{"pack_id": "b4d72e91-8c43-4f1a-95d2-7e3b6c8a0d10", "version": [1, 0, 0]},
|
{"pack_id": "b4d72e91-8c43-4f1a-95d2-7e3b6c8a0d10", "version": [1, 0, 0]},
|
||||||
{"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b60", "version": [1, 0, 0]}
|
{"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b60", "version": [1, 0, 0]},
|
||||||
|
{"pack_id": "fac83943-16bc-4790-aa05-631894f59a03", "version": [1, 0, 0]},
|
||||||
|
{"pack_id": "a35d89fd-99e1-497a-9e78-35443c0cd59f", "version": [1, 0, 0]}
|
||||||
],
|
],
|
||||||
"resource_packs": [
|
"resource_packs": [
|
||||||
{"pack_id": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b53", "version": [1, 0, 1]},
|
{"pack_id": "9a3f8d2e-7c5b-4e1a-b9d2-1f6e3c4a8b53", "version": [1, 0, 1]},
|
||||||
{"pack_id": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b83", "version": [1, 0, 1]},
|
{"pack_id": "c8e51d72-9a4f-4b3e-b8c1-2f7d3e6a4b83", "version": [1, 0, 1]},
|
||||||
{"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b63", "version": [1, 0, 0]}
|
{"pack_id": "d7a4b9c1-2e3f-4a5b-8c6d-1f2e3d4c5b63", "version": [1, 0, 0]},
|
||||||
|
{"pack_id": "a18bdde1-53f8-49aa-b06d-6f0ec6c45b46", "version": [1, 0, 0]},
|
||||||
|
{"pack_id": "c6193eda-f160-475b-ab65-fca17741e41b", "version": [1, 0, 0]}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||