feat: A-frame tent + portal walk-through field + texture polish
All checks were successful
Deploy Addons / deploy (push) Successful in 14s
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:
@@ -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 ──────────────────────────────────────────────
|
||||
const TENT_ITEM = "silverlabs:tent";
|
||||
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 STATE_PROP = "camping_state_v1";
|
||||
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 ──────────────────────────────────────────────────
|
||||
// tents: key = "ox,oy,oz,dim" -> { ownerId, ownerName, cells: [[x,y,z]...] }
|
||||
@@ -51,6 +58,15 @@ function cardinalFacing(yaw) {
|
||||
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) {
|
||||
switch (facing) {
|
||||
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 = [];
|
||||
for (let w = 0; w < 2; w++) {
|
||||
// Back wall at l=0 (Y=0 and Y=1)
|
||||
canvasCells.push({ x: ox + w * rx, y: oy + 0, z: oz + w * rz });
|
||||
canvasCells.push({ x: ox + w * rx, y: oy + 1, z: oz + w * rz });
|
||||
// Roof at l=1 and l=2 (Y=1)
|
||||
canvasCells.push({ x: ox + 1 * fx + w * rx, y: oy + 1, z: oz + 1 * fz + w * rz });
|
||||
canvasCells.push({ x: ox + 2 * fx + w * rx, y: oy + 1, z: oz + 2 * fz + w * rz });
|
||||
for (let l = 0; l < 3; l++) {
|
||||
canvasCells.push({
|
||||
x: ox + l * fx,
|
||||
y: oy,
|
||||
z: oz + l * fz,
|
||||
block: TENT_PANEL_L,
|
||||
});
|
||||
canvasCells.push({
|
||||
x: ox + l * fx + rx,
|
||||
y: oy,
|
||||
z: oz + l * fz + rz,
|
||||
block: TENT_PANEL_R,
|
||||
});
|
||||
}
|
||||
|
||||
for (const c of canvasCells) {
|
||||
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 (_) {}
|
||||
}
|
||||
|
||||
@@ -169,6 +198,7 @@ function tryPlaceTent(player) {
|
||||
state.tents[key] = {
|
||||
ownerId: player.id,
|
||||
ownerName: player.name,
|
||||
facing,
|
||||
cells: canvasCells.map((c) => [c.x, c.y, c.z]),
|
||||
};
|
||||
saveState();
|
||||
@@ -297,10 +327,12 @@ try {
|
||||
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (!block) return;
|
||||
if (block.typeId === TENT_BLOCK) {
|
||||
if (TENT_BLOCK_IDS.includes(block.typeId)) {
|
||||
event.cancel = true;
|
||||
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) {
|
||||
event.cancel = true;
|
||||
const player = event.player;
|
||||
@@ -312,18 +344,182 @@ try {
|
||||
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();
|
||||
if (tod < 12500 && tod > 500) {
|
||||
if (!isNight(tod)) {
|
||||
player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off.");
|
||||
return;
|
||||
}
|
||||
player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
|
||||
player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
|
||||
world.setTimeOfDay(0);
|
||||
player.sendMessage("§a[Camping] §7You rest until dawn. §8Spawn point unchanged.");
|
||||
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("saturation", 40, { amplifier: 0, showParticles: false });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
function toggleHammock(player, loc) {
|
||||
@@ -437,12 +633,13 @@ try {
|
||||
const block = event.block;
|
||||
if (!block) return;
|
||||
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;
|
||||
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
||||
const dimId = block.dimension.id;
|
||||
const player = event.player;
|
||||
if (id === TENT_BLOCK) {
|
||||
if (isTent) {
|
||||
system.run(() => dismantleTentAt(loc, dimId, player));
|
||||
} else {
|
||||
system.run(() => dismantleHammockAt(loc, dimId, player));
|
||||
|
||||
Reference in New Issue
Block a user