You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
15 KiB
JavaScript

// features/seating.js the sit system (seat entity + per-furniture config).
import { world, system, isCreative, getDir, safeGetBlock } from "../core/mc.js";
import { DIR_YAW, DIR_TROT, FWD } from "../core/grid.js";
import { FLOWER_CART, LOUNGER } from "./mb_grid.js";
import { BR_FRONT, BR_BACK, BR_BACK_OFF, LO_FRONT, LO_BACK, LO_BACK_OFF } from "./mb_pairs.js";
import { onBreak } from "../core/events.js";
const SEAT = "rzb_dcs:seat";
const LAY_ANIM = "animation.rzb_dcs.lay_towel", LAY_TAG = "rzb_dcs:laying"; // lay-down pose for the towel front cell
const LOUNGE_ANIM = "animation.rzb_dcs.lounge_sit", LOUNGE_TAG = "rzb_dcs:lounging"; // reclined-25deg pose for the lounge chair
const TENT_LAY_ANIM = "animation.rzb_dcs.lay_tent", TENT_LAY_TAG = "rzb_dcs:laying_tent"; // COPY of lay_towel, kept SEPARATE so it can be customized without touching the towel
const TENT_SIT_ANIM = "animation.rzb_dcs.sit_tent", TENT_SIT_TAG = "rzb_dcs:sitting_tent"; // root scale only (0.82), composed ON TOP of the engine sit so the player still sits at 82% size
const CASKET_ANIM = "animation.rzb_dcs.casket_lay", CASKET_TAG = "rzb_dcs:laying_casket"; // laying casket pose (custom lay-down + player scale, authored in casket_lay.json)
const SITTABLE = {
};
//jacuzzi sit inset
const JC_SEAT = {
"rzb_dcs:jc_b_mid": { y: 0.325, fwd: -0.5, side: 0, face: 180 },
"rzb_dcs:jc_fr_mid": { y: 0.325, fwd: 0.5, side: 0, face: 0 },
"rzb_dcs:jc_mid_l": { y: 0.325, fwd: 0, side: -0.5, face: 90 },
"rzb_dcs:jc_mid_r": { y: 0.325, fwd: 0, side: 0.5, face: -90 },
"rzb_dcs:jc_b_l": { y: 0.325, fwd: -0.35, side: -0.35, face: 135 },
"rzb_dcs:jc_b_r": { y: 0.325, fwd: -0.35, side: 0.35, face: -135 },
"rzb_dcs:jc_fr_l": { y: 0.325, fwd: 0.35, side: -0.35, face: 45 },
"rzb_dcs:jc_fr_r": { y: 0.325, fwd: 0.35, side: 0.35, face: -45 },
};
// Picnic table
const PICNIC_SEAT = {
"rzb_dcs:sp38_fr_l": { y: 0.3125, fwd: 0.5125, side: 0.3, face: 90 }, // renders sp38_bl geo (down 3 + back 5; in toward center)
"rzb_dcs:sp38_bl": { y: 0.3125, fwd: -0.5125, side: 0.3, face: 90 }, // renders sp38_fr_l geo
"rzb_dcs:sp38_fr_r": { y: 0.3125, fwd: 0.5125, side: -0.3, face: -90 }, // renders sp38_br geo
"rzb_dcs:sp38_br": { y: 0.3125, fwd: -0.5125, side: -0.3, face: -90 }, // renders sp38_fr_r geo
};
function seatCfg(typeId) {
if (SITTABLE[typeId]) return SITTABLE[typeId];
if (JC_SEAT[typeId]) return JC_SEAT[typeId];
if (PICNIC_SEAT[typeId]) return PICNIC_SEAT[typeId];
if (typeId.startsWith("rzb_dcs:sp31_bl_") || typeId.startsWith("rzb_dcs:sp31_br_")) return { y: 0.3, fwd: 0, side: 0 };
if (typeId.startsWith("rzb_dcs:sp32_b_")) return { y: 0.2375, fwd: 0.125, side: 0 }; // rose rope swing seat
if (typeId === "rzb_dcs:sp30") return { y: -0.1, fwd: 0, side: 0 }; // picnic blanket (sit on the ground)
if (typeId.startsWith("rzb_dcs:tw_fr_")) return { y: -0.1, fwd: -0.8, side: 0, lay: true }; // beach towel FRONT cell -> lay down (shifted back)
if (typeId === "rzb_dcs:tent_fr_l") return { y: -0.0875, fwd: 0.125, side: -0.5, pose: { anim: TENT_SIT_ANIM, tag: TENT_SIT_TAG } }; // tent FRONT-left cell -> SIT scaled 82%
if (typeId === "rzb_dcs:tent_bl") return { y: -0.1625, fwd: -0.625, side: -0.3125, lay: true, sleep: true, layAnim: TENT_LAY_ANIM, layTag: TENT_LAY_TAG }; // tent BACK-left cell -> lay/sleep (acts as a BED at night)
if (typeId === "rzb_dcs:fa48") return { y: 0.1, fwd: 0.2, side: 0, lay: true, layAnim: CASKET_ANIM, layTag: CASKET_TAG }; // casket ground HEAD -> lay (centre -0.3 forward)
if (typeId === "rzb_dcs:fa48_b") return { y: 0.1, fwd: -0.8, side: 0, lay: true, layAnim: CASKET_ANIM, layTag: CASKET_TAG }; // casket ground FOOT -> lay (centre -0.3 forward)
if (typeId.startsWith("rzb_dcs:tw_b_")) return { y: -0.1625, fwd: 0, side: 0 }; // beach towel BACK cell -> sit (down 1px)
if (typeId.startsWith("rzb_dcs:lo_fr_")) return { y: 0.3, fwd: -0.25, side: 0, lounge: true }; // lounge chair FRONT cell (dead fallback; resolves to back)
if (typeId.startsWith("rzb_dcs:lo_b_")) return { y: 0.3, fwd: 0.29167, side: 0, lounge: true }; // lounge chair BACK cell (live seat; +fwd = back)
if (typeId.startsWith("rzb_dcs:lc_b_")) return { y: 0.875, fwd: 0, side: 0 }; // lifeguard chair base (elevated seat)
if (typeId.startsWith("rzb_dcs:lc_a_")) return { y: -0.125, fwd: 0, side: 0 }; // lifeguard awning: sit at the SAME spot as the base
if (typeId === "rzb_dcs:su03_0_0_1") return { y: 0.0, fwd: 0, side: 0 }; // pool lounger seat (middle cell)
if (typeId.startsWith("rzb_dcs:su02_")) return { y: 0.0, fwd: 0, side: 0 }; // pool tube: sit on LAND (in water it boards a boat)
if (typeId.startsWith("rzb_dcs:su08_")) return { y: 0.3375, fwd: 0, side: 0 }; // bamboo armchair
if (typeId.startsWith("rzb_dcs:su17_")) return { y: 0.4, fwd: 0, side: 0 }; // bamboo chair
if (typeId.startsWith("rzb_dcs:su23_")) return { y: 0.35, fwd: 0, side: 0 }; // camping chair
if (typeId.startsWith("rzb_dcs:su35_")) return { y: 0.3, fwd: 0, side: 0 }; // beach chair
if (typeId.startsWith("rzb_dcs:su22_")) return { y: 0.3, fwd: 0, side: 0 }; // beach stool
if (typeId.startsWith("rzb_dcs:br_b_")) return { y: 0.65, fwd: -0.78, side: 0, face: 0, bikeRack: true }; // bike rack: 2 saddles
if (typeId === "rzb_dcs:su20") return { y: -0.1, fwd: 0, side: 0 }; // inflatable pool
if (typeId.startsWith("rzb_dcs:su09_") || typeId.startsWith("rzb_dcs:su10_") || typeId.startsWith("rzb_dcs:su11_")) return { y: 0.3375, fwd: 0, side: 0 }; // bamboo couch
if (/^rzb_dcs:sp(39|40|41|42)_/.test(typeId)) return { y: 0.275, fwd: 0, side: 0 }; // spring couch set (armchair + couch L/M/R); lowered 1px from bamboo's 0.3375
if (/^rzb_dcs:fa(41|42|43|44)_/.test(typeId)) return { y: 0.275, fwd: 0, side: 0 }; // fall couch set (armchair + couch L/M/R), same seat as spring
if (/^rzb_dcs:wi(45|46|47|48)_/.test(typeId)) return { y: 0.275, fwd: 0, side: 0 }; // winter couch set (armchair + couch L/M/R)
if (typeId === "rzb_dcs:fc_3_0_0") return { y: 0.8375, fwd: 0.46, side: -0.2, face: 90 }; // flower cart bike saddle
return undefined;
}
// guard so the two interaction paths don't spawn two seats for one click
const recent = new Set();
// Active lay/lounge seats (seatId -> { seat, anim }); the per-tick pose loop re-fires only these instead of scanning entities.
const poseSeats = new Map();
// Tent SIT seats (seatId -> { seat, pid, dim, fx,fy,fz }); on dismount the rider is placed at a safe spot in FRONT of the tent.
const tentSit = new Map();
function safeFront(dim, x, y, z) {
for (const dy of [0, 1, -1, 2]) {
const feet = safeGetBlock(dim, { x, y: y + dy, z }), head = safeGetBlock(dim, { x, y: y + dy + 1, z });
if (feet && head && (feet.isAir || feet.isLiquid) && (head.isAir || head.isLiquid)) return { x: x + 0.5, y: y + dy, z: z + 0.5 };
}
return null;
}
export function sit(player, block) {
if (!player || !block) return;
let dir = getDir(block.permutation);
let typeId = block.typeId, bx = block.x, by = block.y, bz = block.z;
const fcCell = FLOWER_CART.parse(typeId);
if (fcCell) { const a = FLOWER_CART.anchorFrom(fcCell, bx, by, bz, dir); bx = a.x; by = a.y; bz = a.z; typeId = "rzb_dcs:fc_3_0_0"; }
const lgCell = LOUNGER.parse(typeId);
if (lgCell) { const a = LOUNGER.anchorFrom(lgCell, bx, by, bz, dir); const s = LOUNGER.cellWorld("0_0_1", a.x, a.y, a.z, dir); bx = s.x; by = s.y; bz = s.z; typeId = "rzb_dcs:su03_0_0_1"; }
if (typeId.startsWith(BR_FRONT)) { const off = BR_BACK_OFF[dir] || [0, 0, 1]; bx += off[0]; by += off[1]; bz += off[2]; typeId = BR_BACK + typeId.slice(BR_FRONT.length); }
if (typeId.startsWith(LO_FRONT)) { const off = LO_BACK_OFF[dir] || [0, 0, -1]; bx += off[0]; by += off[1]; bz += off[2]; typeId = LO_BACK + typeId.slice(LO_FRONT.length); }
const cfg = seatCfg(typeId);
if (cfg === undefined) return;
if (cfg.bikeRack) { // two saddles: pick left (-X) or right (+X) bike by which side the player is on
const t = (DIR_TROT[dir] ?? 0) * Math.PI / 180, c = Math.cos(t), s = Math.sin(t);
const wx = player.location.x - (bx + 0.5), wz = player.location.z - (bz + 0.5);
cfg.side = (wx * c - wz * s) < 0 ? -5 / 16 : 4 / 16;
}
if (player.isSneaking) return;
if (recent.has(player.id)) return;
recent.add(player.id);
system.runTimeout(() => recent.delete(player.id), 5);
const yaw = (cfg.face !== undefined) ? (cfg.face - (DIR_TROT[dir] ?? 0)) : (DIR_YAW[dir] ?? 0);
const trot = (DIR_TROT[dir] ?? 0) * Math.PI / 180, ct = Math.cos(trot), st = Math.sin(trot);
const fwd = cfg.fwd ?? 0, side = cfg.side ?? 0;
const dx = side * ct + fwd * st, dz = -side * st + fwd * ct;
const dim = block.dimension;
const seatPos = { x: bx + 0.5 + dx, y: by + (cfg.y ?? 0.4), z: bz + 0.5 + dz };
const near = dim.getEntities({ type: SEAT, location: seatPos, maxDistance: 0.6 });
for (const s of near) { const rd = s.getComponent("minecraft:rideable"); if (rd && rd.getRiders().length > 0) return; s.remove(); }
const seat = dim.spawnEntity(SEAT, seatPos);
try { seat.setRotation({ x: 0, y: yaw }); } catch {}
const rideable = seat.getComponent("minecraft:rideable");
if (!rideable) { seat.remove(); return; }
rideable.addRider(player);
if (cfg.lay) { const tag = cfg.layTag || LAY_TAG, anim = cfg.layAnim || LAY_ANIM; try { seat.addTag(tag); } catch {} try { player.playAnimation(anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} try { poseSeats.set(seat.id, { seat, anim }); } catch {} }
if (cfg.lounge) { try { seat.addTag(LOUNGE_TAG); } catch {} try { player.playAnimation(LOUNGE_ANIM, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} try { poseSeats.set(seat.id, { seat, anim: LOUNGE_ANIM }); } catch {} }
if (cfg.pose) { try { seat.addTag(cfg.pose.tag); } catch {} try { player.playAnimation(cfg.pose.anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} try { poseSeats.set(seat.id, { seat, anim: cfg.pose.anim }); } catch {} } // generic custom pose (e.g. tent scaled sit)
if (cfg.sleep) {
try { const f = FWD[dir] || FWD.south, sp = safeFront(dim, bx - 2 * f[0], by, bz - 2 * f[2]) || { x: bx - 2 * f[0] + 0.5, y: by, z: bz - 2 * f[2] + 0.5 }; player.setSpawnPoint({ dimension: dim, x: sp.x, y: sp.y, z: sp.z }); } catch {}
sleepSkip(seat, player, dim, bx, by, bz, dir);
}
if (typeId === "rzb_dcs:tent_fr_l") { const f = FWD[dir] || FWD.south; tentSit.set(seat.id, { seat, pid: player.id, dim, fx: bx - f[0], fy: by, fz: bz - f[2] }); } // sit dismount -> teleport to a safe spot in FRONT of the tent
}
system.runInterval(() => {
for (const [sid, info] of tentSit) {
let riders; try { riders = info.seat.getComponent("minecraft:rideable")?.getRiders(); } catch { tentSit.delete(sid); continue; } // seat already gone
if (riders && riders.length) continue; // still seated
const p = world.getAllPlayers().find((pl) => pl.id === info.pid);
if (p) { const pos = safeFront(info.dim, info.fx, info.fy, info.fz); if (pos) { try { p.teleport(pos, { dimension: info.dim }); } catch {} } }
try { info.seat.remove(); } catch {}
tentSit.delete(sid); poseSeats.delete(sid);
}
}, 1);
function sleepSkip(seat, player, dim, bx, by, bz, dir) {
system.runTimeout(() => {
try {
const riders = seat.getComponent("minecraft:rideable")?.getRiders() || [];
if (!riders.some((r) => r?.id === player.id)) return; // got up before falling asleep
const t = world.getTimeOfDay();
if (t < 12542 || t > 23458) return; // daytime -> don't skip, keep lying there
try { world.setTimeOfDay(0); } catch {}
try { seat.remove(); } catch {} // end the lay pose
const f = FWD[dir] || FWD.south, sitB = safeGetBlock(dim, { x: bx - f[0], y: by, z: bz - f[2] }); // the paired sit cell
// sit cell FREE -> sit up there; OCCUPIED -> stand up OFF the tent (behind the bed).
let occupied = false;
try { for (const e of dim.getEntities({ type: SEAT, location: { x: bx - f[0] + 0.5, y: by + 0.5, z: bz - f[2] + 0.5 }, maxDistance: 1.4 })) { if ((e.getComponent("minecraft:rideable")?.getRiders() || []).length) { occupied = true; break; } } } catch {}
if (sitB && sitB.typeId === "rzb_dcs:tent_fr_l" && !occupied) system.runTimeout(() => { try { sit(player, sitB); } catch {} }, 2); // sit up (delay so the lay seat is gone)
else system.runTimeout(() => { const pos = safeFront(dim, bx - 2 * f[0], by, bz - 2 * f[2]); if (pos) { try { player.teleport(pos, { dimension: dim }); } catch {} } }, 2); // sit cell taken -> stand up in FRONT of the tent
} catch {}
}, 60);
}
system.beforeEvents.startup.subscribe((init) => {
init.blockComponentRegistry.registerCustomComponent("rzb_dcs:sit", { onPlayerInteract(e) { sit(e.player, e.block); } });
});
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id || "";
if (!/^rzb_dcs:tent_(fr_l|fr_r|bl|br)$/.test(id)) return;
const b = ev.block;
for (const p of world.getAllPlayers()) {
try {
const sp = p.getSpawnPoint();
if (sp && Math.abs(Math.floor(sp.x) - b.x) <= 3 && Math.abs(Math.floor(sp.z) - b.z) <= 3 && Math.abs(Math.floor(sp.y) - b.y) <= 2) { try { p.setSpawnPoint(undefined); } catch {} } // spawn sits ~1-3 blocks in FRONT of the tent -> wide footprint tolerance
} catch {}
}
});
// Despawn empty seats (player stood up, logged off, etc.).
system.runInterval(() => {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
for (const seat of dim.getEntities({ type: SEAT })) {
const rideable = seat.getComponent("minecraft:rideable");
const riders = rideable ? rideable.getRiders() : [];
if (!riders || riders.length === 0) seat.remove();
}
}
}, 20);
const SEAT_POSES = [{ tag: LAY_TAG, anim: LAY_ANIM }, { tag: LOUNGE_TAG, anim: LOUNGE_ANIM }, { tag: TENT_LAY_TAG, anim: TENT_LAY_ANIM }, { tag: TENT_SIT_TAG, anim: TENT_SIT_ANIM }, { tag: CASKET_TAG, anim: CASKET_ANIM }];
// Re-register any tagged pose-seat that isn't tracked (reload, or a transient 0-rider tick dropped it from poseSeats).
// Runs on load AND periodically, so a lost pose self-heals instead of falling back to the vanilla sit forever.
function syncPoseSeats() {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
for (const pose of SEAT_POSES) {
let seats; try { seats = dim.getEntities({ type: SEAT, tags: [pose.tag] }); } catch { continue; }
for (const seat of seats) { if (!poseSeats.has(seat.id)) { try { poseSeats.set(seat.id, { seat, anim: pose.anim }); } catch {} } }
}
}
}
system.run(syncPoseSeats);
system.runInterval(syncPoseSeats, 20);
system.runInterval(() => {
if (poseSeats.size === 0) return;
for (const [id, e] of poseSeats) {
if (!e.seat || !e.seat.isValid) { poseSeats.delete(id); continue; } // only stop tracking once the seat is actually removed (despawn loop)
let riders; try { riders = e.seat.getComponent("minecraft:rideable")?.getRiders() ?? []; } catch { continue; } // transient read hiccup: keep the pose, retry next tick (don't drop -> that was the 1-tick vanilla-sit flash)
for (const r of riders) { try { r.playAnimation(e.anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} }
}
}, 1);