161 lines
7.5 KiB
JavaScript
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);
|