"use strict"; (function (global) { const NEED_KEYS = Object.freeze(["food", "sleep", "health", "safety", "social", "fulfill"]); const NEED_LABELS = Object.freeze({ food: "\u6442\u990c", sleep: "\u7761\u7720", health: "\u5065\u5eb7", safety: "\u5b89\u5168", social: "\u95a2\u4fc2", fulfill: "\u5145\u8db3", }); const NEED_PHRASES = Object.freeze({ food: "\u304a\u306a\u304b\u304c\u3059\u3044\u3066\u3044\u308b", sleep: "\u306d\u3080\u3044", health: "\u8abf\u5b50\u304c\u60aa\u3044", safety: "\u3042\u3076\u306a\u3044", social: "\u3060\u308c\u304b\u304c\u6c17\u306b\u306a\u308b", fulfill: "\u81ea\u5206\u306e\u5834\u6240\u304c\u6c17\u306b\u306a\u308b", }); const NEED_PRIORITY = Object.freeze(["safety", "health", "food", "sleep", "social", "fulfill"]); function createId(prefix = "id") { if (global.crypto?.randomUUID) return `${prefix}_${global.crypto.randomUUID()}`; return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`; } function createDefaultNeeds() { return { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 }; } function quantizeNeed(value) { return Math.max(0, Math.min(100, Math.round((Number(value) || 0) / 10) * 10)); } function getNeedDisplayValue(value) { return Math.round(quantizeNeed(value) / 10); } function personalityValue(t, key) { if (typeof global.effectivePersonalityValue === "function") return Number(global.effectivePersonalityValue(t, key)) || 0; return Number(t?.currentPersonality?.[key] ?? t?.birthPersonality?.[key] ?? 0) || 0; } function needPressure(value) { const x = clamp((Number(value) || 0) / 100, 0, 1); return x * x * 100; } function deriveStressFromNeeds(needs = {}) { return clamp( needPressure(needs.food) * 0.10 + needPressure(needs.sleep) * 0.12 + needPressure(needs.health) * 0.25 + needPressure(needs.safety) * 0.30 + needPressure(needs.social) * 0.08 + needPressure(needs.fulfill) * 0.15, 0, 130 ); } function itemRolesFor(item) { if (!item) return {}; if (!item.roles) item.roles = {}; const type = String(item.type || ""); const roles = item.roles; if (["sweet", "food", "grass", "ant_corpse", "zunchi", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"].includes(type)) roles.food = true; if (type === "water" || type === "water_bowl") roles.drink = true; if (["sweet", "protein", "water", "water_bowl", "zunda_juice"].includes(type)) roles.medicine = true; if (type === "bed" || type === "nest_box") roles.sleepPlace = true; if (["firecracker", "genkotsu", "pushpin", "oshibyo", "splat", "zunchi", "ant_nest", "cannon"].includes(type)) roles.danger = true; if (type === "grass") roles.grassMaterial = true; if (type === "ball") roles.playObject = true; if (item.isStructure) roles.ownedStructure = true; return roles; } function ensureItemNeedEffects(item) { if (!item) return createDefaultNeeds(); if (!item.needEffects) item.needEffects = createDefaultNeeds(); return item.needEffects; } function findNearestItemWithRole(world, t, role, maxDist = 640, predicate = null) { let best = null; let bestD = maxDist; const items = Number.isFinite(maxDist) && world?.nearbyItems ? world.nearbyItems(t.x, t.y, maxDist) : (world?.items || []); for (const it of items) { if (!it || it.dead || (it.amount != null && it.amount <= 0)) continue; const roles = itemRolesFor(it); if (!roles[role]) continue; if (predicate && !predicate(it)) continue; if (t?.shouldAvoidTarget?.(it)) continue; const d = distXY(t.x, t.y, it.x, it.y); if (d < bestD) { best = it; bestD = d; } } return best; } function findNearestSleepPlace(world, t, maxDist = 560) { const own = findOwnedStructure(world, t, "grass_bed", maxDist); return own || findNearestItemWithRole(world, t, "sleepPlace", maxDist); } function findNearbyDanger(world, t, maxDist = 180) { const ant = (world?.nearbyAnts?.(t.x, t.y, maxDist, true) || []).find(a => a && !a.dead); if (ant) return ant; return findNearestItemWithRole(world, t, "danger", maxDist); } function findNearbyMaterial(world, t, materialRole, maxDist = 520) { return findNearestItemWithRole(world, t, materialRole, maxDist); } function ownedStructures(world, t, type = "") { return (world?.items || []).filter(it => it && !it.dead && it.isStructure && it.ownerId === t?.id && (!type || it.type === type)); } function findOwnedStructure(world, t, type = "", maxDist = Infinity) { let best = null; let bestD = maxDist; for (const it of ownedStructures(world, t, type)) { const d = distXY(t.x, t.y, it.x, it.y); if (d < bestD) { best = it; bestD = d; } } return best; } function hasOwnedStructure(world, t, type) { return Boolean(findOwnedStructure(world, t, type)); } function canActNormally(t) { return !t.dead && t.state !== "sleep" && t.state !== "fight" && t.state !== "panic" && t.state !== "intimidate" && t.birthRitualTimer <= 0.04 && t.fightTimer <= 0.04 && t.defeatedTimer <= 0.04; } function updateAttachedStructures(world, t) { for (const s of ownedStructures(world, t)) { if (s.type !== "plushie" || s.carriedById !== t.id || !s.onHead) continue; s.x = t.x + (t.facingDir ? t.facingDir() : 1) * t.radius * 0.12; s.y = t.y - t.radius * 1.10; s.amount = Math.max(1, s.hp || 1); if (t.needs) t.needShock = { ...(t.needShock || {}), fulfill: Math.max(0, Number(t.needShock?.fulfill || 0) - 0.08) }; } } function updateNeeds(t, world) { const prev = t.needsInitialized && t.needs ? { ...t.needs } : null; const pAgg = personalityValue(t, "aggression"); const pOpen = personalityValue(t, "openness"); const pSoc = personalityValue(t, "sociability"); const pNeu = personalityValue(t, "neuroticism"); const raw = createDefaultNeeds(); raw.food = clamp((t.hunger || 0) * 0.88 + (findNearestItemWithRole(world, t, "drink", 120) ? 6 : 0), 0, 100); const dayP = world?.dayProgress?.() ?? 0.5; const night = dayP >= 0.62 || dayP < 0.18 ? 12 : 0; raw.sleep = clamp((100 - (t.energy || 0)) * 0.98 + night, 0, 100); raw.health = clamp( (t.zunchiDisease ? 38 : 0) + (t.sleepDisease ? 22 : 0) + (t.explosionDisease ? 30 : 0) + (t.fightDisease ? 34 : 0) + clamp((100 - (t.energy || 100)) * 0.22, 0, 26) + clamp((t.zunchiStain || 0) * 0.34, 0, 34) + Number(t.needShock?.health || 0), 0, 100 ); const danger = findNearbyDanger(world, t, 210); const recentDamage = (world?.time || 0) - (t.lastDamageAt || -999) < 8 ? clamp((t.lastDamageAmount || 0) * 2.4, 0, 52) : 0; const fear = clamp((t.fearTimer || 0) * 22, 0, 45); raw.safety = clamp((danger ? 42 : 0) + recentDamage + fear + Number(t.needShock?.safety || 0), 0, 100); raw.social = clamp((t.loneliness || 0) * (0.78 + Math.max(0, pSoc) * 0.16) + (t.reproductionTimer <= 4 ? 18 : 0), 0, 100); const hasBed = hasOwnedStructure(world, t, "grass_bed"); const hasPlushie = hasOwnedStructure(world, t, "plushie"); const play = findNearestItemWithRole(world, t, "playObject", 280) ? 10 : 0; raw.fulfill = clamp((hasBed ? 0 : 26) + (hasPlushie ? 0 : 20) + play + Math.max(0, pOpen) * 16 + Number(t.needShock?.fulfill || 0), 0, 100); raw.safety *= clamp(1 - Math.max(0, pAgg) * 0.12 + Math.max(0, pNeu) * 0.16, 0.78, 1.22); raw.health *= clamp(1 + Math.max(0, pNeu) * 0.12, 0.9, 1.16); raw.social *= clamp(1 + Math.max(0, pSoc) * 0.12, 0.9, 1.16); raw.fulfill *= clamp(1 + Math.max(0, pOpen) * 0.12, 0.9, 1.18); const needs = createDefaultNeeds(); for (const key of NEED_KEYS) needs[key] = quantizeNeed(raw[key]); t.previousNeeds = prev; t.needs = needs; t.needsInitialized = true; t.stress = deriveStressFromNeeds(needs); handleNeedShockReaction(t, prev, needs); return needs; } function handleNeedShockReaction(t, prev, needs, breaker = null) { if (!prev || !needs || t.birthRitualTimer > 0.04 || t.fightTimer > 0.04) return false; const safetyDelta = (needs.safety || 0) - (prev.safety || 0); const healthDelta = (needs.health || 0) - (prev.health || 0); const fulfillDelta = (needs.fulfill || 0) - (prev.fulfill || 0); if (safetyDelta < 40 && healthDelta < 45 && fulfillDelta < 50) return false; const attacker = breaker || t.needShockBreaker || null; if (attacker && personalityValue(t, "aggression") > 0.6) { t.intimidateTimer = Math.max(t.intimidateTimer || 0, 2.6); t.intimidateTargetId = attacker.id || null; t.setActionState?.("intimidate", { target: attacker, reason: "\u5927\u4e8b\u306a\u3082\u306e\u3092\u58ca\u3055\u308c\u3066\u5a01\u5687\u3057\u3066\u3044\u308b", wake: true }); } else { t.panicTimer = Math.max(t.panicTimer || 0, 3.0); t.enterPanic?.({ target: t.panicDestination?.(attacker, true) || null, reason: "\u304a\u3069\u308d\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.8, wake: true, cause: "need_shock" }); } t.needShockBreaker = null; return true; } function addNeedShock(t, values = {}, breaker = null) { if (!t) return; const shock = { ...(t.needShock || {}) }; for (const key of NEED_KEYS) shock[key] = clamp(Number(shock[key] || 0) + Number(values[key] || 0), 0, 100); t.needShock = shock; t.needShockBreaker = breaker || t.needShockBreaker || null; const prev = t.needsInitialized && t.needs ? { ...t.needs } : null; updateNeeds(t, t.world); handleNeedShockReaction(t, prev, t.needs, breaker); } function decayNeedShock(t, dt) { if (!t?.needShock) return; for (const key of NEED_KEYS) t.needShock[key] = Math.max(0, Number(t.needShock[key] || 0) - dt * (key === "fulfill" ? 2.2 : 2.8)); } function chooseTopNeedRandom(needs) { const maxNeed = Math.max(...NEED_KEYS.map(key => Number(needs?.[key] || 0))); const tiedNeeds = NEED_KEYS.filter(key => Number(needs?.[key] || 0) === maxNeed); return tiedNeeds[Math.floor(Math.random() * tiedNeeds.length)] || "fulfill"; } function weightedRandom(items) { const total = items.reduce((sum, a) => sum + Math.max(0.01, Number(a.weight || 1)), 0); let pickPoint = Math.random() * total; for (const item of items) { pickPoint -= Math.max(0.01, Number(item.weight || 1)); if (pickPoint <= 0) return item; } return items[items.length - 1] || null; } function buildReasonText(chosenNeed, tiedNeeds, action) { const tied = Array.isArray(tiedNeeds) ? tiedNeeds : [chosenNeed]; const ignored = NEED_PRIORITY.find(key => key !== chosenNeed && tied.includes(key)); const phrase = action?.phrase || "\u5f85\u3063\u305f"; if (ignored) return `${NEED_PHRASES[ignored]}\u3051\u3069\u3001${phrase}\u3002`; return `${NEED_PHRASES[chosenNeed]}\u306e\u3067\u3001${phrase}\u3002`; } function moveTowardOrUse(t, target, state, label) { if (!target) return false; if (distXY(t.x, t.y, target.x, target.y) > Math.max(34, (target.r || 12) + t.radius * 0.75)) { t.setActionState?.(state, { target, reason: label }); return true; } return false; } function createStructure(world, owner, type, x, y) { const def = StructureRegistry[type]; if (!def) return null; const structure = def.create(owner, x, y, world); structure.update = updateStructure; structure.draw = drawStructure; structure.damage = damageStructure; world.items.push(structure); world.itemCounts[structure.type] = (world.itemCounts[structure.type] || 0) + 1; world.markTerrainDirty?.(`structure:${structure.type}`); world.rebuildSpatial?.(true); return structure; } function consumeGrassMaterial(grass) { if (!grass || grass.dead) return false; grass.amount = Math.max(0, Number(grass.amount || 0) - 34); grass.growth = clamp((grass.growth || 0.5) - 0.24, 0.04, 1.2); if (grass.amount <= 1) grass.dead = true; return true; } function finishBuild(t, type, grass) { const world = t.world; const x = clamp(t.x + rand(-18, 18), CONFIG.worldPadding, world.w - CONFIG.worldPadding); const y = clamp(t.y + rand(10, 30), CONFIG.worldPadding, world.h - CONFIG.worldPadding); consumeGrassMaterial(grass); const s = createStructure(world, t, type, x, y); if (s) { t.buildTimer = 0; t.buildType = ""; t.buildMaterialId = ""; t.needShock = { ...(t.needShock || {}), fulfill: Math.max(0, Number(t.needShock?.fulfill || 0) - 26) }; StructureRegistry[type].onUse?.(t, s, world); } return Boolean(s); } function runBuildAction(t, type, label) { const grass = findNearbyMaterial(t.world, t, "grassMaterial", 520); if (!grass) return false; if (moveTowardOrUse(t, grass, "seek_food", label)) { t.buildType = type; t.buildMaterialId = grass.id; return true; } t.buildType = type; t.buildMaterialId = grass.id; t.buildTimer = Math.max(0, Number(t.buildTimer || 0)) + 1; t.setActionState?.("idle", { target: grass, reason: label }); if (t.buildTimer >= (StructureRegistry[type]?.buildTime || 4)) return finishBuild(t, type, grass); return true; } const TARINAI_ACTIONS = [ { id: "eat_food", need: "food", label: "\u98df\u3079\u7269\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u98df\u3079\u305f", weight: 50, condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "food", 620, it => !t.foodItemHasServingLeft || t.foodItemHasServingLeft(it))), run(t, world) { const item = findNearestItemWithRole(world, t, "food", 620, it => !t.foodItemHasServingLeft || t.foodItemHasServingLeft(it)); return moveTowardOrUse(t, item, "seek_food", this.label) || t.beginEating?.(item, this.label); } }, { id: "drink_water", need: "food", label: "\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u6c34\u3092\u98f2\u3093\u3060", weight: 22, condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "drink", 620)), run(t, world) { const item = findNearestItemWithRole(world, t, "drink", 620); return moveTowardOrUse(t, item, "seek_water", this.label) || (t.setActionState?.("seek_water", { target: item, reason: this.label }), true); } }, { id: "sleep_in_bed", need: "sleep", label: "\u5bdd\u5e8a\u3078\u5411\u304b\u3063\u3066\u3044\u308b", phrase: "\u5bdd\u5e8a\u3078\u623b\u3063\u305f", weight: 42, condition: (t, world) => Boolean(findNearestSleepPlace(world, t, 620)), run(t, world) { const bed = findNearestSleepPlace(world, t, 620); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; return t.startSleeping?.(bed, this.label); } }, { id: "sleep_anywhere", need: "sleep", label: "\u305d\u306e\u5834\u3067\u4f11\u3093\u3067\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 20, condition: () => true, run(t) { return t.startSleeping?.(null, this.label); } }, { id: "use_medicine", need: "health", label: "\u4f53\u306b\u3088\u3055\u305d\u3046\u306a\u3082\u306e\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u624b\u5f53\u3066\u3057\u305f", weight: 35, condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "medicine", 620)), run(t, world) { const item = findNearestItemWithRole(world, t, "medicine", 620); return moveTowardOrUse(t, item, itemRolesFor(item).drink ? "seek_water" : "seek_food", this.label) || t.beginEating?.(item, this.label); } }, { id: "rest_to_recover", need: "health", label: "\u4f53\u3092\u4f11\u3081\u3066\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 20, condition: () => true, run(t) { t.setActionState?.("idle", { target: null, reason: this.label }); t.energy = clamp((t.energy || 0) + 1.0, 0, 100); return true; } }, { id: "flee", need: "safety", label: "\u96e2\u308c\u308b\u5834\u6240\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u9003\u3052\u305f", weight: 32, condition: (t, world) => Boolean(findNearbyDanger(world, t, 260)), run(t, world) { const danger = findNearbyDanger(world, t, 260); t.setActionState?.("panic", { target: t.panicDestination?.(danger, true) || null, reason: this.label, wake: true }); t.panicTimer = Math.max(t.panicTimer || 0, 2.0); return true; } }, { id: "hide_at_owned_structure", need: "safety", label: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u308d\u3046\u3068\u3057\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5bdd\u5e8a\u3078\u623b\u3063\u305f", weight: 28, condition: (t, world) => Boolean(findOwnedStructure(world, t, "grass_bed", 760)), run(t, world) { const bed = findOwnedStructure(world, t, "grass_bed", 760); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; StructureRegistry.grass_bed.onUse(t, bed, world); return t.startSleeping?.(bed, this.label); } }, { id: "approach_friend", need: "social", label: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b", phrase: "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u305f", weight: 38, condition: (t, world) => Boolean(world.nearestOther?.(t, 560)), run(t, world) { const other = world.nearestOther?.(t, 560); t.setActionState?.(t.parentToFollow?.() === other ? "follow_parent" : "seek_friend", { target: other, reason: this.label }); return true; } }, { id: "intimidate_enemy", need: "social", label: "\u6c17\u306b\u306a\u308b\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", phrase: "\u5a01\u5687\u3057\u305f", weight: 8, condition: (t, world) => personalityValue(t, "aggression") > 0.45 && Boolean(world.nearestOther?.(t, 220)), run(t, world) { const other = world.nearestOther?.(t, 220); t.intimidateTimer = Math.max(t.intimidateTimer || 0, 2.0); t.intimidateTargetId = other?.id || null; t.setActionState?.("intimidate", { target: other, reason: this.label }); return true; } }, { id: "play", need: "fulfill", label: "\u904a\u3093\u3067\u3044\u308b", phrase: "\u904a\u3093\u3060", weight: 26, condition: (t, world) => Boolean(findNearestItemWithRole(world, t, "playObject", 460)), run(t, world) { const ball = findNearestItemWithRole(world, t, "playObject", 460); t.setActionState?.("play_ball", { target: ball, reason: this.label }); t.goodMode = "smile"; return true; } }, { id: "build_grass_bed", need: "fulfill", label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5bdd\u5e8a\u3092\u4f5c\u3063\u305f", weight: 24, condition: (t, world) => canActNormally(t) && !hasOwnedStructure(world, t, "grass_bed") && Boolean(findNearbyMaterial(world, t, "grassMaterial", 520)), run(t) { return runBuildAction(t, "grass_bed", this.label); } }, { id: "build_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u4f5c\u3063\u305f", weight: 18, condition: (t, world) => canActNormally(t) && !hasOwnedStructure(world, t, "plushie") && Boolean(findNearbyMaterial(world, t, "grassMaterial", 520)), run(t) { return runBuildAction(t, "plushie", this.label); } }, { id: "return_owned_structure", need: "fulfill", label: "\u81ea\u5206\u306e\u5834\u6240\u306b\u623b\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f", weight: 18, condition: (t, world) => Boolean(findOwnedStructure(world, t, "grass_bed", 760)), run(t, world) { const bed = findOwnedStructure(world, t, "grass_bed", 760); if (moveTowardOrUse(t, bed, "seek_bed", this.label)) return true; StructureRegistry.grass_bed.onUse(t, bed, world); return true; } }, { id: "use_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u5927\u4e8b\u306b\u3057\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3067\u843d\u3061\u7740\u3044\u305f", weight: 22, condition: (t, world) => Boolean(findOwnedStructure(world, t, "plushie", Infinity)), run(t, world) { const plushie = findOwnedStructure(world, t, "plushie", Infinity); StructureRegistry.plushie.onUse(t, plushie, world); t.setActionState?.("idle", { target: plushie, reason: this.label }); return true; } }, { id: "wander_lightly", need: "fulfill", label: "\u8efd\u304f\u6563\u6b69\u3057\u3066\u3044\u308b", phrase: "\u6563\u6b69\u3057\u305f", weight: 12, condition: () => true, run(t) { t.wanderAngle += rand(-1.7, 1.7); t.setActionState?.("idle", { target: null, reason: this.label }); return true; } }, ]; function chooseActionForNeed(need, t, world) { const candidates = TARINAI_ACTIONS.filter(a => a.need === need && (!a.condition || a.condition(t, world))); return weightedRandom(candidates) || TARINAI_ACTIONS.find(a => a.id === "wander_lightly"); } function runNeedDecision(t, world, dt = 0.1) { updateAttachedStructures(world, t); decayNeedShock(t, dt); const needs = updateNeeds(t, world); const maxNeed = Math.max(...NEED_KEYS.map(key => needs[key])); const tiedNeeds = NEED_KEYS.filter(key => needs[key] === maxNeed); const chosenNeed = chooseTopNeedRandom(needs); const action = chooseActionForNeed(chosenNeed, t, world); if (!action || !action.run(t, world, dt)) { t.setActionState?.("idle", { target: null, reason: "\u5f85\u3063\u3066\u3044\u308b" }); } const reasonText = buildReasonText(chosenNeed, tiedNeeds, action); t.intent = { need: chosenNeed, tiedNeeds: tiedNeeds.slice(), actionId: action?.id || "idle", actionLabel: action?.label || "\u5f85\u3063\u3066\u3044\u308b", reasonText, }; t.need = chosenNeed; t.thought = reasonText; return t.intent; } function updateStructure(dt, world) { if (this.dead) return; this.age = (this.age || 0) + dt; if (this.type === "plushie" && this.carriedById) { const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === this.carriedById); if (owner) { this.x = owner.x + (owner.facingDir ? owner.facingDir() : 1) * owner.radius * 0.12; this.y = owner.y - owner.radius * 1.10; } else { this.carriedById = ""; this.onHead = false; } } this.amount = Math.max(0, this.hp || 0); } function damageStructure(amount = 1, breaker = null, world = null) { this.hp = Math.max(0, Number(this.hp || 0) - Math.max(0, Number(amount) || 0)); if (this.hp > 0) return false; const w = world || global.world; const def = StructureRegistry[this.type]; def?.onDestroyed?.(this, w, breaker); this.dead = true; this.amount = 0; w?.markTerrainDirty?.(`structure-destroyed:${this.type}`); return true; } function drawStructure(ctx, time, lighting) { const r = this.r || (this.type === "plushie" ? 14 : 28); ctx.save(); ctx.translate(this.x, this.y); if (this.type === "grass_bed") { ctx.fillStyle = "rgba(175, 196, 102, 0.88)"; ctx.strokeStyle = "rgba(90, 118, 55, 0.72)"; ctx.lineWidth = 2.2; ctx.beginPath(); ctx.ellipse(0, 3, r * 1.55, r * 0.74, -0.08, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = "rgba(71, 112, 48, 0.55)"; ctx.lineWidth = 1.6; for (let i = 0; i < 15; i++) { const x = randSeed((this.seed || 1) + i, -r * 1.1, r * 1.1); const y = randSeed((this.seed || 1) + i + 20, -r * 0.32, r * 0.46); const len = randSeed((this.seed || 1) + i + 40, r * 0.30, r * 0.68); const a = randSeed((this.seed || 1) + i + 60, -0.7, 0.7); ctx.beginPath(); ctx.moveTo(x - Math.cos(a) * len * 0.5, y - Math.sin(a) * len * 0.5); ctx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5); ctx.stroke(); } } else if (this.type === "plushie") { const img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : null; ctx.rotate(Math.sin(time * 2.2 + (this.seed || 0)) * 0.08); if (img) ctx.drawImage(img, -r * 0.8, -r * 0.8, r * 1.6, r * 1.45); else { ctx.fillStyle = "#f4ead6"; ctx.strokeStyle = "rgba(92,71,49,0.48)"; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.ellipse(0, 0, r * 0.68, r * 0.58, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } } ctx.restore(); } const StructureRegistry = { grass_bed: { type: "grass_bed", label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9", buildNeed: "fulfill", buildTime: 4, maxHp: 100, materials: { grassMaterial: 1 }, roles: { sleepPlace: true, ownedStructure: true }, needEffects: { food: 0, sleep: 60, health: 5, safety: 25, social: 0, fulfill: 40 }, create(owner, x, y) { return { id: createId("structure"), type: "grass_bed", isStructure: true, ownerId: owner.id, hp: 100, attachment: 20, usedCount: 0, x, y, r: 28, amount: 100, seed: Math.random() * 1000, roles: { ...this.roles }, needEffects: { ...this.needEffects } }; }, onUse(user, structure) { const owner = user?.id === structure?.ownerId; structure.usedCount = (structure.usedCount || 0) + 1; structure.attachment = clamp((structure.attachment || 0) + (owner ? 2 : 0.4), 0, 100); user.needShock = { ...(user.needShock || {}) }; user.needShock.sleep = Math.max(0, Number(user.needShock.sleep || 0) - (owner ? 18 : 8)); user.needShock.safety = Math.max(0, Number(user.needShock.safety || 0) - (owner ? 12 : 4)); user.needShock.fulfill = Math.max(0, Number(user.needShock.fulfill || 0) - (owner ? 18 : 6)); }, onDestroyed(structure, world, breaker) { const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId); if (owner) addNeedShock(owner, { safety: 45, fulfill: 52 }, breaker); }, }, plushie: { type: "plushie", label: "\u306c\u3044\u3050\u308b\u307f", buildNeed: "fulfill", buildTime: 4, maxHp: 1, materials: { grassMaterial: 1 }, roles: { carriedObject: true, ownedStructure: true, fulfillObject: true }, needEffects: { food: 0, sleep: 0, health: 0, safety: 5, social: 0, fulfill: 50 }, create(owner, x, y) { return { id: createId("structure"), type: "plushie", isStructure: true, ownerId: owner.id, hp: 1, attachment: 30, usedCount: 0, x, y, r: 14, amount: 1, carriedById: owner.id, onHead: true, seed: Math.random() * 1000, roles: { ...this.roles }, needEffects: { ...this.needEffects } }; }, onUse(user, structure) { if (!user || !structure) return; structure.attachment = clamp((structure.attachment || 0) + 1.5, 0, 100); user.needShock = { ...(user.needShock || {}), fulfill: Math.max(0, Number(user.needShock?.fulfill || 0) - 20), safety: Math.max(0, Number(user.needShock?.safety || 0) - 3) }; }, onDestroyed(structure, world, breaker) { const owner = (world?.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId); if (owner) addNeedShock(owner, { safety: 58, fulfill: 70 }, breaker); }, }, }; global.TarinaiNeedSystem = { NEED_KEYS, NEED_LABELS, NEED_PHRASES, TARINAI_ACTIONS, StructureRegistry, createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, chooseActionForNeed, buildReasonText, getNeedDisplayValue, findNearestItemWithRole, findNearestSleepPlace, findNearbyDanger, findNearbyMaterial, findOwnedStructure, ownedStructures, itemRolesFor, ensureItemNeedEffects, deriveStressFromNeeds, addNeedShock, runNeedDecision, createStructure, damageStructure, }; global.createDefaultNeeds = createDefaultNeeds; global.quantizeNeed = quantizeNeed; global.updateNeeds = updateNeeds; global.chooseTopNeedRandom = chooseTopNeedRandom; global.chooseActionForNeed = chooseActionForNeed; global.buildReasonText = buildReasonText; global.getNeedDisplayValue = getNeedDisplayValue; global.findNearestItemWithRole = findNearestItemWithRole; global.findNearestSleepPlace = findNearestSleepPlace; global.findNearbyDanger = findNearbyDanger; global.findNearbyMaterial = findNearbyMaterial; global.StructureRegistry = StructureRegistry; })(typeof window !== "undefined" ? window : globalThis);