"use strict"; // Direct player-to-Tarinai feeding. Placement clicks and pinched-item drops // share this path so item effects, feedback, and consumption stay consistent. (function (global) { const FOOD_TOOL_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice"]); const MEDICINE_TOOL_TYPES = new Set(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]); const HEART_FOOD_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunda_juice"]); function itemType(itemOrType = "") { return String(typeof itemOrType === "object" ? itemOrType?.type || "" : itemOrType || ""); } function isDirectFeedType(itemOrType = "") { const type = itemType(itemOrType); return FOOD_TOOL_TYPES.has(type) || MEDICINE_TOOL_TYPES.has(type); } function isFoodGift(itemOrType = "") { return HEART_FOOD_TYPES.has(itemType(itemOrType)); } function hasConsumableAmount(item = null) { if (!item || item.dead || !isDirectFeedType(item)) return false; const type = itemType(item); if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) { if (Number.isFinite(Number(item.foodServingsRemaining))) return Number(item.foodServingsRemaining) > 0; return Number(item.amount ?? 1) > 0; } return Number(item.amount ?? 1) > 0; } function findTargetAt(worldRef, x, y) { if (!worldRef || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) return null; const px = Number(x); const py = Number(y); const list = worldRef.tarinai || []; for (let i = list.length - 1; i >= 0; i -= 1) { const t = list[i]; if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue; const hit = typeof t.contains === "function" ? t.contains(px, py) : (typeof global.distXY === "function" && global.distXY(px, py, t.x, t.y) <= Math.max(20, Number(t.radius || 22) * 1.35)); if (hit) return t; } return null; } function roleFor(item = null) { const type = itemType(item); if (type === "water") return "drink"; if (MEDICINE_TOOL_TYPES.has(type) || type === "zunda_juice") return "medicine"; return "food"; } function randomRange(min, max) { return min + Math.random() * (max - min); } function spawnFoodGiftHearts(tarinai, nutrition = 0) { const worldRef = tarinai?.world; if (!worldRef?.effects || typeof global.TarinaiEffect !== "function") return false; const count = 10 + Math.min(4, Math.max(0, Math.round((Number(nutrition) || 0) / 7))); const radius = Math.max(18, Number(tarinai.radius || 22) || 22); for (let i = 0; i < count; i += 1) { const side = i % 2 ? 1 : -1; worldRef.queueEffect?.("heart", tarinai.x + side * randomRange(radius * 0.06, radius * 0.82), tarinai.y - randomRange(radius * 0.38, radius * 1.72), { vx: side * randomRange(9, 30) + randomRange(-9, 9), vy: randomRange(-76, -30), size: randomRange(9, 17), life: randomRange(0.82, 1.36), color: "rgba(240,91,135,0.94)", }); } worldRef.queueEffect?.("ring", tarinai.x, tarinai.y, { size: Math.max(24, radius * 1.35), life: 0.28, color: "rgba(240,91,135,0.42)", }); return true; } function applyFoodGiftPersonality(tarinai, nutrition = 0) { if (!tarinai || typeof tarinai.adjustPersonality !== "function") return false; const amount = Math.max(0, Number(nutrition) || 0); // A single gift intentionally outweighs ordinary pointer contact, which // changes openness gradually per second while the cursor remains nearby. const openness = 0.018 + Math.min(0.010, amount * 0.0004); const sociability = 0.010 + Math.min(0.006, amount * 0.00024); const changedOpen = tarinai.adjustPersonality("openness", openness, "\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f") || 0; const changedSocial = tarinai.adjustPersonality("sociability", sociability, "\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f") || 0; return Math.abs(changedOpen) + Math.abs(changedSocial) > 0; } function removeGivenWorldItem(worldRef, item, label) { if (!worldRef || !item || !(worldRef.items || []).includes(item)) return false; if (global.TarinaiStructureLifecycle?.deleteItem) { return Boolean(global.TarinaiStructureLifecycle.deleteItem(worldRef, item, { reason: "direct-feed-consumed", userReason: `${label}\u304c\u76f4\u63a5\u4e0e\u3048\u3089\u308c\u305f`, wake: false, panicOwner: false, })); } item.amount = 0; item.dead = true; worldRef.markItemBucketsDirty?.("direct-feed-consumed"); worldRef.markSpatialDirty?.("direct-feed-consumed"); return true; } function give(worldRef, tarinai, item, options = {}) { if (!worldRef || !tarinai || tarinai.dead || !hasConsumableAmount(item)) return null; if (typeof global.consumeBehaviorTarget !== "function") return null; const type = itemType(item); if (tarinai.isZunchiSlave && type === "sweet") return null; const label = typeof global.toolLabel === "function" ? global.toolLabel(type) : type; const healthNeedBefore = Number(tarinai.needs?.health); const consumed = global.consumeBehaviorTarget(tarinai, worldRef, item, roleFor(item), 0.18); if (!consumed) return null; const nutrition = global.TarinaiItemRegistry?.food?.nutrition?.(type, 0) ?? 0; tarinai.target = null; tarinai.targetKey = ""; tarinai.sleeping = false; tarinai.goodMode = "smile"; tarinai.moodLiftTimer = Math.max(Number(tarinai.moodLiftTimer || 0) || 0, 1.2); tarinai.foodReactTimer = Math.max(Number(tarinai.foodReactTimer || 0) || 0, 0.55); tarinai.surpriseTimer = Math.max(Number(tarinai.surpriseTimer || 0) || 0, 0.28); tarinai.thought = `${label}\u3092\u3082\u3089\u3063\u305f`; const foodGift = isFoodGift(type); if (foodGift) { tarinai.affection = Number(tarinai.affection || 0) + 1.8 + Math.max(0, Number(nutrition) || 0) * 0.035; applyFoodGiftPersonality(tarinai, nutrition); // Cosmetic feedback must never interrupt consumption, removal, or achievement recording. try { spawnFoodGiftHearts(tarinai, nutrition); } catch (_) {} try { tarinai.bubble?.("\u2661", 2.2, "rgba(190,82,132,0.80)"); } catch (_) {} global.TarinaiAchievements?.recordDirectFeed?.({ world: worldRef, tarinai, item, type, source: options.source || "direct" }); } else { tarinai.focusPulseTimer = Math.max(Number(tarinai.focusPulseTimer || 0) || 0, 0.72); const healthNeedAfter = Number(tarinai.needs?.health); const beneficialRecovery = Number.isFinite(healthNeedBefore) && Number.isFinite(healthNeedAfter) && healthNeedAfter < healthNeedBefore - 0.0001; global.TarinaiAchievements?.recordDirectCare?.({ world: worldRef, tarinai, item, type, beneficialRecovery, source: options.source || "direct" }); } // Direct feeding is a player operation and is intentionally not logged. const removed = options.removeItem !== false ? removeGivenWorldItem(worldRef, item, label) : false; if (!removed && (worldRef.items || []).includes(item)) { item.vx = 0; item.vy = 0; item.prevX = item.x; item.prevY = item.y; } worldRef.hoverGiveTarget = null; worldRef.drawListDirty = true; worldRef.recordFamily?.(tarinai); if (global.uiCache) global.uiCache.selectedSnapshot = ""; return { tarinai, item, type, label, foodGift, removed }; } global.TarinaiDirectFeedingSystem = Object.freeze({ isDirectFeedType, isFoodGift, hasConsumableAmount, findTargetAt, give, }); })(typeof window !== "undefined" ? window : globalThis);