217 lines
10 KiB
JavaScript
217 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
// Two-creature leisure equipment. Seats are reserved independently and only
|
|
// produce play/friendship while both ends are occupied.
|
|
(function (global) {
|
|
const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => Number.isFinite(Number(v)) ? Number(v) : fallback);
|
|
const clamp = global.TarinaiCoreHelpers?.clampNumber || ((v, a, b) => Math.max(a, Math.min(b, Number(v) || 0)));
|
|
|
|
function ensure(item) {
|
|
if (!item) return null;
|
|
if (!Array.isArray(item.seesawSeatIds)) item.seesawSeatIds = ["", ""];
|
|
if (item.seesawSeatIds.length < 2) item.seesawSeatIds = [item.seesawSeatIds[0] || "", item.seesawSeatIds[1] || ""];
|
|
item.angle = 0;
|
|
item.seesawPhase = num(item.seesawPhase);
|
|
item.seesawTilt = num(item.seesawTilt);
|
|
return item;
|
|
}
|
|
|
|
function seatWorld(item, index = 0) {
|
|
ensure(item);
|
|
const side = index === 1 ? 1 : -1;
|
|
const reach = Math.max(48, num(item?.r, 60) * 0.94);
|
|
return { x: num(item?.x) + reach * side, y: num(item?.y) - 2, index: index === 1 ? 1 : 0 };
|
|
}
|
|
|
|
function actorById(worldRef, id) {
|
|
if (!id) return null;
|
|
return worldRef?.liveTarinaiById?.(id) || (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === id) || null;
|
|
}
|
|
|
|
function release(tarinai, item = null) {
|
|
const board = item || tarinai?.target;
|
|
if (board?.type === "seesaw") {
|
|
ensure(board);
|
|
for (let i = 0; i < 2; i += 1) if (board.seesawSeatIds[i] === tarinai?.id) board.seesawSeatIds[i] = "";
|
|
}
|
|
if (tarinai) {
|
|
tarinai.seesawItemId = "";
|
|
tarinai.seesawSeatIndex = -1;
|
|
tarinai.seesawWaitStartedAt = -Infinity;
|
|
tarinai.seesawPlayStartedAt = -Infinity;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function cleanReservations(item, worldRef) {
|
|
ensure(item);
|
|
for (let i = 0; i < 2; i += 1) {
|
|
const actor = actorById(worldRef, item.seesawSeatIds[i]);
|
|
if (!actor || actor.dead || actor.seesawItemId !== item.id || actor.seesawSeatIndex !== i || actor.state !== "play_seesaw") item.seesawSeatIds[i] = "";
|
|
}
|
|
}
|
|
|
|
function findAvailable(worldRef, tarinai, range = 560) {
|
|
let best = null;
|
|
let bestScore = Number(range) || 560;
|
|
const list = worldRef?.itemsOfType?.("seesaw") || (worldRef?.items || []).filter(item => item?.type === "seesaw");
|
|
for (const item of list) {
|
|
if (!item || item.dead) continue;
|
|
cleanReservations(item, worldRef);
|
|
for (let index = 0; index < 2; index += 1) {
|
|
const reserved = item.seesawSeatIds[index];
|
|
if (reserved && reserved !== tarinai?.id) continue;
|
|
const point = seatWorld(item, index);
|
|
const d = Math.hypot(num(tarinai?.x) - point.x, num(tarinai?.y) - point.y);
|
|
const hasPartner = Boolean(item.seesawSeatIds[1 - index]);
|
|
const score = d - (hasPartner ? 90 : 0);
|
|
if (score < bestScore) { bestScore = score; best = { item, seatIndex: index, point, hasPartner }; }
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function shouldSeek(worldRef, tarinai, needs = null) {
|
|
const found = findAvailable(worldRef, tarinai, 620);
|
|
if (!found) return false;
|
|
const fulfill = num(needs?.fulfill ?? tarinai?.needs?.fulfill);
|
|
const social = num(needs?.social ?? tarinai?.needs?.social);
|
|
if (found.hasPartner) return Math.max(fulfill, social) >= 8;
|
|
return fulfill >= 26 || social >= 40;
|
|
}
|
|
|
|
function actionPriority(worldRef, tarinai) {
|
|
const found = findAvailable(worldRef, tarinai, 620);
|
|
if (!found) return 0;
|
|
return found.hasPartner ? 74 : 34;
|
|
}
|
|
|
|
function isPlaying(tarinai, worldRef = tarinai?.world) {
|
|
const reservation = validReservation(tarinai, worldRef);
|
|
if (!reservation) return false;
|
|
const partnerId = reservation.item.seesawSeatIds[1 - reservation.index];
|
|
const partner = actorById(worldRef, partnerId);
|
|
if (!partner || partner.dead || partner.state !== "play_seesaw") return false;
|
|
const ownPoint = reservation.point;
|
|
const partnerPoint = seatWorld(reservation.item, 1 - reservation.index);
|
|
const ownArrived = Math.hypot(num(tarinai.x) - ownPoint.x, num(tarinai.y) - ownPoint.y) <= Math.max(28, num(tarinai.radius, 20) * 1.25);
|
|
const partnerArrived = Math.hypot(num(partner.x) - partnerPoint.x, num(partner.y) - partnerPoint.y) <= Math.max(28, num(partner.radius, 20) * 1.25);
|
|
return ownArrived && partnerArrived;
|
|
}
|
|
|
|
function reserve(item, tarinai, preferredIndex = null) {
|
|
if (!item || item.dead || item.type !== "seesaw" || !tarinai || tarinai.dead) return -1;
|
|
ensure(item);
|
|
const candidates = preferredIndex === 0 || preferredIndex === 1 ? [preferredIndex, 1 - preferredIndex] : [0, 1];
|
|
for (const index of candidates) {
|
|
if (!item.seesawSeatIds[index] || item.seesawSeatIds[index] === tarinai.id) {
|
|
item.seesawSeatIds[index] = tarinai.id;
|
|
tarinai.seesawItemId = item.id || "";
|
|
tarinai.seesawSeatIndex = index;
|
|
tarinai.seesawWaitStartedAt = num(tarinai.world?.time, 0);
|
|
tarinai.seesawPlayStartedAt = -Infinity;
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function start(tarinai, worldRef, item = null) {
|
|
const found = item ? { item, seatIndex: null } : findAvailable(worldRef, tarinai, 620);
|
|
const target = found?.item;
|
|
if (!target) return false;
|
|
cleanReservations(target, worldRef);
|
|
const index = reserve(target, tarinai, found.seatIndex);
|
|
if (index < 0) return false;
|
|
tarinai.setActionState?.("play_seesaw", { target, reason: "\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u307c\u3046\u3068\u3057\u3066\u3044\u308b", sleeping: false, wake: true });
|
|
tarinai.target = target;
|
|
tarinai.thought = "\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u307c\u3046\u3068\u3057\u3066\u3044\u308b";
|
|
return true;
|
|
}
|
|
|
|
function validReservation(tarinai, worldRef) {
|
|
if (!tarinai || tarinai.dead || tarinai.state !== "play_seesaw") return null;
|
|
const item = worldRef?.itemById?.(tarinai.seesawItemId) || (worldRef?.items || []).find(value => value && !value.dead && value.id === tarinai.seesawItemId);
|
|
if (!item || item.type !== "seesaw") return null;
|
|
cleanReservations(item, worldRef);
|
|
const index = tarinai.seesawSeatIndex === 1 ? 1 : 0;
|
|
if (item.seesawSeatIds[index] !== tarinai.id) return null;
|
|
return { item, index, point: seatWorld(item, index) };
|
|
}
|
|
|
|
function tickAction(tarinai, worldRef) {
|
|
const reservation = validReservation(tarinai, worldRef);
|
|
if (!reservation) return false;
|
|
const d = Math.hypot(num(tarinai.x) - reservation.point.x, num(tarinai.y) - reservation.point.y);
|
|
tarinai.thought = d > Math.max(22, num(tarinai.radius, 20)) ? "\u30b7\u30fc\u30bd\u30fc\u3078\u5411\u304b\u3063\u3066\u3044\u308b" : (reservation.item.seesawSeatIds[1 - reservation.index] ? "\u4e00\u7dd2\u306b\u30b7\u30fc\u30bd\u30fc\u3067\u904a\u3093\u3067\u3044\u308b" : "\u76f8\u624b\u3092\u5f85\u3063\u3066\u3044\u308b");
|
|
const now = num(worldRef?.time);
|
|
if (Number.isFinite(tarinai.seesawPlayStartedAt) && tarinai.seesawPlayStartedAt > -Infinity) {
|
|
if (now - tarinai.seesawPlayStartedAt >= 18) return "finished";
|
|
} else {
|
|
const dayLength = Math.max(1, num(worldRef?.config?.dayLength ?? global.CONFIG?.dayLength, 120));
|
|
if (now - num(tarinai.seesawWaitStartedAt, now) >= Math.max(30, dayLength * 0.5)) return "finished";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function updateWorld(worldRef, dt = 0.016) {
|
|
if (!worldRef?.items) return false;
|
|
const step = Math.max(0, Math.min(0.1, num(dt, 0.016)));
|
|
const seesaws = worldRef.itemsOfType?.("seesaw") || worldRef.items.filter(item => item?.type === "seesaw");
|
|
for (const item of seesaws) {
|
|
if (!item || item.dead) continue;
|
|
ensure(item); cleanReservations(item, worldRef);
|
|
const actors = [actorById(worldRef, item.seesawSeatIds[0]), actorById(worldRef, item.seesawSeatIds[1])];
|
|
const arrived = actors.map((actor, index) => {
|
|
if (!actor) return false;
|
|
const point = seatWorld(item, index);
|
|
return Math.hypot(num(actor.x) - point.x, num(actor.y) - point.y) <= Math.max(28, num(actor.radius, 20) * 1.25);
|
|
});
|
|
const playing = Boolean(actors[0] && actors[1] && arrived[0] && arrived[1]);
|
|
const pairKey = playing ? `${actors[0].id}|${actors[1].id}` : "";
|
|
if (pairKey && item.seesawPairKey !== pairKey) {
|
|
item.seesawPairKey = pairKey;
|
|
const now = num(worldRef.time);
|
|
actors[0].seesawPlayStartedAt = now;
|
|
actors[1].seesawPlayStartedAt = now;
|
|
} else if (!pairKey) item.seesawPairKey = "";
|
|
if (playing) item.seesawPhase += step * 4.6;
|
|
const targetTilt = playing ? Math.sin(item.seesawPhase) * 0.16 : 0;
|
|
item.seesawTilt += (targetTilt - item.seesawTilt) * Math.min(1, step * 8);
|
|
for (let index = 0; index < 2; index += 1) {
|
|
const actor = actors[index];
|
|
if (!actor || !arrived[index]) continue;
|
|
const base = seatWorld(item, index);
|
|
const side = index === 1 ? 1 : -1;
|
|
const lift = Math.sin(item.seesawPhase) * 9 * side;
|
|
actor.x = base.x;
|
|
actor.y = base.y - 12 + (playing ? lift : 0);
|
|
actor.vx = 0; actor.vy = 0;
|
|
const actionText = playing ? "\u30b7\u30fc\u30bd\u30fc\u3092\u3057\u3066\u3044\u308b\u3002" : "\u76f8\u624b\u3092\u5f85\u3063\u3066\u3044\u308b";
|
|
actor.thought = actionText;
|
|
if (playing && typeof global.setBehaviorText === "function") {
|
|
const behavior = global.currentTarinaiBehavior?.(actor);
|
|
if (behavior?.actionId === "play_seesaw" && behavior?.reason !== actionText) {
|
|
global.setBehaviorText(actor, { need: "fulfill", subNeed: "social_play", actionId: "play_seesaw", actionLabel: actionText, reasonText: actionText, target: item, phase: "perform", source: behavior?.source || "need" });
|
|
}
|
|
}
|
|
}
|
|
if (playing) {
|
|
const [a, b] = actors;
|
|
a.adjustRelation?.(b, step * 0.70, -step * 0.025, "seesaw_play");
|
|
b.adjustRelation?.(a, step * 0.70, -step * 0.025, "seesaw_play");
|
|
if (typeof global.applyNeedSatisfaction === "function") {
|
|
global.applyNeedSatisfaction(a, { fulfill: step * 1.4, social: step * 0.55 }, "seesaw");
|
|
global.applyNeedSatisfaction(b, { fulfill: step * 1.4, social: step * 0.55 }, "seesaw");
|
|
} else {
|
|
a.stress = clamp(num(a.stress) - step * 0.35, 0, 100);
|
|
b.stress = clamp(num(b.stress) - step * 0.35, 0, 100);
|
|
}
|
|
item.seesawPlayTime = num(item.seesawPlayTime) + step;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
global.TarinaiSeesawSystem = Object.freeze({ ensure, seatWorld, findAvailable, shouldSeek, actionPriority, isPlaying, reserve, release, start, tickAction, updateWorld, validReservation });
|
|
})(typeof window !== "undefined" ? window : globalThis);
|