"use strict"; // Layer: entity-runtime/items/dynamic-system // Owns duplicator item loading/reloading behavior. Direct stored-type helpers // remain exposed from TarinaiDuplicatorRuntime for UI/tool commands and // save/restore compatibility. (function (global) { function support() { return global.TarinaiItemLifecycleRuntimeSupport || {}; } function update(item, dt, worldRef) { if (!item || item.dead || item.type !== "duplicator") return false; const s = support(); const syncRoles = s.syncDuplicatorRoles || global.TarinaiDuplicatorRuntime?.syncRoles; const applyStoredPin = s.duplicatorApplyStoredPin; const canLoadNow = s.duplicatorCanLoadNow; const loadPointFor = s.duplicatorLoadPoint || global.TarinaiDuplicatorRuntime?.loadPoint; const loadTypeForItem = s.duplicatorLoadTypeForItem || global.TarinaiDuplicatorRuntime?.loadTypeForItem; const candidateReach = s.duplicatorCandidateReach; const setStoredType = s.duplicatorSetStoredType || global.TarinaiDuplicatorRuntime?.setStoredType; if (typeof syncRoles !== "function" || typeof loadPointFor !== "function" || typeof loadTypeForItem !== "function" || typeof setStoredType !== "function") return false; item.amount = 999; syncRoles(item); if (typeof applyStoredPin === "function") applyStoredPin(item, worldRef); item.duplicatorPinFlash = Math.max(0, (item.duplicatorPinFlash || 0) - dt); if (!worldRef?.nearbyItems) return true; if (typeof canLoadNow === "function" && !canLoadNow(item, worldRef)) { item._duplicatorCandidateKey = ""; item._duplicatorCandidateSince = 0; return true; } const loadPoint = loadPointFor(item); const pickupRadius = Math.max(68, (item.r || 34) + 44); const candidates = Array.from(worldRef.nearbyItems(item.x, item.y, pickupRadius, true) || []); // SpatialGrid の分類漏れを補う。ただし、複製機からかなり近いものだけに限定し、 // 移動中の複製機が周辺アイテムを吸い込む挙動を避ける。 if (worldRef.items) { for (const candidate of worldRef.items) { if (!candidate || candidates.includes(candidate) || candidate === item || candidate.dead) continue; const approxReach = pickupRadius + Math.max(16, candidate.r || 12); if (distXY(item.x, item.y, candidate.x, candidate.y) <= approxReach) candidates.push(candidate); } } let best = null, bestType = "", bestSlotD = Infinity, bestScore = Infinity; for (const it of candidates) { if (!it || it === item || it.dead || it.playerHeld || it._heldByPlayer) continue; const loadType = loadTypeForItem(it); if (!loadType) continue; if ((it.foodServingsRemaining ?? it.amount ?? 0) <= 0) continue; const slotD = distXY(loadPoint.x, loadPoint.y, it.x, it.y); const centerD = distXY(item.x, item.y, it.x, it.y); const slotReach = typeof candidateReach === "function" ? candidateReach(it) : Math.max(20, Math.min(36, (it?.r || 12) * 0.82 + 13)); const bodyReach = Math.max(22, (item.r || 34) * 0.46 + (it.r || 12) * 0.36); if (slotD > slotReach && centerD > bodyReach) continue; // 基本は投入口に近いものを優先。同距離なら置かれたばかりのものを優先し、 // 複数候補があるときに意図しない古いアイテムへ寄りにくくする。 const score = slotD + Math.max(0, Number(it.age || 0)) * 0.012; if (score < bestScore) { best = it; bestType = loadType; bestSlotD = slotD; bestScore = score; } } if (!best || !bestType) { item._duplicatorCandidateKey = ""; item._duplicatorCandidateSince = 0; return true; } const candidateKey = `${best.id || ""}:${bestType}`; const now = Number(worldRef.time || 0); if (item._duplicatorCandidateKey !== candidateKey) { item._duplicatorCandidateKey = candidateKey; item._duplicatorCandidateSince = now; return true; } const dwell = item.storedFoodType ? (s.DUPLICATOR_REPLACE_DWELL_SECONDS ?? 0.70) : (s.DUPLICATOR_LOAD_DWELL_SECONDS ?? 0.38); if (now - Number(item._duplicatorCandidateSince || 0) < dwell) return true; const changed = item.storedFoodType !== bestType; if (!changed && item.storedFoodType) { item._duplicatorCandidateKey = ""; item._duplicatorCandidateSince = 0; return true; } setStoredType(item, bestType, worldRef, { consumeItem: best, direct: false, force: false, log: false, emit: false }); // Item.dead is a getter based on amount. Do not assign to it. worldRef.markItemBucketsDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded"); worldRef.markSpatialDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded"); worldRef.markTerrainDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded"); worldRef.emit?.("duplicator:loaded", { duplicator: item, foodType: item.storedFoodType, replaced: changed, slotDistance: bestSlotD }); worldRef.log?.(`複製機に${item.storedFoodLabel}をセットした。`, typeof isPinType === "function" && isPinType(item.storedFoodType) ? "observe" : "food"); return true; } global.TarinaiItemDynamicDuplicatorSystem = Object.freeze({ update }); })(typeof window !== "undefined" ? window : globalThis);