167 lines
7.7 KiB
JavaScript
167 lines
7.7 KiB
JavaScript
"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 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.effects.push(new global.TarinaiEffect("heart",
|
|
tarinai.x + side * global.rand(radius * 0.06, radius * 0.82),
|
|
tarinai.y - global.rand(radius * 0.38, radius * 1.72), {
|
|
vx: side * global.rand(9, 30) + global.rand(-9, 9),
|
|
vy: global.rand(-76, -30),
|
|
size: global.rand(9, 17),
|
|
life: global.rand(0.82, 1.36),
|
|
color: "rgba(240,91,135,0.94)",
|
|
}));
|
|
}
|
|
worldRef.effects.push(new global.TarinaiEffect("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);
|
|
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);
|
|
spawnFoodGiftHearts(tarinai, nutrition);
|
|
tarinai.bubble?.("\u2661", 2.2, "rgba(190,82,132,0.80)");
|
|
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" });
|
|
}
|
|
|
|
const sourceLabel = options.source === "pinch" ? "\u624b\u6e21\u3057\u3067" : "\u76f4\u63a5";
|
|
worldRef.log?.(`${tarinai.name}\u306b${label}\u3092${sourceLabel}\u4e0e\u3048\u305f\u3002`, "food", {
|
|
participants: [tarinai], eventType: "direct_feed", sound: false,
|
|
});
|
|
|
|
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);
|