"use strict"; // ActionSpec factories and TARINAI_ACTIONS registry. function selectRoleTarget(role, range) { return function selectTarget(tarinai, world) { return findNearestItemWithRole(world, tarinai, role, range); }; } function startMoveToTarget(state, fallbackLabel = "行動している") { return function start(tarinai, world, ctx = {}) { const target = ctx.target ?? this.selectTarget?.(tarinai, world, ctx) ?? null; return moveToOrUse(tarinai, target, state, this.label || fallbackLabel); }; } function selectOwnedStructure(type, range = Infinity) { return function selectTarget(tarinai, world) { return findOwnedStructure(world, tarinai, type, range); }; } function selectMateTarget(tarinai, world, range = 440) { return world?.nearestOther?.(tarinai, range, other => !world.areParentChild?.(tarinai, other) && !!tarinai.isZunchiSlave === !!other.isZunchiSlave && !other.sleepDisease && !other.fightDisease) || null; } function basicCanStartWithTarget(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget?.(t, world, ctx)); } function createConsumableActionSpec({ id, subNeed = "", state, label, phrase, weight, role, range }) { return { id, need: role === "medicine" ? "health" : "food", subNeed, state, label, phrase, weight, timerBacked: true, selectTarget: selectRoleTarget(role, range), canStart: basicCanStartWithTarget, start: startMoveToTarget(state, label), tick(t, world, dt) { return updateConsumableBehavior(t, world, dt, role); }, finish(t) { if (role === "drink" && typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { food: 8, health: 2 }, "drink"); return true; }, fail(t) { if (t) t.target = null; return false; }, }; } function createSleepInBedActionSpec() { return { id: "sleep_in_bed", need: "sleep", state: "seek_bed", label: "寝る場所を探している", phrase: "寝る場所へ戻った", weight: 42, timerBacked: true, selectTarget(t, world) { return findNearestSleepPlace(world, t, 760); }, canStart: basicCanStartWithTarget, start(t, world, ctx = {}) { const bed = ctx.target ?? this.selectTarget(t, world, ctx); if (!bed) return false; if (dist(t, bed) > (t.radius || 20) + 18) return moveToOrUse(t, bed, "seek_bed", this.label); const isEasyBed = bed.type === "grass_bed"; bed.use?.(t, world, isEasyBed ? { purpose: "sleep" } : undefined); t.startSleeping?.(bed, bed.type === "nest_box" ? "巣箱の中で寝ている" : (isEasyBed ? "かんたんベッドで寝ている" : "ベッドで寝ている")); return true; }, tick(t) { if (t?.state === "sleep" || t?.sleeping) return true; return basicCanStartWithTarget.call(this, t, t?.world || null, {}) ? true : false; }, }; } function createSleepBuildGrassBedActionSpec() { return { id: "sleep_build_grass_bed", need: "sleep", subNeed: "bed", state: "build", label: "寝る場所がないのでかんたんベッドを作っている", phrase: "かんたんベッドを作った", weight: 18, timerBacked: true, selectTarget: selectRoleTarget("grassMaterial", 520), canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findNearestSleepPlace(world, t, 760) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["fight", "panic", "intimidate", "birth_ritual"].includes(t.state); }, start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); }, tick(t, world, dt) { if (findOwnedStructure(world, t, "grass_bed")) return "finished"; return continueBuildPlan(t, world, dt) ? true : false; }, }; } function createSleepAnywhereActionSpec() { return { id: "sleep_anywhere", need: "sleep", state: "sleep", label: "なにもない所で寝ている", phrase: "そのまま寝た", weight: 8, timerBacked: true, canStart(t, world) { if (findNearestSleepPlace(world, t, 760)) { if (t) t.noBedNoBuildSince = NaN; return false; } const now = world?.time || 0; const failedRecently = (t?.lastFailedGrassBedBuildAt || -999) + 14 > now; const buildTarget = this.selectBuildTarget ? this.selectBuildTarget(t, world, {}) : findNearbyMaterial(world, t, "grassMaterial", 520); const canTryBuild = canBuildOwnedStructureByAge(t) && !!buildTarget && !(Number(t?.actionIdCooldowns?.sleep_build_grass_bed || 0) > 0.01); if (canTryBuild && !failedRecently) { if (t) t.noBedNoBuildSince = NaN; return false; } if (t && !Number.isFinite(t.noBedNoBuildSince)) t.noBedNoBuildSince = now; const since = Number.isFinite(t?.noBedNoBuildSince) ? t.noBedNoBuildSince : now; const waited = now - since; const urgent = (t?.energy || 100) < 18 || (t?.needRaw?.sleep || t?.needs?.sleep || 0) >= 92; return failedRecently || urgent || waited >= 6.0; }, selectBuildTarget: selectRoleTarget("grassMaterial", 520), start(t) { t.startSleeping?.(null, this.label); return true; }, tick(t) { return (t?.state === "sleep" || t?.sleeping) ? true : false; }, }; } function createRestActionSpec() { return { id: "rest_to_recover", need: "health", state: "idle", label: "元気になるまで休んでいる", phrase: "休んだ", weight: 24, canStart() { return true; }, start(t) { t.setActionState?.("idle", { target: null, reason: this.label, sleeping: false }); t.energy = clamp((t.energy || 0) + 1.6, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100)); return true; }, tick(t) { t.energy = clamp((t.energy || 0) + 0.8, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100)); return true; }, }; } function createSunbathActionSpec() { return { id: "sunbath", need: "health", subNeed: "sunbath", state: "sunbath", label: "日光浴している", phrase: "日光浴している", weight: 32, timerBacked: true, canStart(t, world, ctx = {}) { const healthNeed = Number(t?.needRaw?.health ?? t?.needs?.health ?? 0) || 0; return (ctx.leisure || healthNeed >= needThreshold("health", "start")) && !!t?.canStartSunbath?.({ leisure: !!ctx.leisure }); }, start(t, world, ctx = {}) { return !!t?.startSunbath?.({ leisure: !!ctx.leisure }); }, tick(t) { return (t?.sunbathTimer || 0) > 0.04 ? true : "finished"; }, finish(t) { if ((t?.sunbathTimer || 0) <= 0 && t?.state === "sunbath") t.finishSunbath?.(); return true; }, fail(t) { if (t && t.state === "sunbath") { t.sunbathTimer = 0; t.goIdle?.("日光浴をやめた"); } return false; }, }; } function createWanderActionSpec() { function targetFor(t, world) { const radius = rand(80, 180); return { x: clamp(t.x + Math.cos(t.wanderAngle) * radius, t.radius || 20, (world?.w || 1600) - (t.radius || 20)), y: clamp(t.y + Math.sin(t.wanderAngle) * radius, t.radius || 20, (world?.h || 900) - (t.radius || 20)), dead: false, detour: true, }; } return { id: "wander_lightly", need: "fulfill", state: "wander", label: "少し歩いている", phrase: "少し歩いた", weight: 14, canStart() { return true; }, selectTarget: targetFor, start(t, world, ctx = {}) { t.wanderAngle += rand(-1.2, 1.2); const target = ctx.target && Number.isFinite(ctx.target.x) ? ctx.target : targetFor(t, world); t.wanderTarget = target; t.setActionState?.("wander", { target, reason: this.label, sleeping: false }); return true; }, tick(t, world) { const target = t?.wanderTarget || currentBehaviorTarget(t) || null; if (!target || !Number.isFinite(target.x) || dist(t, target) <= Math.max(28, (t.radius || 20) + 10)) return "finished"; moveToOrUse(t, target, "wander", this.label); return true; }, }; } function selectFightRivalTarget(tarinai, world, range = 360) { const candidate = conflictTargetFor(tarinai, world, (tarinai?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(tarinai) ? 18 : 34); const current = currentBehaviorTarget(tarinai); return isLiveTarinaiEntity(candidate?.target) ? candidate.target : (isLiveTarinaiEntity(current) ? current : nearestForcedFightTarget(tarinai, world, isCurrentBehaviorForced(tarinai) ? 760 : range)); } function createPanicEscapeActionSpec() { return { id: "panic_escape", need: "safety", subNeed: "panic", state: "panic", label: "怖くて逃げている", phrase: "逃げた", weight: 62, timerBacked: true, selectTarget(t, world) { return t.lastNeedShockBreaker || findNearbyDanger(world, t, 240) || ((t.defeatedById && world?.tarinai) ? world.liveTarinaiById?.(t.defeatedById) : null); }, canStart(t, world, ctx = {}) { const threat = ctx.target ?? this.selectTarget(t, world, ctx); const safety = Number(t?.needRaw?.safety ?? t?.needs?.safety ?? 0) || 0; // Do not start panic from ordinary safety pressure alone. Panic needs a // concrete trigger, lingering fear, defeat, or an extreme safety spike. return !!threat || (t?.defeatedTimer || 0) > 0.04 || (safety >= 92 && !!activePanicBreaker(t, world, 260)); }, start(t, world, ctx = {}) { const threat = ctx.target ?? this.selectTarget(t, world, ctx); t.setActionState?.("panic", { target: t.panicDestination?.(threat, true), reason: this.label, wake: true, sleeping: false }); t.fearTimer = Math.max(t.fearTimer || 0, 1.6); return true; }, tick: updatePanicBehavior, }; } function createFleeActionSpec() { return { id: "flee", need: "safety", subNeed: "avoid", state: "flee", label: "危ないものから離れている", phrase: "離れた", weight: 40, selectTarget(t, world) { return findNearbyDanger(world, t, 220); }, canStart: basicCanStartWithTarget, start(t, world, ctx = {}) { const danger = ctx.target ?? this.selectTarget(t, world, ctx); t.setActionState?.("flee", { target: t.panicDestination?.(danger), reason: this.label, wake: true, sleeping: false }); return true; }, tick: updatePanicBehavior, }; } function createApproachFriendActionSpec() { return { id: "approach_friend", need: "social", subNeed: "bond", state: "seek_friend", label: "仲間に近づいている", phrase: "仲間に近づいた", weight: 46, selectTarget(t, world) { return t.bestFriendLive?.(FRIEND_AFFINITY_THRESHOLD, 520) || world.nearestOther?.(t, 420) || null; }, canStart: basicCanStartWithTarget, start: startMoveToTarget("seek_friend", "仲間に近づいている"), tick(t, world, dt) { return updateSocialContactBehavior(t, world, dt, "bond"); }, }; } function createApproachFamilyActionSpec() { return { id: "approach_parent_or_child", need: "social", subNeed: "family", state: "follow_parent", label: "家族に近づいている", phrase: "家族に近づいた", weight: 28, selectTarget(t) { return t.parentToFollow?.() || null; }, canStart: basicCanStartWithTarget, start: startMoveToTarget("follow_parent", "家族に近づいている"), tick(t, world, dt) { return updateSocialContactBehavior(t, world, dt, "family"); }, }; } function createApproachMateActionSpec() { return { id: "approach_mate", need: "social", subNeed: "mate", state: "seek_friend", label: "繁殖できる相手に近づいている", phrase: "繁殖できる相手に近づいている", weight: 48, selectTarget(t, world) { return selectMateTarget(t, world, 520); }, canStart(t, world, ctx = {}) { return (t.reproductionTimer || 0) <= 12 && !!(ctx.target ?? this.selectTarget(t, world, ctx)); }, start(t, world, ctx = {}) { const other = ctx.target ?? this.selectTarget(t, world, ctx); if (!other) return false; if (dist(t, other) <= Math.max(48, (t.radius || 20) + (other.radius || 20) + 12) && world.startBirthRitual?.(t, other)) return true; return moveToOrUse(t, other, "seek_friend", this.label); }, tick: updateMateBehavior, }; } function createFightRivalActionSpec() { return { id: "fight_rival", need: "social", subNeed: "conflict", state: "seek_enemy", label: "気に入らない相手に向かっている", phrase: "喧嘩を始めた", weight: 18, timerBacked: true, selectTarget(t, world) { return selectFightRivalTarget(t, world); }, canStart(t, world, ctx = {}) { if ((t.fightCooldown || 0) > 0.04 || (t.postConflictPeaceTimer || 0) > 0.04) return false; const forced = (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t); const candidate = conflictTargetFor(t, world, forced ? 24 : 42); if (!forced) { const parts = t?.socialReasonParts || {}; const conflict = Number(parts.conflict || 0) || 0; const socialPull = Math.max(Number(parts.bond || 0) || 0, Number(parts.mate || 0) || 0, Number(parts.family || 0) || 0); if (!candidate?.target || conflict < 30 || conflict < socialPull * 0.82) return false; } return !!(ctx.target ?? candidate?.target ?? this.selectTarget(t, world, ctx)); }, start(t, world, ctx = {}) { const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 18 : 34); const rival = ctx.target ?? this.selectTarget(t, world, ctx); if (!isLiveTarinaiEntity(rival)) return false; t.conflictTargetId = rival.id; t.conflictUrge = Math.max(t.conflictUrge || 0, candidate?.score || 48); if (dist(t, rival) > Math.max(42, (t.radius || 20) + (rival.radius || 20) + 12)) return moveToOrUse(t, rival, "seek_enemy", this.label); if (candidate?.forced || isCurrentBehaviorForced(t) || (t.fightMochiTimer || 0) > 0.04 || (rival.fightMochiTimer || 0) > 0.04) return !!world.startForcedFight?.(t, rival); world.startConflict?.(t, rival); return true; }, tick: updateFightBehavior, }; } function createIntimidateEnemyActionSpec() { return { id: "intimidate_enemy", need: "social", subNeed: "conflict", state: "intimidate", label: "気になる相手を威嚇している", phrase: "威嚇した", weight: 8, timerBacked: true, selectTarget(t) { return t.recentFightRival?.(80, 180) || null; }, canStart(t, world, ctx = {}) { if ((t.intimidateTimer || 0) > 0.04 || (t.postConflictPeaceTimer || 0) > 0.04) return false; const parts = t?.socialReasonParts || {}; if ((t.fightMochiTimer || 0) <= 0.04 && Number(parts.conflict || 0) < 24) return false; return basicCanStartWithTarget.call(this, t, world, ctx); }, start(t, world, ctx = {}) { const rival = ctx.target ?? this.selectTarget(t, world, ctx); t.setActionState?.("intimidate", { target: rival, reason: this.label, sleeping: false }); t.intimidateTimer = Math.max(t.intimidateTimer || 0, 0.85); return true; }, tick(t, world) { if ((t.intimidateTimer || 0) > 0.04) return true; const rival = currentBehaviorTarget(t) || t.recentFightRival?.(80, 180); if (!rival) return false; return world?.startIntimidation?.(t, rival) || true; }, }; } function createBirthRitualActionSpec() { return { id: "birth_ritual", need: "social", subNeed: "mate", label: "へこへこしている", phrase: "へこへこしている", weight: 0, state: "birth_ritual", timerBacked: true, canStart(t) { return (t.birthRitualTimer || 0) > 0.04; }, start() { return true; }, tick(t) { return (t.birthRitualTimer || 0) > 0.04 ? true : "finished"; }, }; } const TARINAI_ACTION_DEFINITIONS = [ createConsumableActionSpec({ id: "eat_food", subNeed: "meal", state: "seek_food", label: "食べ物を探している", phrase: "食べ物を食べている", weight: 62, role: "food", range: 760, }), createConsumableActionSpec({ id: "drink_water", subNeed: "water", state: "seek_water", label: "水を探している", phrase: "水を飲んだ", weight: 24, role: "drink", range: 560, }), createSleepInBedActionSpec(), createSleepBuildGrassBedActionSpec(), createSleepAnywhereActionSpec(), createConsumableActionSpec({ id: "use_medicine", subNeed: "medicine", state: "seek_food", label: "元気になるものを探している", phrase: "元気になった", weight: 34, role: "medicine", range: 620, }), createRestActionSpec(), createSunbathActionSpec(), createPanicEscapeActionSpec(), createFleeActionSpec(), { id: "hide_at_owned_structure", need: "safety", state: "seek_bed", label: "自分の場所に隠れている", phrase: "自分の場所へ戻った", weight: 30, selectTarget: selectOwnedStructure("grass_bed", 620), canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); }, start: startMoveToTarget("seek_bed", "自分の場所に隠れている"), }, createApproachFriendActionSpec(), createApproachFamilyActionSpec(), createApproachMateActionSpec(), createFightRivalActionSpec(), createIntimidateEnemyActionSpec(), { id: "play", need: "fulfill", state: "play_ball", label: "遊んでいる", phrase: "遊んだ", weight: 24, selectTarget(t, world) { return world.nearest?.(t, ["ball"], 520) || null; }, canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); }, start: startMoveToTarget("play_ball", "遊んでいる"), }, { id: "build_grass_bed", need: "fulfill", state: "build", label: "かんたんベッドを作っている", phrase: "かんたんベッドを作った", weight: 34, selectTarget: selectRoleTarget("grassMaterial", 520), canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "grass_bed") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, start(t, world) { return buildOwnedStructureFromGrass(t, world, "grass_bed", this.label); }, }, { id: "build_plushie", need: "fulfill", state: "build", label: "ぬいぐるみを作っている", phrase: "ぬいぐるみを作った", weight: 28, selectTarget: selectRoleTarget("grassMaterial", 520), canStart(t, world, ctx = {}) { return canBuildOwnedStructureByAge(t) && !findOwnedStructure(world, t, "plushie") && !!(ctx.target ?? this.selectTarget(t, world, ctx)) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, start(t, world) { return buildOwnedStructureFromGrass(t, world, "plushie", this.label); }, }, { id: "return_owned_structure", need: "fulfill", state: "seek_bed", label: "自分の場所へ戻っている", phrase: "自分の場所へ戻った", weight: 22, selectTarget: selectOwnedStructure("grass_bed", 620), canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); }, start: startMoveToTarget("seek_bed", "自分の場所へ戻っている"), }, { id: "use_plushie", need: "fulfill", state: "idle", label: "ぬいぐるみを抱えている", phrase: "ぬいぐるみを抱えた", weight: 18, selectTarget: selectOwnedStructure("plushie", Infinity), canStart(t, world, ctx = {}) { return !!(ctx.target ?? this.selectTarget(t, world, ctx)); }, start(t, world, ctx = {}) { const plushie = ctx.target ?? this.selectTarget(t, world, ctx); plushie?.use?.(t, world); t.setActionState?.("idle", { target: plushie, reason: this.label, sleeping: false }); return true; }, }, createBirthRitualActionSpec(), createWanderActionSpec(), ]; const TARINAI_ACTIONS = typeof registerTarinaiActionSpecs === "function" ? registerTarinaiActionSpecs(TARINAI_ACTION_DEFINITIONS) : TARINAI_ACTION_DEFINITIONS; if (typeof window !== "undefined") Object.assign(window, { TARINAI_ACTION_DEFINITIONS, TARINAI_ACTIONS });