tarinai/js/seesaw_system.js
2026-07-16 22:12:03 +09:00

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);