tarinai/js/item_lifecycle_support.js
2026-06-26 19:04:32 +09:00

161 lines
7.5 KiB
JavaScript

"use strict";
// Layer: entity-runtime/items/support
// Owns shared lifecycle support helpers used by dynamic item systems and
// placement/tool commands. The runtime imports these via
// globals instead of carrying helper implementations inline.
(function (global) {
const DUPLICATOR_STORABLE_TYPES = new Set([
"food", "sweet", "love_mochi", "fight_mochi", "sleep_drug",
"laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug",
"zunda_juice", "grass", "water", "pushpin", "oshibyo"
]);
function duplicatorLoadTypeForItem(item) {
if (!item || item.dead || item.type === "duplicator") return "";
const type = String(item.type || "");
if (!type || type === "water_bowl") return "";
if ((typeof isPinType === "function" && isPinType(type)) && item.pinState === "lodged") return "";
// Explicit list first. これで「ずんだ餅」「へこ餅」「けんか餅」が
// serving-food 判定や medicine role の揺れに左右されず複製機へ入る。
if (DUPLICATOR_STORABLE_TYPES.has(type)) return type;
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return type;
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return type;
const foodDef = global.TarinaiItemRegistry?.food?.get?.(type);
if (foodDef && (Number(foodDef.nutrition || 0) > 0 || foodDef.effectId)) return type;
if (typeof roleMatches === "function" && roleMatches(item, "medicine") && type !== "water_bowl") return type;
return "";
}
function itemAngleForRuntime(item) {
return typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0);
}
const DUPLICATOR_LOAD_DWELL_SECONDS = 0.38;
const DUPLICATOR_REPLACE_DWELL_SECONDS = 0.70;
function duplicatorLoadPoint(duplicator) {
const r = duplicator?.r || 34;
return { x: (duplicator?.x || 0) + r * 0.62, y: (duplicator?.y || 0) + r * 0.24 };
}
function duplicatorCandidateReach(item) {
return Math.max(20, Math.min(36, (item?.r || 12) * 0.82 + 13));
}
function duplicatorCanLoadNow(duplicator, worldRef) {
if (!duplicator || duplicator.type !== "duplicator") return false;
if (duplicator.playerHeld || duplicator._heldByPlayer) return false;
const now = Number(worldRef?.time || 0);
if (now < Number(duplicator.duplicatorLoadSuppressedUntil || 0)) return false;
if (Math.hypot(duplicator.vx || 0, duplicator.vy || 0) > 24) return false;
return true;
}
function syncDuplicatorRoles(item) {
if (!item || item.type !== "duplicator") return;
const type = String(item.storedFoodType || "");
const pinLoaded = Boolean(type && typeof isPinType === "function" && isPinType(type));
item.roles.food = Boolean(type && !pinLoaded);
item.roles.medicine = Boolean(type && !pinLoaded && (type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))));
item.roles.drink = Boolean(!pinLoaded && (type === "water" || type === "zunda_juice"));
item.roles.danger = pinLoaded;
}
function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, options = {}) {
if (!duplicator || duplicator.type !== "duplicator") return false;
const type = String(storedType || "");
if (!type) return false;
const probe = { type, amount: 1, foodServingsRemaining: 1, dead: false };
const loadType = duplicatorLoadTypeForItem(probe);
if (!loadType) return false;
const changed = duplicator.storedFoodType !== loadType;
if (!changed && duplicator.storedFoodType && !options.force) return true;
const def = typeof itemDefinition === "function" ? itemDefinition(loadType) : null;
const newLabel = def?.label || (typeof toolLabel === "function" ? toolLabel(loadType) : loadType);
duplicator.storedFoodType = loadType;
duplicator.storedFoodLabel = newLabel;
syncDuplicatorRoles(duplicator);
duplicator.loadedAt = Number(worldRef?.time || 0);
duplicator._duplicatorCandidateKey = "";
duplicator._duplicatorCandidateSince = 0;
duplicator.duplicatorPinFlash = typeof isPinType === "function" && isPinType(loadType) ? 0.28 : (duplicator.duplicatorPinFlash || 0);
const consume = options.consumeItem;
if (consume && typeof consume === "object") {
consume.amount = 0;
consume.foodServingsRemaining = 0;
}
worldRef?.markItemBucketsDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
worldRef?.markSpatialDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
worldRef?.markTerrainDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
if (options.emit !== false) worldRef?.emit?.("duplicator:loaded", { duplicator, foodType: loadType, replaced: changed, direct: Boolean(options.direct) });
if (options.log !== false) worldRef?.log?.(`複製機に${duplicator.storedFoodLabel}をセットした。`, typeof isPinType === "function" && isPinType(loadType) ? "observe" : "food");
return true;
}
global.TarinaiDuplicatorRuntime = Object.freeze({
loadTypeForItem: duplicatorLoadTypeForItem,
loadPoint: duplicatorLoadPoint,
setStoredType: duplicatorSetStoredType,
syncRoles: syncDuplicatorRoles,
});
function duplicatorApplyStoredPin(duplicator, worldRef) {
const storedType = String(duplicator?.storedFoodType || "");
if (!duplicator || duplicator.type !== "duplicator" || !(typeof isPinType === "function" && isPinType(storedType))) return false;
const now = Number(worldRef?.time || 0);
if (now < Number(duplicator.nextDuplicatedPinAt || 0)) return false;
const dish = duplicatorLoadPoint(duplicator);
const reach = Math.max(24, (duplicator.r || 34) * 0.56);
const candidates = worldRef?.nearbyTarinai?.(dish.x, dish.y, reach + 72, true) || [];
for (const t of candidates) {
if (!t || t.dead || worldRef?.isTarinaiHiddenInNestBox?.(t)) continue;
if (t.stuckPushpinId || t.currentLodgedPin?.()) continue;
const hit = reach + Math.max(10, (t.radius || 18) * 0.78);
if (distXY(dish.x, dish.y, t.x, t.y) > hit) continue;
const pin = new Item(storedType, dish.x, dish.y);
pin.amount = 999;
pin.noStickUntil = 0;
pin.noStickTargetId = "";
pin.noStickTargetUntil = 0;
pin.duplicatedFromId = duplicator.id || "";
const added = worldRef?.addItem?.(pin, "duplicator-pin", { terrain: false }) || null;
if (!added && worldRef?.items) {
pin.world = worldRef;
worldRef.items.push(pin);
worldRef.markItemBucketsDirty?.("duplicator-pin");
worldRef.markSpatialDirty?.("duplicator-pin");
}
const ok = pin.attachPushpin?.(t, worldRef, distXY(dish.x, dish.y, t.x, t.y));
if (!ok) {
pin.amount = 0;
continue;
}
duplicator.usedCount = (duplicator.usedCount || 0) + 1;
duplicator.nextDuplicatedPinAt = now + 0.82;
duplicator.duplicatorPinFlash = 0.24;
worldRef.effects?.push(new Effect("ring", dish.x, dish.y, { size: Math.max(15, (duplicator.r || 34) * 0.62), life: 0.18, color: storedType === "oshibyo" ? "rgba(73,119,205,0.40)" : "rgba(214,72,72,0.42)" }));
worldRef.drawListDirty = true;
worldRef.markSpatialDirty?.("duplicator-pin-lodged");
return true;
}
return false;
}
global.TarinaiItemLifecycleRuntimeSupport = Object.freeze({
itemAngleForRuntime,
duplicatorLoadTypeForItem,
duplicatorLoadPoint,
duplicatorCandidateReach,
duplicatorCanLoadNow,
syncDuplicatorRoles,
duplicatorSetStoredType,
duplicatorApplyStoredPin,
DUPLICATOR_LOAD_DWELL_SECONDS,
DUPLICATOR_REPLACE_DWELL_SECONDS,
});
})(typeof window !== "undefined" ? window : globalThis);