From 54aff02658d7d1577e15d03cde0791c2eeeca740 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Tue, 23 Jun 2026 18:11:42 +0900 Subject: [PATCH] hmm --- .tmp-edge-profile/Crashpad/metadata | 0 .tmp-edge-profile/Crashpad/settings.dat | Bin 0 -> 288 bytes .tmp-edge-profile/Crashpad/throttle_store.dat | 1 + .tmp-edge-profile/Variations | 1 + PATCH_NOTES.md | 136 +- app_manifest.json | 2 +- css/components.css | 7 + docs/prompt.txt | 1326 +++++++++++------ index.html | 133 +- js/effect_registry.js | 6 +- js/freeze_system.js | 2 +- js/health.js | 2 + js/items.js | 14 +- js/relation_event_system.js | 2 +- js/render.js | 4 + js/structures.js | 214 +++ js/tarinai.js | 7 +- js/tarinai_action_state.js | 40 +- js/tarinai_disease_nest.js | 14 +- js/tarinai_identity_social.js | 12 +- js/tarinai_needs_items.js | 727 +++++---- js/tarinai_social_move_life.js | 35 +- js/ui_selected.js | 15 +- js/version.js | 4 +- js/world_combat_effects.js | 24 +- js/world_family_social.js | 4 +- service-worker.js | 2 +- 27 files changed, 1700 insertions(+), 1034 deletions(-) create mode 100644 .tmp-edge-profile/Crashpad/metadata create mode 100644 .tmp-edge-profile/Crashpad/settings.dat create mode 100644 .tmp-edge-profile/Crashpad/throttle_store.dat create mode 100644 .tmp-edge-profile/Variations create mode 100644 js/structures.js diff --git a/.tmp-edge-profile/Crashpad/metadata b/.tmp-edge-profile/Crashpad/metadata new file mode 100644 index 0000000..e69de29 diff --git a/.tmp-edge-profile/Crashpad/settings.dat b/.tmp-edge-profile/Crashpad/settings.dat new file mode 100644 index 0000000000000000000000000000000000000000..e76f11a5de8023cd7a257067dd8fd4e58d8ae613 GIT binary patch literal 288 pcmXR;32 display 8 +internal value 100 -> display 10 + +Safety is capped at 100 like every other need. +Safety is not a hard interrupt. + +============================================================ +2. NEED CALCULATION +============================================================ + +Calculate needs from existing tarinai/world state where possible. + +Suggested mapping: + +food: +- hunger +- thirst if available + +sleep: +- low energy +- long awake time if available +- night/time-of-day bonus if available + +health: +- disease +- injury +- dirtiness +- poison or negative status if available + +safety: +- nearby ants +- nearby dangerous items +- recent damage +- fear/panic-related existing state if any +- being attacked +- owned structure destruction shock + +social: +- loneliness if available +- nearby friend / parent / child +- reproduction readiness +- enemy proximity if current relation system supports it + +fulfill: +- wants to play +- has no owned structure +- wants to return to owned grass bed +- wants to carry/use plushie +- wants to build a simple owned structure + +Use existing personality fields if available: + +aggression: +- safety rises slightly less from danger +- may choose intimidate instead of panic when there is a breaker/attacker + +openness: +- fulfill rises more easily +- play/build actions become more likely + +sociability: +- social rises more easily +- reacts less negatively when others use owned structures + +neuroticism: +- safety and health rise more easily +- owned structure damage/destruction has stronger effect + +Keep personality modifiers small and easy to tune. + +============================================================ +3. ACTION SELECTION +============================================================ + +Use this behavior flow: + +1. Calculate all six raw needs. +2. Quantize all needs. +3. Find the maximum need value. +4. Collect all needs equal to the maximum. +5. Randomly choose one need from that tied top set. +6. Choose an executable action associated with that chosen need. +7. Store the chosen need and action in tarinai.intent. +8. Highlight only the chosen need in the tarinai data UI. + +If safety and food are both 100, either may be chosen randomly. + +Add helpers: + +- createDefaultNeeds() +- quantizeNeed(value) +- updateNeeds(tarinai, world) +- chooseTopNeedRandom(needs) +- chooseActionForNeed(need, tarinai, world) +- buildReasonText(chosenNeed, tiedNeeds, action) +- getNeedDisplayValue(value) + +============================================================ +4. SIMPLE ACTION DEFINITIONS +============================================================ + +Use a simple action list, not a complex behavior tree. + +Each action should have: + +- id +- need +- label +- phrase +- weight +- condition(tarinai, world) +- run(tarinai, world) + +Example shape: + +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) { + return !!findNearestItemWithRole(world, t, "food"); + }, + run(t, world) { + const item = findNearestItemWithRole(world, t, "food"); + moveToOrUse(t, item); + } + } +]; + +Suggested actions: + +food: +- eat_food +- drink_water + +sleep: +- sleep_in_bed +- sleep_in_owned_structure +- sleep_anywhere + +health: +- use_medicine +- wash_or_drink_water +- rest_to_recover + +safety: +- flee +- hide_in_nest +- hide_at_owned_structure + +social: +- approach_friend +- approach_parent_or_child +- approach_mate +- intimidate_enemy + +fulfill: +- play +- wander_lightly +- build_grass_bed +- build_plushie +- return_owned_structure +- use_plushie + +Use weighted random among executable actions for the chosen need. + +If no action is executable, fall back to idle, wander, or rest. + +Do not make tarinai perfectly optimal. + +============================================================ +5. TIE REASON TEXT +============================================================ + +If multiple needs are tied for highest and the chosen need is only one of them, generate: + +“[ignored need phrase]\u3051\u3069\u3001[chosen action phrase]\u3002” + +Examples: +- "\u3042\u3076\u306a\u3044\u3051\u3069\u3001\u98df\u3079\u305f\u3002" +- "\u306d\u3080\u3044\u3051\u3069\u3001\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u305f\u3002" +- "\u304a\u306a\u304b\u304c\u3059\u3044\u3066\u3044\u308b\u3051\u3069\u3001\u81ea\u5206\u306e\u5bdd\u5e8a\u3078\u623b\u3063\u305f\u3002" +- "\u8abf\u5b50\u304c\u60aa\u3044\u3051\u3069\u3001\u904a\u3093\u3060\u3002" + +When multiple ignored needs exist, mention the ignored need with highest priority: + +safety > health > food > sleep > social > fulfill + +Conflict phrases: + +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" + +If there is no tie, use: + +“[need phrase]\u306e\u3067\u3001[action phrase]\u3002” + +Store: + +tarinai.intent = { + need: chosenNeed, + tiedNeeds, + actionId: action.id, + actionLabel: action.label, + reasonText +}; + +============================================================ +6. NEED UI +============================================================ + +Update the tarinai data UI. + +Show six needs as 0–10 values. + +Labels must be escaped in source: + +food label: +"\u6442\u990c" + +sleep label: +"\u7761\u7720" + +health label: +"\u5065\u5eb7" + +safety label: +"\u5b89\u5168" + +social label: +"\u95a2\u4fc2" + +fulfill label: +"\u5145\u8db3" + +Example UI: + +摂餌 8 +睡眠 3 +健康 1 +安全 10 +関係 4 +充足 10 ★ + +Only the need actually chosen for the current action receives the highlight. + +Also show: + +"\u3044\u307e": +[action label] + +"\u7406\u7531": +[reasonText] + +Do not show detailed need breakdowns. +Do not add graphs or extra panels. + +============================================================ +7. ITEM ROLES AND NEED EFFECTS +============================================================ + +Use minimal item roles and needEffects. + +Do not create a broad tag system. + +Item example: + +item.roles = { + food: true, + drink: true, + medicine: true, + sleepPlace: true, + danger: true, + grassMaterial: true +}; + +item.needEffects = { + food: 0, + sleep: 0, + health: 0, + safety: 0, + social: 0, + fulfill: 0 +}; + +Only add roles that are actually needed by the new behavior. + +Useful helpers: + +- findNearestItemWithRole(world, tarinai, role) +- findNearestSleepPlace(world, tarinai) +- findNearbyDanger(world, tarinai) +- findNearbyMaterial(world, tarinai, materialRole) + +Adapt to existing item registries where possible. + +============================================================ +8. STRUCTURE REGISTRY +============================================================ + +Keep StructureRegistry because tarinai-built structures will expand in the future. + +Do not collapse structures into one-off special item cases. + +Create or adapt a small StructureRegistry for buildable tarinai structures. + +Shared structure instance shape: + +{ + id, + type, + isStructure: true, + ownerId, + hp, + attachment, + usedCount, + x, + y, + roles, + needEffects +} + +Do not implement: +- repair +- inheritance +- ownership privacy +- material-carrying inventory +- complex construction logistics + +First buildable structures: + +- grass_bed +- plushie + +============================================================ +9. STRUCTURE: GRASS BED +============================================================ + +Structure type: +"grass_bed" + +Japanese label, escaped: +"\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9" + +Concept: +A simple bed made from grass. It is an owned place. The owner prefers it for sleep, safety, and fulfill. + +Example registry entry shape: + +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, world) { + return { + id: createId("structure"), + type: "grass_bed", + isStructure: true, + ownerId: owner.id, + hp: 100, + attachment: 20, + usedCount: 0, + x, + y, + roles: this.roles, + needEffects: this.needEffects + }; + }, + onUse(user, structure, world) { + // owner gets stronger effects + // non-owner gets weaker effects + // owner use increases attachment + }, + onDestroyed(structure, world, breaker) { + // increase owner safety and fulfill + // trigger panic or intimidate through need shock logic + } +} + +Build condition: +- chosen need is fulfill +- tarinai has no owned grass_bed +- grass material is nearby +- tarinai is not sleeping, fighting, or panicking + +Build process: +- move near grass +- spend short build time if existing state system supports it +- consume/reduce grass if easy +- create grass_bed through StructureRegistry + +Owner behavior: +- owner prefers own grass_bed for sleep, safety, and fulfill +- using own grass_bed reduces sleep, safety, and fulfill +- using own grass_bed increases attachment + +Other tarinai: +- may use it with weaker effects +- if owner sees another tarinai using it: + - aggression may cause intimidate + - neuroticism may increase safety/fulfill + - sociability reduces negative reaction + +Destruction: +- hp can be reduced by explosions, punches, weather, repeated use, or existing damage sources if easy to connect +- when hp reaches 0, call onDestroyed and remove structure + +============================================================ +10. STRUCTURE: PLUSHIE +============================================================ + +Add a new tarinai-built structure: + +type: +"plushie" + +Japanese label, escaped: +"\u306c\u3044\u3050\u308b\u307f" + +Concept: +A small plushie item that resembles a tarinai. It is made from grass. When completed, it attaches to the owner tarinai’s head. While attached, it reduces fulfill. If damaged, it breaks and disappears. The owner receives a strong need shock and enters panic or intimidate. + +Example registry entry shape: + +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, world) { + return { + id: createId("structure"), + type: "plushie", + isStructure: true, + ownerId: owner.id, + hp: 1, + attachment: 30, + usedCount: 0, + x, + y, + carriedById: owner.id, + onHead: true, + roles: this.roles, + needEffects: this.needEffects + }; + }, + onUse(user, structure, world) { + // if owner carries/uses it, reduce fulfill + // optionally reduce safety slightly + // increase attachment slightly + }, + onDestroyed(structure, world, breaker) { + // large shock to owner safety and fulfill + // trigger panic or intimidate + } +} + +Build condition: +- chosen need is fulfill +- tarinai does not already own/carry a plushie +- grass material is nearby +- tarinai is not sleeping, fighting, or panicking + +Build process: +- move near grass +- spend short build time if existing state system supports it +- consume/reduce grass if easy +- create plushie through StructureRegistry +- attach it to owner’s head + +Behavior while attached: +- follow owner position +- render above owner’s head +- use same or similar sprite as tarinai, scaled down +- reduce fulfill over time or when used +- optionally reduce safety slightly +- do not implement inventory + +Damage/destruction: +- any meaningful damage destroys it +- remove plushie +- increase owner safety and fulfill strongly +- trigger panic or intimidate through need shock logic + +Do not implement: +- repair +- inheritance +- trading +- inventory +- multiple plushies per tarinai + +============================================================ +11. EMOTION SYSTEM SIMPLIFICATION +============================================================ + +Remove the old detailed emotion system as a behavior driver. + +Behavior must be selected only from the six needs: +food, sleep, health, safety, social, fulfill. + +Old emotions should be removed or absorbed: + +fear -> safety +loneliness -> social +happiness -> low stress / low fulfill pressure +sadness -> fulfill/social/safety pressure +comfort -> sleep/safety/fulfill satisfaction +boredom -> do not implement +anger -> intimidate state only +panic -> need shock reaction +stress -> derived pressure from needs + +Do not keep happiness, sadness, fear, loneliness, comfort, boredom, or anger as independent behavior-driving values. + +============================================================ +12. STRESS REDEFINITION +============================================================ + +Stress is not an independent emotion. + +Stress is a derived value calculated from current need pressure. + +Do not directly add to stress for shock events. +For shock events, increase relevant needs first, then recalculate stress from needs. + +Stress must not be used as a primary action-selection need. + +Behavior is selected only from the six needs. + +Stress may affect: +- expression +- minor visual intensity +- UI display if already present +- minor secondary effects if existing systems need it + +Suggested pressure function: + +function needPressure(value) { + const x = value / 100; + return x * x * 100; +} + +Suggested stress calculation: + +stress = + needPressure(food) * 0.10 + + needPressure(sleep) * 0.12 + + needPressure(health) * 0.25 + + needPressure(safety) * 0.30 + + needPressure(social) * 0.08 + + needPressure(fulfill) * 0.15; + +Adjust weights if needed, but keep stress derived from needs. + +============================================================ +13. PANIC REDEFINITION +============================================================ + +Panic is not a need. +Panic is not a persistent emotion. + +Panic is a short temporary reaction state caused by large sudden increases in safety, health, or fulfill. + +Food, sleep, and social should not normally trigger panic. + +Track previous needs or otherwise compute need delta. + +Suggested rule: + +panic triggers if: +- safety increases suddenly by 40 or more +- health increases suddenly by 45 or more +- fulfill increases suddenly by 50 or more + +Panic should last for a short timer, such as 2–4 seconds. + +After panic timer ends, return to normal need-based action selection. + +If panic would be triggered and the tarinai has high aggression and a known breaker/attacker, use intimidate instead of panic. + +Example: + +if (shockTriggered) { + if (breaker && t.personality?.aggression > 0.6) { + t.state = "intimidate"; + t.target = breaker; + } else { + t.state = "panic"; + t.panicTimer = 3.0; + } +} + +============================================================ +14. EXPRESSION / VISUAL EMOTION COMPATIBILITY +============================================================ + +If the old emotion system is deeply tied to expressions, keep only a small compatibility layer for visuals. + +Expressions should be derived from: +- current state +- chosen need +- stress level +- panic/intimidate + +Suggested mapping: + +state panic: +panic expression + +state intimidate: +angry/threat expression + +chosen need safety: +worried expression + +chosen need food: +hungry expression + +chosen need sleep: +sleepy expression + +chosen need health: +unwell expression + +chosen need social: +social/looking expression + +chosen need fulfill: +neutral/pleased expression + +Do not let old expression emotions drive behavior. + +============================================================ +15. SAVE COMPATIBILITY +============================================================ + +Backward compatibility is not required. + +Do not implement save migrations. +Do not add compatibility shims for old saves unless the current code would immediately crash during normal startup. + +It is acceptable if old saves are incompatible after this feature. + +============================================================ +16. DO NOT ADD +============================================================ + +Do not add: +- long-term memory +- boredom/habit system +- player-intervention profile +- ReactionRegistry +- WorldRules +- full behavior tree +- complex utility AI +- detailed need breakdown UI +- repair system +- inheritance system +- material-carrying inventory +- structure privacy system + +============================================================ +17. ACCEPTANCE CRITERIA +============================================================ + +After implementation: + +- Each tarinai has six needs: + - food + - sleep + - health + - safety + - social + - fulfill + +- Needs are clamped 0–100. +- Needs are quantized to 10-point steps. +- UI displays needs as 0–10. +- Only the chosen action-driving need is highlighted. +- If multiple needs are tied for highest, one is chosen randomly. +- Safety does not force interruption. +- If safety and another need are tied, non-safety behavior can happen. +- Tie reason text uses the “Xけど、Yした。” format. +- Newly added Japanese source strings are Unicode escaped. +- Actions are defined in a simple action list. +- Items expose minimal roles and needEffects. +- StructureRegistry exists and supports at least grass_bed and plushie. +- grass_bed consumes grass to build. +- grass_bed is preferred by its owner. +- grass_bed destruction increases owner safety/fulfill and can trigger panic/intimidate. +- plushie consumes grass to build. +- plushie attaches to the owner’s head. +- plushie visually resembles a small tarinai. +- plushie reduces fulfill while carried/used. +- plushie breaks and disappears when damaged. +- plushie destruction strongly increases owner safety/fulfill and triggers panic/intimidate. +- Old detailed emotion values no longer drive behavior. +- Stress is derived from needs. +- Panic is caused by sudden safety/health/fulfill increases. +- Old save compatibility is not required. \ No newline at end of file diff --git a/index.html b/index.html index 75ac774..4ea4a73 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,11 @@ たりない観察 - - - - - + + + + +
@@ -183,66 +183,67 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/effect_registry.js b/js/effect_registry.js index 4d73aa4..1810a8c 100644 --- a/js/effect_registry.js +++ b/js/effect_registry.js @@ -109,7 +109,7 @@ const scale = context.scale ?? 1; tarinai.loveMochiTimer = Math.max(tarinai.loveMochiTimer || 0, 42 * scale + rand(4, 12)); const nutrition = Number(context.nutrition) || 0; - tarinai.stress = clamp(tarinai.stress - (context.source === "eating" ? nutrition * 0.25 : 8), 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { fulfill: -(context.source === "eating" ? nutrition * 0.5 : 12), social: -4 }); return true; }, }); @@ -118,7 +118,7 @@ const scale = context.scale ?? 1; tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + rand(10, 18)); const nutrition = Number(context.nutrition) || 0; - tarinai.stress = clamp(tarinai.stress + (context.source === "eating" ? nutrition * 0.22 : 10), 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(tarinai, { safety: context.source === "eating" ? nutrition * 0.5 : 16 }); if (context.source === "eating") tarinai.fearTimer = Math.max(tarinai.fearTimer || 0, 0.32); return true; }, @@ -127,7 +127,7 @@ onApply(tarinai, context = {}) { if (context.source === "eating") { const nutrition = Number(context.nutrition) || 0; - tarinai.stress = clamp(tarinai.stress - nutrition * 0.10, 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { safety: -nutrition * 0.25, sleep: -nutrition * 0.15 }); tarinai.energy = clamp(tarinai.energy - nutrition * 0.20, 0, 100); tarinai.fearTimer = Math.max(0, (tarinai.fearTimer || 0) - 0.18); tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 2.5); diff --git a/js/freeze_system.js b/js/freeze_system.js index 506a2a6..17356bd 100644 --- a/js/freeze_system.js +++ b/js/freeze_system.js @@ -183,7 +183,7 @@ const stress = Math.min(36, 10 + unique.length * 6 + relPriority * 8); if (t.enterPanic) t.enterPanic({ reason: `${unique[0]?.name || "誰か"}がいないことに気づいた`, fear: 2.4 + Math.min(2.0, unique.length * 0.35), stress, cause: "pending_relation_death" }); else { - t.stress = clamp((t.stress || 0) + stress, 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: stress * 1.8, social: stress * 1.2 }); t.fearTimer = Math.max(t.fearTimer || 0, 2.4 + Math.min(2.0, unique.length * 0.35)); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination() : null, reason: `${unique[0]?.name || "誰か"}がいないことに気づいた`, wake: true }); } diff --git a/js/health.js b/js/health.js index 690a16c..75f674f 100644 --- a/js/health.js +++ b/js/health.js @@ -134,6 +134,8 @@ const HEALTH = (() => { if (actualLoss > 0.01) { cause = rememberDamage(t, actualLoss, reason); t.world?.emit?.("tarinai:damaged", { tarinai: t, amount: actualLoss, cause, reason }); + const plushie = (t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id); + if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null); if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure(); audio.damage?.(actualLoss); if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f"); diff --git a/js/items.js b/js/items.js index 1989a4b..54e0cef 100644 --- a/js/items.js +++ b/js/items.js @@ -184,6 +184,14 @@ class Item { this.amount = itemAmountFor(type, 80); this.age = 0; this.seed = Math.random() * 1000; + this.roles = {}; + this.needEffects = { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 }; + if (isServingFoodType(type) || ["grass", "ant_corpse", "zunchi"].includes(type)) this.roles.food = true; + if (type === "water" || type === "water_bowl" || type === "zunda_juice") this.roles.drink = true; + if (["sweet", "water", "water_bowl", "zunda_juice"].includes(type) || isParamEffectItemType(type)) this.roles.medicine = true; + if (type === "bed" || type === "nest_box") this.roles.sleepPlace = true; + if (["firecracker", "genkotsu", "pushpin", "oshibyo", "zunchi", "splat"].includes(type)) this.roles.danger = true; + if (type === "grass") this.roles.grassMaterial = true; this.dropTimer = 0; this.dropMax = 0; this.dropImpactDone = false; @@ -412,7 +420,7 @@ class Item { t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.5); t.fearTimer = Math.max(t.fearTimer || 0, 0.55); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : null, reason: "高速ずんちにぶつかって吹き飛んでいる", wake: true }); - t.addStress ? t.addStress(7, { threshold: 8, duration: 3.2 }) : (t.stress = clamp((t.stress || 0) + 7, 0, 130)); + if (t.addStress) t.addStress(7, { threshold: 8, duration: 3.2 }); } worldRef.spawnFallEffect?.(t.x, t.y + (t.radius || 20) * 0.45, 0.65); worldRef.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 14, life: 0.38, color: "rgba(77,92,42,0.36)" })); @@ -693,7 +701,7 @@ class Item { t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2); t.fearTimer = Math.max(t.fearTimer || 0, 1.3); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(this) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }, reason: "画鋲が刺さってパニックになっている", wake: true }); - t.addStress ? t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 }) : (t.stress = clamp((t.stress || 0) + (behavior?.stressOnAttach ?? 18), 0, 130)); + if (t.addStress) t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 }); } if (t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "画鋲"); if ((worldRef.time || 0) >= (this.pinLogAt || -999) + 1.2) { @@ -730,7 +738,7 @@ class Item { this.pinDamageTick -= 0.55; if (t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "画鋲"); t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50); - t.addStress ? t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 }) : (t.stress = clamp((t.stress || 0) + (behavior?.stressPerTick ?? 2.6), 0, 130)); + if (t.addStress) t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 }); if (Math.random() < 0.22) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)"); } t.sleeping = false; diff --git a/js/relation_event_system.js b/js/relation_event_system.js index fcb3249..4d7cd80 100644 --- a/js/relation_event_system.js +++ b/js/relation_event_system.js @@ -30,7 +30,7 @@ observer.sleeping = false; observer.setActionState?.("panic", { target: observer.panicDestination ? observer.panicDestination(dead) : null, reason: `${relation}が死んだ`, wake: true }); observer.fearTimer = Math.max(observer.fearTimer || 0, fear); - observer.stress = clamp((observer.stress || 0) + stress, 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(observer, { safety: stress * 1.6, social: stress * 1.4 }); observer.bubble?.("!!", 1.0, "rgba(60,50,68,0.82)"); } observer.recordChangeCause?.(`${relation}の死亡`, "ストレス", { value: stress }); diff --git a/js/render.js b/js/render.js index 5ea4cb6..38f79f8 100644 --- a/js/render.js +++ b/js/render.js @@ -743,6 +743,7 @@ function render() { for (const it of world.items) { if (isCachedTerrainItem(it)) continue; if (isPinType(it.type) && it.pinState === "lodged") continue; + if (it.isStructure && it.type === "plushie" && it.carriedById) continue; if (it.type !== "zunchi" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); } for (const it of world.items) { @@ -769,6 +770,9 @@ function render() { for (const t of sorted) { if (isEntityVisibleInRect(t, visibleRect)) t.draw(ctx, world.time, lighting); } + for (const it of world.items) { + if (it.isStructure && it.type === "plushie" && it.carriedById && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); + } for (const it of world.items) { if (isPinType(it.type) && it.pinState === "lodged" && isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting); } diff --git a/js/structures.js b/js/structures.js new file mode 100644 index 0000000..59bb11d --- /dev/null +++ b/js/structures.js @@ -0,0 +1,214 @@ +"use strict"; + +(function (global) { + function createStructureId(prefix = "structure") { + return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`; + } + + function clonePlain(value) { + return JSON.parse(JSON.stringify(value || {})); + } + + class TarinaiStructure { + constructor(definition, owner, x, y, world) { + this.id = createStructureId("structure"); + this.type = definition.type; + this.label = definition.label; + this.isStructure = true; + this.ownerId = owner?.id || ""; + this.hp = definition.maxHp || 1; + this.maxHp = definition.maxHp || this.hp; + this.attachment = definition.type === "plushie" ? 30 : 20; + this.usedCount = 0; + this.x = x; + this.y = y; + this.r = definition.type === "plushie" ? 13 : 24; + this.amount = this.hp; + this.seed = Math.random() * 1000; + this.roles = clonePlain(definition.roles); + this.needEffects = clonePlain(definition.needEffects); + this.carriedById = definition.type === "plushie" ? this.ownerId : ""; + this.onHead = definition.type === "plushie"; + this.dead = false; + this.world = world || null; + } + + owner(worldRef = this.world) { + return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === this.ownerId) || null; + } + + update(dt, worldRef = this.world) { + this.world = worldRef || this.world; + if (this.dead || this.hp <= 0) { + this.amount = 0; + return; + } + if (this.type === "plushie") { + const owner = this.owner(worldRef); + if (!owner) { + this.damage(1, worldRef, null); + return; + } + this.x = owner.x; + this.y = owner.y - Math.max(18, (owner.radius || 24) * 0.78); + if (owner.needs) owner.needs.fulfill = Math.max(0, (owner.needs.fulfill || 0) - dt * 2.8); + } + this.amount = Math.max(0.001, this.hp); + } + + use(user, worldRef = this.world) { + const definition = global.StructureRegistry?.get?.(this.type); + return definition?.onUse?.(user, this, worldRef) || false; + } + + damage(amount = 1, worldRef = this.world, breaker = null) { + if (this.dead) return false; + this.hp = Math.max(0, (this.hp || 0) - Math.max(0, amount || 0)); + this.amount = this.hp; + if (this.hp > 0) return false; + this.dead = true; + this.amount = 0; + global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, breaker); + return true; + } + + draw(ctx, time = 0, lighting = {}) { + if (this.dead) return; + ctx.save(); + ctx.translate(this.x, this.y); + const night = (lighting?.light || 1) < 0.42; + if (this.type === "grass_bed") { + ctx.fillStyle = night ? "rgba(108,129,77,0.88)" : "rgba(134,174,83,0.92)"; + ctx.strokeStyle = night ? "rgba(55,73,49,0.72)" : "rgba(72,105,50,0.72)"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.ellipse(0, 4, this.r * 1.55, this.r * 0.62, -0.08, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + ctx.strokeStyle = night ? "rgba(185,203,142,0.35)" : "rgba(231,229,139,0.55)"; + for (let i = 0; i < 12; i++) { + const a = -0.8 + i * 0.145; + ctx.beginPath(); + ctx.moveTo(-this.r * 1.05 + i * this.r * 0.19, this.r * 0.38); + ctx.quadraticCurveTo(Math.sin(time + i) * 4, -this.r * 0.12, Math.cos(a) * this.r * 0.92, -this.r * 0.42 + Math.sin(i) * 4); + ctx.stroke(); + } + } else if (this.type === "plushie") { + const img = typeof getRenderableImage === "function" ? getRenderableImage("smile", "smile") : global.images?.get?.("smile"); + ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05); + if (img) { + const w = this.r * 2.2; + const metrics = typeof getImageMetrics === "function" ? getImageMetrics("smile") : null; + const h = w * (metrics?.ratio || 0.92); + ctx.drawImage(img, -w * 0.5, -h * 0.58, w, h); + } else { + ctx.fillStyle = "#f7dfc0"; + ctx.strokeStyle = "rgba(86,62,48,0.70)"; + ctx.lineWidth = 1.7; + ctx.beginPath(); + ctx.arc(0, 0, this.r, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = "rgba(52,43,38,0.88)"; + ctx.beginPath(); ctx.arc(-this.r * 0.28, -this.r * 0.08, 1.5, 0, Math.PI * 2); ctx.fill(); + ctx.beginPath(); ctx.arc(this.r * 0.28, -this.r * 0.08, 1.5, 0, Math.PI * 2); ctx.fill(); + } + } + ctx.restore(); + } + } + + const StructureRegistry = { + defs: {}, + register(definition) { this.defs[definition.type] = definition; return definition; }, + get(type) { return this.defs[type] || null; }, + create(type, owner, x, y, world) { + const definition = this.get(type); + if (!definition) return null; + return definition.create ? definition.create(owner, x, y, world) : new TarinaiStructure(definition, owner, x, y, world); + }, + }; + + function applyStructureNeedShock(owner, safetyGain, fulfillGain, breaker = null) { + if (!owner || owner.dead) return; + if (typeof global.applyNeedShock === "function") global.applyNeedShock(owner, { safety: safetyGain || 0, fulfill: fulfillGain || 0 }, breaker); + else { + owner.needShock = owner.needShock || {}; + owner.needShock.safety = Math.max(owner.needShock.safety || 0, safetyGain || 0); + owner.needShock.fulfill = Math.max(owner.needShock.fulfill || 0, fulfillGain || 0); + owner.lastNeedShockBreaker = breaker || null; + } + if (typeof global.triggerNeedShockReaction === "function") { + const reason = breaker ? "\u81ea\u5206\u306e\u3082\u306e\u304c\u58ca\u308c\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b" : "\u81ea\u5206\u306e\u3082\u306e\u304c\u58ca\u308c\u3066\u3044\u308b"; + global.triggerNeedShockReaction(owner, breaker, reason); + } + } + + function damageNearbyStructures(worldRef, x, y, radius, amount, breaker = null) { + let hit = 0; + for (const structure of worldRef?.nearbyItems?.(x, y, radius + 80) || worldRef?.items || []) { + if (!structure?.isStructure || structure.dead) continue; + const d = distXY(x, y, structure.x, structure.y); + const p = clamp(1 - d / Math.max(1, radius + (structure.r || 12)), 0, 1); + if (p <= 0) continue; + if (structure.damage?.(Math.max(1, amount * (0.35 + p * 0.65)), worldRef, breaker)) hit++; + } + return hit; + } + + StructureRegistry.register({ + 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, world) { return new TarinaiStructure(this, owner, x, y, world); }, + onUse(user, structure) { + const ownerUse = user?.id && user.id === structure.ownerId; + if (ownerUse) structure.attachment = Math.min(100, (structure.attachment || 0) + 2); + structure.usedCount = (structure.usedCount || 0) + 1; + if (user) { + user.needShock = user.needShock || {}; + user.needShock.sleep = Math.min(0, (user.needShock.sleep || 0) - (ownerUse ? 18 : 8)); + user.needShock.safety = Math.min(0, (user.needShock.safety || 0) - (ownerUse ? 12 : 4)); + user.needShock.fulfill = Math.min(0, (user.needShock.fulfill || 0) - (ownerUse ? 18 : 6)); + } + return true; + }, + onDestroyed(structure, world, breaker) { + applyStructureNeedShock(structure.owner(world), 42, 46, breaker); + }, + }); + + StructureRegistry.register({ + 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, world) { return new TarinaiStructure(this, owner, x, y, world); }, + onUse(user, structure) { + if (user?.id === structure.ownerId) structure.attachment = Math.min(100, (structure.attachment || 0) + 1); + structure.usedCount = (structure.usedCount || 0) + 1; + if (user?.id === structure.ownerId) { + user.needShock = user.needShock || {}; + user.needShock.fulfill = Math.min(0, (user.needShock.fulfill || 0) - 24); + user.needShock.safety = Math.min(0, (user.needShock.safety || 0) - 4); + } + return true; + }, + onDestroyed(structure, world, breaker) { + applyStructureNeedShock(structure.owner(world), 55, 64, breaker); + }, + }); + + global.TarinaiStructure = TarinaiStructure; + global.StructureRegistry = StructureRegistry; + global.damageNearbyStructures = damageNearbyStructures; +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/tarinai.js b/js/tarinai.js index d3cd670..4862e29 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -53,7 +53,12 @@ class Tarinai { constructor(world, opts = {}) { this.hunger = opts.hunger ?? rand(16, 45); this.loneliness = opts.loneliness ?? rand(18, 62); this.energy = opts.energy ?? rand(54, 98); - this.stress = opts.stress ?? rand(0, 20); + this.needs = opts.needs && typeof opts.needs === "object" && typeof createDefaultNeeds === "function" ? { ...createDefaultNeeds(), ...opts.needs } : (typeof createDefaultNeeds === "function" ? createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 }); + this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needs) : 0; + this.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" && typeof createDefaultNeeds === "function" ? { ...createDefaultNeeds(), ...opts.previousNeeds } : { ...this.needs }; + this.intent = opts.intent && typeof opts.intent === "object" ? { ...opts.intent } : null; + this.needShock = opts.needShock && typeof opts.needShock === "object" ? { ...opts.needShock } : {}; + this.buildPlan = opts.buildPlan && typeof opts.buildPlan === "object" ? { ...opts.buildPlan } : null; this.hpBarTimer = opts.hpBarTimer ?? 0; this.stressBarTimer = opts.stressBarTimer ?? 0; this.affection = opts.affection ?? rand(0, 30); diff --git a/js/tarinai_action_state.js b/js/tarinai_action_state.js index c5665cb..5c30997 100644 --- a/js/tarinai_action_state.js +++ b/js/tarinai_action_state.js @@ -58,18 +58,30 @@ const profile = this.personalityProfile ? this.personalityProfile() : { fear: 1 }; const fearMul = opts.fearMultiplier ?? profile.fear ?? 1; const threat = opts.threat ?? opts.target ?? null; - let target = null; - if ("target" in opts) target = opts.target; - else if (opts.destination) target = opts.destination; - else if (this.panicDestination) target = this.panicDestination(threat, !!opts.forceDestination); - this.setActionState("panic", { - target, - reason: opts.reason || this.thought || "パニックになっている", - wake: opts.wake !== false, - sleeping: false, - }); - if (Number.isFinite(opts.fearTimer)) this.fearTimer = Math.max(this.fearTimer || 0, opts.fearTimer * fearMul); - else this.fearTimer = Math.max(this.fearTimer || 0, (opts.fear ?? 0.55) * fearMul); + const safetyShock = Math.max(40, (opts.fearTimer ?? opts.fear ?? 0.55) * 32 * fearMul, Number(opts.stress || 0) * 1.8); + const healthShock = Math.max(0, Number(opts.hurtTimer || 0) > 0 ? 45 : 0); + if (typeof applyNeedShock === "function") { + applyNeedShock(this, { safety: safetyShock, health: healthShock }, threat); + } else { + this.needShock = this.needShock || {}; + this.needShock.safety = Math.max(this.needShock.safety || 0, safetyShock); + if (healthShock > 0) this.needShock.health = Math.max(this.needShock.health || 0, healthShock); + } + let handled = false; + if (typeof triggerNeedShockReaction === "function") handled = triggerNeedShockReaction(this, threat, opts.reason || "パニックになっている"); + if (!handled) { + let target = null; + if ("target" in opts) target = opts.target; + else if (opts.destination) target = opts.destination; + else if (this.panicDestination) target = this.panicDestination(threat, !!opts.forceDestination); + this.setActionState("panic", { + target, + reason: opts.reason || this.thought || "パニックになっている", + wake: opts.wake !== false, + sleeping: false, + }); + this.fearTimer = Math.max(this.fearTimer || 0, 2.4); + } if (Number.isFinite(opts.surpriseTimer)) this.surpriseTimer = Math.max(this.surpriseTimer || 0, opts.surpriseTimer); if (Number.isFinite(opts.hurtTimer)) this.hurtTimer = Math.max(this.hurtTimer || 0, opts.hurtTimer); if (Number.isFinite(opts.awakeLockTimer)) this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, opts.awakeLockTimer); @@ -77,10 +89,6 @@ this.fallTimer = Math.max(this.fallTimer || 0, opts.fallTimer); this.fallMax = Math.max(this.fallMax || 0, this.fallTimer); } - if (Number.isFinite(opts.stress) && opts.stress > 0) { - if (this.addStress) this.addStress(opts.stress, { threshold: opts.stressThreshold ?? 8, duration: opts.stressDuration ?? 4.2 }); - else this.stress = clamp((this.stress || 0) + opts.stress, 0, 130); - } if (opts.bubble) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)"); this.lastPanicCause = opts.cause || opts.reason || this.thought || "panic"; this.lastPanicAt = this.world?.time || 0; diff --git a/js/tarinai_disease_nest.js b/js/tarinai_disease_nest.js index 5bbec40..fa06865 100644 --- a/js/tarinai_disease_nest.js +++ b/js/tarinai_disease_nest.js @@ -26,7 +26,7 @@ this.zunchiDiseaseSeverity = 0; this.zunchiDiseaseCooldown = 18; this.zunchiStain = clamp((this.zunchiStain || 0) - 35, 0, 100); - this.stress = clamp(this.stress - 8, 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -18, safety: -6 }); this.thought = reason || "\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f"; audio.heal?.(); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(62,104,62,0.78)"); @@ -61,7 +61,7 @@ } if (this.zunchiDisease) { this.zunchiDiseaseSeverity = clamp((this.zunchiDiseaseSeverity || 0.7) + dt * 0.004, 0, 1.25); - this.stress = clamp(this.stress + dt * (0.28 + 0.18 * this.zunchiDiseaseSeverity), 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * (5 + 4 * this.zunchiDiseaseSeverity) }); this.energy = clamp(this.energy - dt * 0.035 * this.zunchiDiseaseSeverity, 0, 100); if (this.energy > 82) this.zunchiDiseaseSeverity = Math.max(0, this.zunchiDiseaseSeverity - dt * (this.energy > 94 ? 0.020 : 0.010)); this.emitZunchiDiseaseEffect(dt); @@ -136,7 +136,7 @@ this.explosionDiseaseTimer = 0; this.thought = reason || "\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f"; audio.heal?.(); - this.stress = clamp(this.stress - 10, 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -22, safety: -8 }); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)"); this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] }); this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_explosion", reason }); @@ -168,7 +168,7 @@ this.fightDiseaseCooldown = 20; this.thought = reason || "\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"; audio.heal?.(); - this.stress = clamp(this.stress - 12, 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -24, safety: -10 }); this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)"); this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] }); this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_fight", reason }); @@ -311,7 +311,7 @@ applyWaterEffect(dt, source = "water") { const power = Math.max(0, dt || 0); - this.stress = clamp(this.stress - power * 2.4, 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -power * 12, safety: -power * 6 }); this.energy = clamp(this.energy + power * 0.5, 0, 100); this.hunger = clamp(this.hunger - power * 0.2, 0, 115); this.zunchiStain = clamp((this.zunchiStain || 0) - power * 10.5, 0, 100); @@ -329,7 +329,7 @@ this.fightDiseaseCooldown = Math.max(0, (this.fightDiseaseCooldown || 0) - dt); if (this.explosionDisease) { this.explosionDiseaseTimer = Math.max(0, (this.explosionDiseaseTimer || 0) - dt); - this.addStress ? this.addStress(dt * 0.22, { threshold: 9, duration: 3.6 }) : (this.stress = clamp(this.stress + dt * 0.22, 0, 130)); + if (this.addStress) this.addStress(dt * 0.22, { threshold: 9, duration: 3.6 }); if (Math.random() < dt * 2.1) { const a = rand(0, Math.PI * 2); this.world?.effects?.push(new Effect("explosion", this.x + Math.cos(a) * this.radius * rand(0.2, 0.9), this.y + Math.sin(a) * this.radius * rand(0.1, 0.8), { @@ -353,7 +353,7 @@ this.recoverFightDisease("\u81ea\u7136\u306b\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"); } if (this.fightDisease) { - this.stress = clamp(this.stress + dt * 0.16, 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 3, safety: dt * 2 }); if (Math.random() < dt * 0.30) this.wanderAngle += rand(-2.8, 2.8); if (Math.random() < dt * 0.08) this.bubble("?", 2.6, "rgba(150,78,44,0.80)"); } diff --git a/js/tarinai_identity_social.js b/js/tarinai_identity_social.js index 6d0776b..c50f81f 100644 --- a/js/tarinai_identity_social.js +++ b/js/tarinai_identity_social.js @@ -147,7 +147,7 @@ const changedA = this.adjustPersonality?.("neuroticism", 0.025, "after being hurt") || 0; const changedB = this.adjustPersonality?.("aggression", -0.015, "after being hurt") || 0; this.fearTimer = Math.max(this.fearTimer || 0, 1.2); - this.stress = clamp((this.stress || 0) + 5, 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 9 }); return Boolean(changedA || changedB); }, @@ -256,9 +256,15 @@ const tolerance = Number.isFinite(opts.tolerance) ? opts.tolerance : 0.88; const delta = Math.max(0, (amount || 0) * tolerance); if (delta <= 0) return 0; + if (!this.needShock) this.needShock = {}; + const shock = Math.min(100, delta * 1.8); + this.needShock.safety = Math.max(this.needShock.safety || 0, shock); const before = this.stress || 0; - this.stress = clamp(before + delta, 0, 130); - const gained = Math.max(0, this.stress - before); + if (this.needs && typeof calculateStressFromNeeds === "function") { + const nextSafety = typeof quantizeNeed === "function" ? quantizeNeed((this.needs.safety || 0) + shock) : Math.min(100, (this.needs.safety || 0) + shock); + this.stress = calculateStressFromNeeds({ ...this.needs, safety: nextSafety }); + } + const gained = Math.max(delta, Math.max(0, (this.stress || 0) - before)); const threshold = opts.threshold ?? 8; if (gained >= threshold) this.showStressBar(opts.duration ?? 4.2); return gained; diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index fc9170d..03180e7 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -1,5 +1,324 @@ "use strict"; +const TARINAI_NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"]; +const TARINAI_NEED_PRIORITY = ["safety", "health", "food", "sleep", "social", "fulfill"]; +const TARINAI_NEED_LABELS = { + food: "\u6442\u990c", + sleep: "\u7761\u7720", + health: "\u5065\u5eb7", + safety: "\u5b89\u5168", + social: "\u95a2\u4fc2", + fulfill: "\u5145\u8db3", +}; +const TARINAI_NEED_PHRASES = { + 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", +}; + +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.max(0, Math.min(10, Math.round((Number(value) || 0) / 10))); +} + +function needPressure(value) { + const x = Math.max(0, Math.min(100, Number(value) || 0)) / 100; + return x * x * 100; +} + +function calculateStressFromNeeds(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 applyNeedShock(tarinai, deltas = {}, breaker = null) { + if (!tarinai || tarinai.dead) return false; + tarinai.needShock = tarinai.needShock || {}; + for (const key of TARINAI_NEED_KEYS) { + const delta = Number(deltas[key] || 0) || 0; + if (delta > 0) tarinai.needShock[key] = Math.max(tarinai.needShock[key] || 0, delta); + else if (delta < 0) tarinai.needShock[key] = Math.min(tarinai.needShock[key] || 0, delta); + } + if (breaker) tarinai.lastNeedShockBreaker = breaker; + if (tarinai.needs && typeof calculateStressFromNeeds === "function") { + const projected = { ...tarinai.needs }; + for (const key of TARINAI_NEED_KEYS) projected[key] = quantizeNeed((projected[key] || 0) + (tarinai.needShock[key] || 0)); + tarinai.stress = calculateStressFromNeeds(projected); + } + return true; +} + +function applyNeedRelief(tarinai, deltas = {}) { + return applyNeedShock(tarinai, deltas, null); +} + +function triggerNeedShockReaction(tarinai, breaker = null, reason = "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b") { + if (!tarinai || tarinai.dead) return false; + const before = tarinai.previousNeeds || createDefaultNeeds(); + const projected = { ...(tarinai.needs || createDefaultNeeds()) }; + for (const key of TARINAI_NEED_KEYS) projected[key] = quantizeNeed((projected[key] || 0) + (tarinai.needShock?.[key] || 0)); + const safetyDelta = (projected.safety || 0) - (before.safety || 0); + const healthDelta = (projected.health || 0) - (before.health || 0); + const fulfillDelta = (projected.fulfill || 0) - (before.fulfill || 0); + const shockTriggered = safetyDelta >= 40 || healthDelta >= 45 || fulfillDelta >= 50; + if (!shockTriggered) return false; + if (["panic", "intimidate", "fight", "birth_ritual"].includes(tarinai.state)) return false; + const aggression = Number(tarinai.currentPersonality?.aggression || 0); + if (breaker && aggression > 0.6) { + tarinai.setActionState?.("intimidate", { target: breaker, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", wake: true, sleeping: false }); + tarinai.intimidateTimer = Math.max(tarinai.intimidateTimer || 0, 2.0); + } else { + tarinai.setActionState?.("panic", { target: tarinai.panicDestination?.(breaker), reason, wake: true, sleeping: false }); + tarinai.fearTimer = Math.max(tarinai.fearTimer || 0, 2.6); + } + tarinai.lastNeedShockBreaker = null; + return true; +} + +function chooseTopNeedRandom(needs) { + const max = Math.max(...TARINAI_NEED_KEYS.map(key => Number(needs?.[key]) || 0)); + const tiedNeeds = TARINAI_NEED_KEYS.filter(key => (Number(needs?.[key]) || 0) === max); + return { need: pick(tiedNeeds), tiedNeeds, max }; +} + +function buildReasonText(chosenNeed, tiedNeeds, action) { + const chosenPhrase = action?.phrase || "\u5f85\u3063\u305f"; + const tied = Array.isArray(tiedNeeds) ? tiedNeeds.filter(key => key !== chosenNeed) : []; + if (tied.length) { + const ignored = TARINAI_NEED_PRIORITY.find(key => tied.includes(key)) || tied[0]; + return `${TARINAI_NEED_PHRASES[ignored] || ignored}\u3051\u3069\u3001${chosenPhrase}\u3002`; + } + return `${TARINAI_NEED_PHRASES[chosenNeed] || chosenNeed}\u306e\u3067\u3001${chosenPhrase}\u3002`; +} + +function roleMatches(item, role) { + if (!item || item.dead || (item.amount != null && item.amount <= 0)) return false; + if (item.roles?.[role]) return true; + if (role === "food") return ["sweet", "food", "grass", "ant_corpse", "zunchi"].includes(item.type) || (typeof isServingFoodType === "function" && isServingFoodType(item.type)); + if (role === "drink") return item.type === "water" || item.type === "water_bowl" || item.type === "zunda_juice"; + if (role === "medicine") return item.type === "sweet" || item.type === "water" || item.type === "water_bowl" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(item.type)); + if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box"; + if (role === "danger") return ["firecracker", "genkotsu", "pushpin", "oshibyo", "zunchi", "splat"].includes(item.type); + if (role === "grassMaterial") return item.type === "grass"; + return false; +} + +function findNearestItemWithRole(world, tarinai, role, maxDist = 520) { + let best = null, bestD = maxDist; + for (const item of world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist) || world?.items || []) { + if (!roleMatches(item, role)) continue; + if (tarinai?.shouldAvoidTarget?.(item)) continue; + const d = dist(tarinai, item); + if (d < bestD) { best = item; bestD = d; } + } + return best; +} + +function findNearestSleepPlace(world, tarinai, maxDist = 520) { + const own = findOwnedStructure(world, tarinai, "grass_bed", maxDist); + return own || world?.bestBedFor?.(tarinai, maxDist) || findNearestItemWithRole(world, tarinai, "sleepPlace", maxDist); +} + +function findNearbyDanger(world, tarinai, maxDist = 150) { + const ant = (world?.nearbyAnts?.(tarinai.x, tarinai.y, maxDist, true) || [])[0] || null; + const item = findNearestItemWithRole(world, tarinai, "danger", maxDist); + if (ant && item) return distXY(tarinai.x, tarinai.y, ant.x, ant.y) <= dist(tarinai, item) ? ant : item; + return ant || item; +} + +function findNearbyMaterial(world, tarinai, materialRole, maxDist = 520) { + return findNearestItemWithRole(world, tarinai, materialRole, maxDist); +} + +function findOwnedStructure(world, tarinai, type = "", maxDist = Infinity) { + let best = null, bestD = maxDist; + for (const item of world?.items || []) { + if (!item?.isStructure || item.dead || item.ownerId !== tarinai.id) continue; + if (type && item.type !== type) continue; + const d = dist(tarinai, item); + if (d < bestD) { best = item; bestD = d; } + } + return best; +} + +function consumeGrassMaterial(material) { + if (!material) return false; + material.amount = Math.max(0, (material.amount || 0) - 18); + material.eatenAmount = (material.eatenAmount || 0) + 18; + if (Number.isFinite(material.growth)) material.growth = clamp(material.growth - 0.18, 0.04, 1.2); + return true; +} + +function moveToOrUse(t, target, state = "seek_food", reason = "") { + if (!target) return false; + t.setActionState?.(state, { target, reason, sleeping: false }); + return true; +} + +if (typeof window !== "undefined") { + Object.assign(window, { createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, chooseActionForNeed, buildReasonText, getNeedDisplayValue, findNearestItemWithRole, findNearestSleepPlace, findNearbyDanger, findNearbyMaterial, calculateStressFromNeeds, applyNeedShock, applyNeedRelief, triggerNeedShockReaction }); +} + +function updateNeeds(tarinai, world) { + const p = tarinai.currentPersonality || {}; + const aggression = Number(p.aggression) || 0; + const openness = Number(p.openness) || 0; + const sociability = Number(p.sociability) || 0; + const neuroticism = Number(p.neuroticism) || 0; + const needs = createDefaultNeeds(); + needs.food = (tarinai.hunger || 0) * 0.88; + needs.sleep = Math.max(0, 100 - (tarinai.energy || 0)); + const dayP = world?.dayProgress?.() || 0; + const night = dayP >= 0.62 || dayP < 0.18; + if (night) needs.sleep += 10; + needs.health = Math.max(0, (100 - (tarinai.energy || 100)) * 0.18 + (tarinai.zunchiStain || 0) * 0.32); + if (tarinai.zunchiDisease || tarinai.sleepDisease || tarinai.explosionDisease || tarinai.fightDisease) needs.health += 42; + if ((tarinai.hurtTimer || 0) > 0.04 || (tarinai.stuckPushpinId || "")) needs.health += 36; + const danger = findNearbyDanger(world, tarinai, 170); + if (danger) needs.safety += danger.kind ? 78 : 48; + if ((tarinai.fearTimer || 0) > 0.12 || (tarinai.lastDamageAt || -999) + 5 > (world?.time || 0)) needs.safety += 34; + needs.social = (tarinai.loneliness || 0) * (0.88 + Math.max(0, sociability) * 0.16); + if (tarinai.parentToFollow?.()) needs.social += 18; + if ((tarinai.reproductionTimer || 0) <= 8 && !tarinai.hasPaired) needs.social += 16; + const enemy = tarinai.strongestRelation?.("fear"); + if (enemy?.score > 12) needs.social += 10; + needs.fulfill = 28 + (openness > 0 ? openness * 18 : 0); + if (!findOwnedStructure(world, tarinai, "grass_bed")) needs.fulfill += 20; + const ownBed = findOwnedStructure(world, tarinai, "grass_bed"); + const ownPlushie = findOwnedStructure(world, tarinai, "plushie"); + if (ownBed) { + needs.sleep -= dist(tarinai, ownBed) < 80 ? 16 : 6; + needs.safety -= dist(tarinai, ownBed) < 100 ? 14 : 4; + needs.fulfill -= dist(tarinai, ownBed) < 120 ? 22 : 8; + } + if (!ownPlushie) needs.fulfill += 16; + else needs.fulfill -= ownPlushie.carriedById === tarinai.id ? 28 : 14; + if (findNearestItemWithRole(world, tarinai, "grassMaterial", 420)) needs.fulfill += 12; + for (const key of TARINAI_NEED_KEYS) { + const shock = Number(tarinai.needShock?.[key] || 0) || 0; + if (shock !== 0) needs[key] += shock; + } + needs.safety *= clamp(1 - Math.max(0, aggression) * 0.12 + Math.max(0, neuroticism) * 0.16, 0.75, 1.25); + needs.health *= clamp(1 + Math.max(0, neuroticism) * 0.14, 0.9, 1.22); + needs.fulfill *= clamp(1 + Math.max(0, openness) * 0.14, 0.9, 1.24); + for (const key of TARINAI_NEED_KEYS) needs[key] = quantizeNeed(needs[key]); + tarinai.previousNeeds = tarinai.needs ? { ...tarinai.needs } : createDefaultNeeds(); + tarinai.needs = needs; + tarinai.stress = calculateStressFromNeeds(needs); + if (tarinai.needShock) { + for (const key of TARINAI_NEED_KEYS) { + const shock = Number(tarinai.needShock[key] || 0) || 0; + tarinai.needShock[key] = shock > 0 ? Math.max(0, shock - 12) : Math.min(0, shock + 12); + } + } + return needs; +} + +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) { return !!findNearestItemWithRole(world, t, "food", 560); }, run(t, world) { return moveToOrUse(t, findNearestItemWithRole(world, t, "food", 560), "seek_food", this.label); } }, + { id: "drink_water", need: "food", label: "\u6c34\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u6c34\u3092\u98f2\u3093\u3060", weight: 24, condition(t, world) { return !!findNearestItemWithRole(world, t, "drink", 560); }, run(t, world) { return moveToOrUse(t, findNearestItemWithRole(world, t, "drink", 560), "seek_water", this.label); } }, + { 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) { return !!findNearestSleepPlace(world, t, 560); }, run(t, world) { const bed = findNearestSleepPlace(world, t, 560); if (bed && dist(t, bed) > (t.radius || 20) + 18) return moveToOrUse(t, bed, "seek_bed", this.label); bed?.use?.(t, world); t.startSleeping?.(bed, this.label); return true; } }, + { id: "sleep_anywhere", need: "sleep", label: "\u305d\u306e\u5834\u3067\u4f11\u3093\u3067\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 18, condition() { return true; }, run(t) { t.startSleeping?.(null, this.label); return true; } }, + { id: "use_medicine", need: "health", label: "\u56de\u5fa9\u3067\u304d\u308b\u3082\u306e\u3092\u63a2\u3057\u3066\u3044\u308b", phrase: "\u56de\u5fa9\u3057\u305f", weight: 34, condition(t, world) { return !!findNearestItemWithRole(world, t, "medicine", 620); }, run(t, world) { return moveToOrUse(t, findNearestItemWithRole(world, t, "medicine", 620), "seek_food", this.label); } }, + { id: "rest_to_recover", need: "health", label: "\u56de\u5fa9\u306e\u305f\u3081\u4f11\u3093\u3067\u3044\u308b", phrase: "\u4f11\u3093\u3060", weight: 24, condition() { return true; }, run(t) { t.setActionState?.("idle", { target: null, reason: this.label, sleeping: false }); t.energy = clamp((t.energy || 0) + 1.6, 0, 100); return true; } }, + { id: "flee", need: "safety", label: "\u5371\u306a\u3044\u3082\u306e\u304b\u3089\u96e2\u308c\u3066\u3044\u308b", phrase: "\u96e2\u308c\u305f", weight: 40, condition(t, world) { return !!findNearbyDanger(world, t, 220); }, run(t, world) { const danger = findNearbyDanger(world, t, 220); t.setActionState?.("flee", { target: t.panicDestination?.(danger), reason: this.label, wake: true, sleeping: false }); return true; } }, + { id: "hide_at_owned_structure", need: "safety", label: "\u81ea\u5206\u306e\u5834\u6240\u306b\u96a0\u308c\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f", weight: 30, condition(t, world) { return !!findOwnedStructure(world, t, "grass_bed", 620); }, run(t, world) { return moveToOrUse(t, findOwnedStructure(world, t, "grass_bed", 620), "seek_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) { return !!(t.bestFriendLive?.(FRIEND_AFFINITY_THRESHOLD, 520) || world.nearestOther?.(t, 420)); }, run(t, world) { const other = t.bestFriendLive?.(FRIEND_AFFINITY_THRESHOLD, 520) || world.nearestOther?.(t, 420); return moveToOrUse(t, other, "seek_friend", this.label); } }, + { id: "approach_parent_or_child", need: "social", label: "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b", phrase: "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u305f", weight: 28, condition(t) { return !!t.parentToFollow?.(); }, run(t) { return moveToOrUse(t, t.parentToFollow?.(), "follow_parent", this.label); } }, + { id: "approach_mate", need: "social", label: "\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b", phrase: "\u76f8\u624b\u306b\u8fd1\u3065\u3044\u305f", weight: 24, condition(t, world) { return (t.reproductionTimer || 0) <= 8 && !!world.nearestOther?.(t, 440, o => !world.areParentChild?.(t, o) && !!t.isZunchiSlave === !!o.isZunchiSlave && !o.sleepDisease && !o.fightDisease); }, run(t, world) { const other = world.nearestOther?.(t, 440, o => !world.areParentChild?.(t, o) && !!t.isZunchiSlave === !!o.isZunchiSlave && !o.sleepDisease && !o.fightDisease); 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); } }, + { 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: 12, condition(t) { return !!t.recentFightRival?.(80, 180); }, run(t) { const rival = t.recentFightRival?.(80, 180); t.setActionState?.("intimidate", { target: rival, reason: this.label, sleeping: false }); t.intimidateTimer = Math.max(t.intimidateTimer || 0, 1.6); return true; } }, + { id: "play", need: "fulfill", label: "\u904a\u3093\u3067\u3044\u308b", phrase: "\u904a\u3093\u3060", weight: 24, condition(t, world) { return !!world.nearest?.(t, ["ball"], 520); }, run(t, world) { return moveToOrUse(t, world.nearest?.(t, ["ball"], 520), "play_ball", this.label); } }, + { id: "build_grass_bed", need: "fulfill", label: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u3066\u3044\u308b", phrase: "\u8349\u306e\u7c21\u6613\u30d9\u30c3\u30c9\u3092\u4f5c\u3063\u305f", weight: 34, condition(t, world) { return !findOwnedStructure(world, t, "grass_bed") && !!findNearbyMaterial(world, t, "grassMaterial", 520) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, run(t, world) { return buildOwnedStructureFromGrass(t, world, "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: 28, condition(t, world) { return !findOwnedStructure(world, t, "plushie") && !!findNearbyMaterial(world, t, "grassMaterial", 520) && !["sleep", "fight", "panic", "intimidate"].includes(t.state); }, run(t, world) { return buildOwnedStructureFromGrass(t, world, "plushie", this.label); } }, + { id: "return_owned_structure", need: "fulfill", label: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u3066\u3044\u308b", phrase: "\u81ea\u5206\u306e\u5834\u6240\u3078\u623b\u3063\u305f", weight: 22, condition(t, world) { return !!findOwnedStructure(world, t, "grass_bed", 620); }, run(t, world) { return moveToOrUse(t, findOwnedStructure(world, t, "grass_bed", 620), "seek_bed", this.label); } }, + { id: "use_plushie", need: "fulfill", label: "\u306c\u3044\u3050\u308b\u307f\u3092\u62b1\u3048\u3066\u3044\u308b", phrase: "\u306c\u3044\u3050\u308b\u307f\u3092\u62b1\u3048\u305f", weight: 18, condition(t, world) { return !!findOwnedStructure(world, t, "plushie"); }, run(t, world) { const plushie = findOwnedStructure(world, t, "plushie"); plushie?.use?.(t, world); t.setActionState?.("idle", { target: plushie, reason: this.label, sleeping: false }); return true; } }, + { id: "wander_lightly", need: "fulfill", label: "\u5c11\u3057\u6b69\u3044\u3066\u3044\u308b", phrase: "\u5c11\u3057\u6b69\u3044\u305f", weight: 14, condition() { return true; }, run(t) { t.wanderAngle += rand(-1.7, 1.7); t.goIdle?.(this.label); return true; } }, +]; + +function buildOwnedStructureFromGrass(t, world, type, label) { + const material = findNearbyMaterial(world, t, "grassMaterial", 520); + if (!material || !globalThis.StructureRegistry) return false; + t.buildPlan = { + type, + materialId: material.id, + timer: Math.max(0.4, t.buildPlan?.type === type ? Number(t.buildPlan.timer || 0) : 4), + }; + if (dist(t, material) > (t.radius || 20) + (material.r || 12) + 18) return moveToOrUse(t, material, "seek_material", label); + t.setActionState?.("build", { target: material, reason: label, sleeping: false }); + return true; +} + +function continueBuildPlan(t, world, dt) { + const plan = t.buildPlan; + if (!plan?.type || !globalThis.StructureRegistry) return false; + if (findOwnedStructure(world, t, plan.type)) { + t.buildPlan = null; + return false; + } + let material = (world?.items || []).find(item => item && !item.dead && item.id === plan.materialId && roleMatches(item, "grassMaterial")); + if (!material) material = findNearbyMaterial(world, t, "grassMaterial", 520); + if (!material) { + t.buildPlan = null; + return false; + } + plan.materialId = material.id; + const label = globalThis.StructureRegistry.get(plan.type)?.label || "\u69cb\u9020\u7269"; + if (dist(t, material) > (t.radius || 20) + (material.r || 12) + 18) { + moveToOrUse(t, material, "seek_material", `${label}\u3092\u4f5c\u3063\u3066\u3044\u308b`); + return true; + } + plan.timer = Math.max(0, Number(plan.timer || 0) - Math.max(0.05, dt || 0.1)); + t.setActionState?.("build", { target: material, reason: `${label}\u3092\u4f5c\u3063\u3066\u3044\u308b`, sleeping: false }); + if (plan.timer > 0) return true; + consumeGrassMaterial(material); + const structure = globalThis.StructureRegistry.create(plan.type, t, t.x + rand(-18, 18), t.y + rand(16, 28), world); + if (!structure) { + t.buildPlan = null; + return false; + } + world.items.push(structure); + world.drawListDirty = true; + structure.use?.(t, world); + t.setActionState?.("idle", { target: structure, reason: `${label}\u3092\u4f5c\u3063\u305f`, sleeping: false }); + applyNeedRelief(t, { fulfill: -(plan.type === "plushie" ? 50 : 36), safety: plan.type === "plushie" ? -4 : -8 }); + t.buildPlan = null; + return true; +} + +function weightedPickAction(actions) { + const total = actions.reduce((sum, action) => sum + Math.max(0.01, Number(action.weight) || 1), 0); + let roll = Math.random() * total; + for (const action of actions) { + roll -= Math.max(0.01, Number(action.weight) || 1); + if (roll <= 0) return action; + } + return actions[0] || null; +} + +function chooseActionForNeed(need, tarinai, world) { + const actions = TARINAI_ACTIONS.filter(action => action.need === need && (!action.condition || action.condition(tarinai, world))); + return weightedPickAction(actions) || TARINAI_ACTIONS.find(action => action.id === "wander_lightly"); +} + (function (global) { const Tarinai = global.Tarinai; if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_needs_items.js"); @@ -56,7 +375,7 @@ if (this.fightDisease) return "stress_dizzy"; if ((this.oshiriByoZunchiStock || 0) >= 6) return this.variantSprite ? this.variantSprite("hurt") : "weak"; if (this.isZunchiSlave) return this.zunchiSlaveSpriteId(); - if (this.type === "cry" && (this.mood < 26 || this.stress > 78 || this.hunger > 96 || this.energy < 12)) return "cry"; + if (this.type === "cry" && (this.stress > 78 || this.hunger > 96 || this.energy < 12 || (this.needs?.safety || 0) >= 90)) return "cry"; if ((this.state === "eat" || this.eatTimer > 0.04) && this.target?.type === "sweet") return "zunda_eat"; if ((this.state === "seek_food" || this.foodReactTimer > 0.04) && this.target?.type === "sweet" && dist(this, this.target) < 80) return "zunda_eat"; if (this.hurtTimer > 0.08 || this.pokeFlashTimer > 0.08) return this.variantSprite("hurt"); @@ -68,7 +387,7 @@ if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate"); if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear"); if (this.state === "fight" || this.fightTimer > 0.04) return "angry"; - if (this.stress > 62 || (this.stress > 50 && this.mood < 46)) return this.variantSprite("stress"); + if (this.stress > 62) return this.variantSprite("stress"); if (low) return low; if (this.state === "sleep" || this.energy < 18) return this.variantSprite("sleep"); if (this.surpriseTimer > 0.08) return "pokan"; @@ -76,9 +395,9 @@ if (this.state === "eat" || this.eatTimer > 0.04) return this.goodMode; if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back"; if (this.state === "seek_food") return "drool"; - if (this.mood >= 66 && this.stress < 24 && this.hunger < 58 && this.loneliness < 62) return this.variantSprite("low_stress"); - if (this.mood >= 63 && this.stress < 44 && this.hunger < 58) return this.goodMode; - if (this.mood < 38 || this.hunger > 72 || this.loneliness > 74) return "angry"; + if (this.stress < 24 && (this.needs?.food || 0) < 60 && (this.needs?.social || 0) < 60 && (this.needs?.fulfill || 0) < 60) return this.variantSprite("low_stress"); + if (this.stress < 44 && (this.needs?.food || 0) < 60) return this.goodMode; + if ((this.needs?.food || 0) > 70 || (this.needs?.social || 0) > 80) return "angry"; return this.goodMode || this.variantSprite("normal"); }, @@ -104,8 +423,8 @@ if (this.dead || this.sleepDisease || this.sleeping || this.state === "sleep" || this.state === "seek_bed") return false; if (["fight", "panic", "cursor_enemy", "hurt", "defeated", "fight_sick"].includes(this.state)) return false; if ((this.fearTimer || 0) > 0.18 || (this.intimidatedTimer || 0) > 0.04 || (this.intimidateTimer || 0) > 0.04) return false; - if (this.mood < 46 || this.stress > 64) return false; - return this.affection > 12 || this.mood > 58 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1); + if (this.stress > 64 || (this.needs?.safety || 0) >= 70) return false; + return (this.needs?.social || 0) < 70 || this.goodMode === "smile" || this.shouldApplyPersonalityBehavior("sociability", 1); }, sunbathSpriteId() { @@ -135,24 +454,24 @@ const sick = this.zunchiDisease || this.sleepDisease || this.explosionDisease || this.fightDisease; if (!sick || Math.random() >= 0.75) return false; let healed = false; - if (this.zunchiDisease && this.recoverZunchiDisease) healed = this.recoverZunchiDisease("日光浴でずんち病が治まった") || healed; - if (this.sleepDisease && this.recoverSleepDisease) healed = this.recoverSleepDisease("日光浴でねむり病が治った") || healed; - if (this.explosionDisease && this.recoverExplosionDisease) healed = this.recoverExplosionDisease("日光浴で爆発病が治った") || healed; - if (this.fightDisease && this.recoverFightDisease) healed = this.recoverFightDisease("日光浴できずつき病が治った") || healed; - if (healed) this.world?.log?.(`${this.name}は日光浴で病気が軽くなった。`, "event", { participants: [this] }); + if (this.zunchiDisease && this.recoverZunchiDisease) healed = this.recoverZunchiDisease("\u65e5\u5149\u6d74\u3067\u305a\u3093\u3061\u75c5\u304c\u6cbb\u3063\u305f") || healed; + if (this.sleepDisease && this.recoverSleepDisease) healed = this.recoverSleepDisease("\u65e5\u5149\u6d74\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f") || healed; + if (this.explosionDisease && this.recoverExplosionDisease) healed = this.recoverExplosionDisease("\u65e5\u5149\u6d74\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f") || healed; + if (this.fightDisease && this.recoverFightDisease) healed = this.recoverFightDisease("\u65e5\u5149\u6d74\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f") || healed; + if (healed) this.world?.log?.(`${this.name}\u306f\u65e5\u5149\u6d74\u3067\u75c5\u6c17\u304c\u8efd\u304f\u306a\u3063\u305f\u3002`, "event", { participants: [this] }); return healed; }, startSunbath() { if (!this.canStartSunbath()) return false; - this.setActionState?.("sunbath", { target: null, reason: "日なたぼっこ", sleeping: false }); + this.setActionState?.("sunbath", { target: null, reason: "\u65e5\u306a\u305f\u307c\u3063\u3053", sleeping: false }); this.sunbathTimer = rand(6, 14); this.sunbathCooldown = rand(16, 34); this.sunbathFrameTimer = 0; this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3); this.sunbathRecoveryChecked = false; this.tryRecoverBySunbath(); - if ((this.world?.time || 0) >= (this.nextBubbleAt || 0) && Math.random() < 0.45) this.bubble("ぽかぽか", 4.0, "rgba(112,86,36,0.78)"); + if ((this.world?.time || 0) >= (this.nextBubbleAt || 0) && Math.random() < 0.45) this.bubble("\u307d\u304b\u3063", 4.0, "rgba(112,86,36,0.78)"); return true; }, @@ -164,7 +483,7 @@ this.sunbathSpriteVariant = 1 + Math.floor(Math.random() * 3); this.sunbathFrameTimer = rand(0.55, 1.15); } - this.stress = clamp((this.stress || 0) - dt * 1.15, 0, 130); + applyNeedRelief(this, { safety: -dt * 6, fulfill: -dt * 4 }); this.energy = clamp((this.energy || 0) + dt * 0.08, 0, 100); if (!this.isSunbathTime() || this.hunger > 74 || this.energy < 10 || this.state === "panic") this.sunbathTimer = 0; if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") { @@ -180,7 +499,7 @@ if (d > touch) return; const p = clamp(1 - d / touch, 0, 1); const relief = dt * (1.35 + 2.65 * p); - this.stress = clamp((this.stress || 0) - relief, 0, 130); + applyNeedRelief(this, { safety: -relief * 1.2, fulfill: -relief * 1.6 }); this.adjustPersonality("openness", dt * 0.006 * (0.4 + p), "after being petted."); this.moodLiftTimer = Math.max(this.moodLiftTimer || 0, 0.5); this.cursorPetting = clamp((this.cursorPetting || 0) + dt * (1.0 + p * 2.4), 0, 1.75); @@ -222,7 +541,7 @@ this.loveMochiTimer = Math.max(0, (this.loveMochiTimer || 0) - dt); this.fightMochiTimer = Math.max(0, (this.fightMochiTimer || 0) - dt); if (this.fightMochiTimer > 0.04) { - this.stress = clamp(this.stress + dt * 0.11, 0, 130); + applyNeedShock(this, { safety: dt * 4 }); if (this.state !== "fight") this.fearTimer = Math.max(this.fearTimer || 0, 0.10); } if (this.intimidateTimer <= 0.04) this.intimidateTargetId = null; @@ -246,13 +565,13 @@ } this.applyCursorContactCare(dt); const colonyMoodId = this.world?.colonyMood?.id || "relaxed"; - if (colonyMoodId === "weaklings" && !this.isZunchiSlave) this.stress = clamp((this.stress || 0) - dt * 0.28, 0, 130); - if (colonyMoodId === "zunda_party" || colonyMoodId === "ball_party") this.stress = clamp((this.stress || 0) - dt * 0.34, 0, 130); + if (colonyMoodId === "weaklings" && !this.isZunchiSlave) applyNeedRelief(this, { safety: -dt * 2, fulfill: -dt * 1 }); + if (colonyMoodId === "zunda_party" || colonyMoodId === "ball_party") applyNeedRelief(this, { fulfill: -dt * 3 }); if (colonyMoodId === "ant_overrun" && this.state !== "panic") { const antSeen = (this.world?.nearbyAnts?.(this.x, this.y, 110, true) || []).some(a => a && !a.dead); if (antSeen) { - if (this.enterPanic) this.enterPanic({ target: null, reason: "アリだ!", fear: 1.1, wake: true, cause: "colony_ant_overrun" }); - else { this.setActionState?.("panic", { target: null, reason: "アリだ!", wake: true }); this.fearTimer = Math.max(this.fearTimer || 0, 1.1 * this.personalityProfile().fear); } + if (this.enterPanic) this.enterPanic({ target: null, reason: "\u30a2\u30ea\u3060", fear: 1.1, wake: true, cause: "colony_ant_overrun" }); + else { this.setActionState?.("panic", { target: null, reason: "\u30a2\u30ea\u3060", wake: true }); this.fearTimer = Math.max(this.fearTimer || 0, 1.1 * this.personalityProfile().fear); } } } const wasFighting = this.fightTimer > 0.04; @@ -297,19 +616,19 @@ this.hunger += CONFIG.hungerPerMinute * minute * this.trait.hunger * (1 + tooCold * 0.8); this.loneliness += CONFIG.lonelinessPerMinute * minute * (2 - this.trait.social); this.energy -= CONFIG.energyPerMinute * minute * this.trait.sleep * (this.state === "sleep" ? 0.025 : 1); - this.stress -= CONFIG.stressDecayPerMinute * minute / this.trait.stress; - this.stress += tooCold * dt * 0.25; + // stress is derived from needs; no independent stress decay. + if (tooCold > 0) applyNeedShock(this, { health: tooCold * dt * 4, safety: tooCold * dt * 2 }); const phase = this.world.lightLevel(); if (phase < 0.25) this.energy += dt * 0.07; - if (phase > 0.75) this.stress += dt * 0.04; + if (phase > 0.75) applyNeedShock(this, { health: dt * 1.2 }); if (this.world.weather === "cloudy") { - this.stress -= dt * 0.018; + applyNeedRelief(this, { safety: -dt * 1 }); this.vx *= Math.pow(0.996, dt * 60); this.vy *= Math.pow(0.996, dt * 60); } else if (this.world.weather === "light_rain") { this.energy += dt * 0.018; - this.stress -= dt * 0.025; + applyNeedRelief(this, { health: -dt * 1, safety: -dt * 1 }); } else if (this.world.weather === "sunny") { this.vx *= Math.pow(1.001, dt * 60); this.vy *= Math.pow(1.001, dt * 60); @@ -320,7 +639,7 @@ this.hunger = clamp(this.hunger, 0, 115); this.loneliness = clamp(this.loneliness, 0, 110); this.energy = clamp(this.energy, 0, 100); - this.stress = clamp(this.stress, 0, 130); + this.stress = calculateStressFromNeeds(this.needs || createDefaultNeeds()); const wasSleepingAtFrameStart = this.state === "sleep" || this.sleeping; this.awakeLockTimer = Math.max(0, (this.awakeLockTimer || 0) - dt); @@ -371,14 +690,14 @@ applyEnvironment(dt) { const parent = this.parentToFollow ? this.parentToFollow() : null; if (parent && dist(this, parent) < 96) { - this.stress = clamp(this.stress - dt * 0.42, 0, 130); + applyNeedRelief(this, { social: -dt * 4, safety: -dt * 2 }); this.loneliness = clamp(this.loneliness - dt * 0.32, 0, 110); this.adjustPersonality("sociability", dt * 0.004 * this.sociabilityFriendScale(), "after staying near family."); } const friend = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 92) : null; const friendCount = this.currentFriendCount ? this.currentFriendCount(FRIEND_AFFINITY_THRESHOLD, true) : 0; if (friend) { - this.stress = clamp(this.stress - dt * 0.36, 0, 130); + applyNeedRelief(this, { social: -dt * 3, fulfill: -dt * 1 }); this.adjustPersonality("sociability", dt * 0.004 * this.sociabilityFriendScale(), "after staying near friends."); } else if (friendCount <= 0 && this.loneliness > 72 && this.age > 8) { this.adjustPersonality("sociability", -dt * 0.0018, "after having no friends."); @@ -395,9 +714,9 @@ if (!it || it.dead || it.type !== "nest_box") continue; nestComfort += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 120, 0, 1); } - if (nestComfort > 0) this.stress = clamp(this.stress - dt * Math.min(1.15, nestComfort * 0.62), 0, 130); + if (nestComfort > 0) applyNeedRelief(this, { sleep: -dt * Math.min(4, nestComfort * 3), safety: -dt * Math.min(5, nestComfort * 4) }); if (grassComfort > 0) { - this.stress = clamp(this.stress - dt * Math.min(0.32, grassComfort * 0.08), 0, 130); + applyNeedRelief(this, { fulfill: -dt * Math.min(2, grassComfort * 0.5), safety: -dt * Math.min(1.5, grassComfort * 0.4) }); if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.22, grassComfort * 0.06)); if (this.need === "\u9759\u3051\u3055" || this.shouldApplyPersonalityBehavior("neuroticism", 1)) this.affection += dt * Math.min(0.06, grassComfort * 0.012); } @@ -418,10 +737,11 @@ }, isSleepFurniture(target) { - return Boolean(target && (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(target.type) : (target.type === "bed" || target.type === "nest_box"))); + return Boolean(target && (target.roles?.sleepPlace || (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(target.type) : (target.type === "bed" || target.type === "nest_box")))); }, sleepFurnitureLabel(target) { + if (target?.label) return target.label; return target?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5e72\u8349\u5bdd\u5e8a"; }, @@ -513,11 +833,11 @@ if (!z || z.stage !== "fresh") return; const d = dist(this, z); const push = clamp(1 - d / 90, 0, 1); - this.stress = clamp(this.stress + push * dt * 1.2 * this.personalityProfile().fear, 0, 130); + applyNeedShock(this, { safety: push * dt * 6 }); if (push > 0.2) this.adjustPersonality("neuroticism", -dt * push * 0.0035, "after repeated contact with poop."); this.vx += (this.x - z.x) / Math.max(d, 1) * push * dt * 40; this.vy += (this.y - z.y) / Math.max(d, 1) * push * dt * 40; - if (push > 0.35 && (this.state === "idle" || !this.thought)) this.thought = "新鮮なずんちを避けている"; + if (push > 0.35 && (this.state === "idle" || !this.thought)) this.thought = "\u65b0\u9bae\u306a\u305a\u3093\u3061\u3092\u907f\u3051\u3066\u3044\u308b"; }, resolveNeeds(dt) { @@ -527,18 +847,18 @@ this.sleeping = false; if (this.birthRitualTimer > 0.04) { - this.setActionState?.("birth_ritual", { target: this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead) || null, reason: "近くで体をゆすっている" }); + this.setActionState?.("birth_ritual", { target: this.world.tarinai.find(o => o.id === this.birthPartnerId && !o.dead) || null, reason: "\u8fd1\u304f\u3067\u4f53\u3092\u3086\u3059\u3063\u3066\u3044\u308b" }); return; } if (this.intimidateTimer > 0.04) { - this.setActionState?.("intimidate", { target: this.world.tarinai.find(o => o.id === this.intimidateTargetId && !o.dead) || null, reason: "相手を威嚇している" }); + this.setActionState?.("intimidate", { target: this.world.tarinai.find(o => o.id === this.intimidateTargetId && !o.dead) || null, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" }); this.bubble("!", 1.2); return; } if (this.fightTimer > 0.04) { - this.setActionState?.("fight", { reason: "喧嘩している" }); + this.setActionState?.("fight", { reason: "\u55a7\u5629\u3057\u3066\u3044\u308b" }); this.fightTargetIds = (this.fightTargetIds || []).filter(id => this.world.tarinai.some(t => t.id === id && !t.dead)); this.fightTargetId = this.fightTargetIds[0] || this.fightTargetId; let best = null, bestD = Infinity; @@ -548,7 +868,7 @@ const d = dist(this, t); if (d < bestD) { best = t; bestD = d; } } - this.setActionState?.("fight", { target: best, reason: "喧嘩している" }); + this.setActionState?.("fight", { target: best, reason: "\u55a7\u5629\u3057\u3066\u3044\u308b" }); this.bubble("!", 1.6); return; } @@ -568,265 +888,55 @@ const pin = this.currentLodgedPin?.() || null; const pinBehavior = this.currentLodgedPinBehavior?.() || null; if (pin && pinBehavior?.panicOnAttach) { - if (this.enterPanic) this.enterPanic({ threat: pin, reason: "画鋲が刺さってパニックになっている", fear: 0.60, hurtTimer: 0.24, awakeLockTimer: 1.8, bubble: "!!", bubbleColor: "rgba(168,72,72,0.82)", cause: "pushpin_stuck" }); - else { this.setActionState?.("panic", { target: this.panicDestination(pin), reason: "画鋲が刺さってパニックになっている", wake: true }); this.fearTimer = Math.max(this.fearTimer || 0, 0.60); } + this.needShock = this.needShock || {}; + this.needShock.safety = Math.max(this.needShock.safety || 0, 42); + this.needShock.health = Math.max(this.needShock.health || 0, 46); + this.lastNeedShockBreaker = pin; this.hurtTimer = Math.max(this.hurtTimer || 0, 0.24); this.awakeLockTimer = Math.max(this.awakeLockTimer || 0, 1.8); this.bubble("!!", 0.9, "rgba(168,72,72,0.82)"); this.thought = "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b"; - return; } this.stuckPushpinId = null; } - - const friendNearby = this.bestFriendLive ? this.bestFriendLive(FRIEND_AFFINITY_THRESHOLD, 170) : null; - const panicSource = (this.world?.nearbyTarinai?.(this.x, this.y, 128, true) || []) - .find(o => o && o !== this && !o.dead && !this.world?.isTarinaiHiddenInNestBox?.(o) && o.state === "panic" && (o.fearTimer || 0) > 0.12); - if (panicSource && !["panic", "sleep", "seek_bed", "fight", "birth_ritual", "eat", "sunbath"].includes(this.state)) { - const d = distXY(this.x, this.y, panicSource.x, panicSource.y); - const neuro = typeof effectivePersonalityValue === "function" ? effectivePersonalityValue(this, "neuroticism") : (this.currentPersonality?.neuroticism || 0); - const distFactor = clamp(1 - d / 128, 0, 1); - const stressFactor = clamp(((this.stress || 0) - 35) / 80, 0, 0.22); - const chance = clamp(0.10 + Math.max(0, neuro) * 0.36 + distFactor * 0.18 + stressFactor, 0.05, 0.76); - if (Math.random() < chance) { - if (this.enterPanic) this.enterPanic({ threat: panicSource.target || panicSource, reason: `${panicSource.name || "近くのたりない"}のパニックにつられている`, fear: 0.75, stress: 3 + distFactor * 5, stressThreshold: 6, cause: "nearby_panic_chain" }); - else { this.setActionState?.("panic", { target: this.panicDestination(panicSource.target || panicSource), reason: `${panicSource.name || "近くのたりない"}のパニックにつられている`, wake: true }); this.fearTimer = Math.max(this.fearTimer || 0, 0.75 * this.personalityProfile().fear); this.addStress ? this.addStress(3 + distFactor * 5, { threshold: 6 }) : (this.stress = clamp((this.stress || 0) + 3 + distFactor * 5, 0, 130)); } - return; + + if (continueBuildPlan(this, this.world, dt)) return; + + const beforeNeeds = this.needs ? { ...this.needs } : createDefaultNeeds(); + const needs = updateNeeds(this, this.world); + const breaker = this.lastNeedShockBreaker || null; + const safetyDelta = (needs.safety || 0) - (beforeNeeds.safety || 0); + const healthDelta = (needs.health || 0) - (beforeNeeds.health || 0); + const fulfillDelta = (needs.fulfill || 0) - (beforeNeeds.fulfill || 0); + const shockTriggered = safetyDelta >= 40 || healthDelta >= 45 || fulfillDelta >= 50; + if (shockTriggered && !["panic", "intimidate", "fight", "birth_ritual"].includes(this.state)) { + const aggression = Number(this.currentPersonality?.aggression || 0); + if (breaker && aggression > 0.6) { + this.setActionState?.("intimidate", { target: breaker, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b", wake: true, sleeping: false }); + this.intimidateTimer = Math.max(this.intimidateTimer || 0, 2.0); + } else { + this.setActionState?.("panic", { target: this.panicDestination?.(breaker), reason: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true, sleeping: false }); + this.fearTimer = Math.max(this.fearTimer || 0, 2.6); } - } - - const nearbyAnts = this.world.nearbyAnts?.(this.x, this.y, 112, true) || []; - let nearestAnt = null; - let nearestAntDist = Infinity; - for (const ant of nearbyAnts) { - const ad = distXY(this.x, this.y, ant.x, ant.y); - if (ad < nearestAntDist) { - nearestAnt = ant; - nearestAntDist = ad; - } - } - if (nearestAnt) { - const aggressiveToAnts = this.shouldApplyPersonalityBehavior("aggression", 1); - if (aggressiveToAnts && this.energy > 12 && this.fightTimer <= 0.04 && this.defeatedTimer <= 0.04) { - if (nearestAntDist < Math.max(36, this.radius + (nearestAnt.r || 4) + 8) && Math.random() < dt * 0.28) { - // Intimidation has no effect on ants. This is only a visible reaction. - this.setActionState?.("ant_intimidate", { target: nearestAnt, reason: "アリを威嚇している" }); - this.bubble("!", 0.9, "rgba(145,68,52,0.78)"); - return; - } - this.setActionState?.("ant_attack", { target: nearestAnt, reason: "アリを攻撃している" }); - this.goodMode = "angry"; - return; - } - if (this.enterPanic) this.enterPanic({ threat: nearestAnt, reason: "\u30a2\u30ea\u304b\u3089\u9003\u3052\u3066\u3044\u308b", fear: 0.34, wake: true, cause: "ant_seen" }); - else this.setActionState?.("panic", { target: this.panicDestination(nearestAnt), reason: "\u30a2\u30ea\u304b\u3089\u9003\u3052\u3066\u3044\u308b", wake: true }); + this.lastNeedShockBreaker = null; return; } - - const dayP = this.world.dayProgress(); - const personalSleep = this.sleepStart < this.sleepEnd - ? dayP >= this.sleepStart && dayP < this.sleepEnd - : dayP >= this.sleepStart || dayP < this.sleepEnd; - - if (this.world.canStartFight?.(this) && this.hasFightMochiEffect() && this.fightTimer <= 0.04 && this.intimidateTimer <= 0.04 && this.defeatedTimer <= 0.04) { - const opponent = this.world.nearestOther(this, 320, o => this.world.canBeFightTarget?.(o)); - if (opponent) { - const d = dist(this, opponent); - if (d <= Math.max(48, this.radius + opponent.radius + 18)) { - if (this.world.startForcedFight?.(this, opponent)) return; - } - this.setActionState?.("seek_friend", { target: opponent, reason: "けんか餅で相手を探している" }); - this.goodMode = "angry"; - if (Math.random() < dt * 0.24) this.bubble("!", 1.4, "rgba(160,74,45,0.78)"); - return; - } - } - - const recentRival = this.recentFightRival ? this.recentFightRival(60, 170) : null; - const feared = this.strongestRelation("fear"); - if (!wasSleeping && (recentRival || (feared && feared.score > 12))) { - const rival = recentRival || this.world.tarinai.find(o => o.id === feared.id && !o.dead); - if (rival && dist(this, rival) < 150) { - if (this.enterPanic) this.enterPanic({ threat: rival, reason: `${rival.name}\u3092\u907f\u3051\u3066\u9060\u56de\u308a\u3057\u3066\u3044\u308b`, fear: 0.35, wake: true, cause: "avoid_rival" }); - else this.setActionState?.("panic", { target: this.panicDestination(rival), reason: `${rival.name}\u3092\u907f\u3051\u3066\u9060\u56de\u308a\u3057\u3066\u3044\u308b`, wake: true }); - this.world.relationNotice(this.id, rival.id, "avoid", 22); - return; - } - } - - if (this.hasWaterCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) { - const water = this.world.nearest(this, ["water", "water_bowl"], 760); - if (water && !water.dead) { - this.setActionState?.("seek_water", { target: water, reason: this.explosionDisease ? "爆発病で水を探している" : "ずんち病で水を探している" }); - return; - } - if (this.zunchiDisease) { - this.setActionState?.("zunchi_sick", { target: null, reason: "ずんち病で当てもなく彷徨っている" }); - if (Math.random() < dt * 0.35) this.wanderAngle += rand(-2.4, 2.4); - return; - } - } - - if (this.hasZundaCurableDisease?.() && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) { - const cureFood = this.world.nearest(this, ["sweet"], 700); - if (cureFood && this.foodItemHasServingLeft(cureFood) && !this.shouldAvoidTarget(cureFood)) { - this.setActionState?.("seek_food", { target: cureFood, reason: this.fightDisease ? "きずつき病でずんだ餅を探している" : "爆発病でずんだ餅を探している" }); - this.foodReactTimer = Math.max(this.foodReactTimer, 0.42); - return; - } - } - - if (this.fightDisease && this.fightTimer <= 0.04 && this.birthRitualTimer <= 0.04 && this.defeatedTimer <= 0.04) { - this.setActionState?.("fight_sick", { target: null, reason: "きずつき病で当てもなく彷徨っている" }); - if (Math.random() < dt * 0.45) this.wanderAngle += rand(-3.4, 3.4); - return; - } - - if (!this.isZunchiSlave) { - const medicineTypes = window.TarinaiFoodRegistry?.typesByInterest?.("medicine_low") || ["love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]; - const specialMochi = this.world.nearest(this, medicineTypes, wasSleeping ? 190 : 155); - const specialDist = specialMochi ? distXY(this.x, this.y, specialMochi.x, specialMochi.y) : Infinity; - const weakInterest = this.hunger > (wasSleeping ? 68 : 76) && (specialDist < 72 || Math.random() < dt * 0.028); - if (specialMochi && this.foodItemHasServingLeft(specialMochi) && weakInterest && !this.shouldAvoidTarget(specialMochi)) { - this.setActionState?.("seek_food", { target: specialMochi, reason: `${toolLabel(specialMochi.type)}が少し気になっている` }); - this.foodReactTimer = Math.max(this.foodReactTimer, 0.18); - return; - } - } else if (this.hunger > 20) { - const zOnly = this.world.nearest(this, ["zunchi"], 520); - if (zOnly && zOnly.amount > 1.2) { - this.setActionState?.("seek_food", { target: zOnly, reason: "ずんちを探している" }); - this.foodReactTimer = Math.max(this.foodReactTimer, 0.30); - return; - } - } - - const zundaRange = wasSleeping ? 540 : 430 + 70 * this.personalityProfile().zunda; - const zunda = this.world.nearest(this, ["sweet"], zundaRange); - const zundaHungry = this.hunger > Math.max(7, 18 - 10 * (this.personalityProfile().zunda - 1)); - const zundaReward = this.affection > 8 && this.hunger > 7; - if (zunda && this.foodItemHasServingLeft(zunda) && ((wasSleeping && this.hunger > 7) || zundaHungry || zundaReward)) { - this.setActionState?.("seek_food", { target: zunda, reason: "ずんだ餅を探している" }); - this.foodReactTimer = Math.max(this.foodReactTimer, 0.36); - return; - } - - if (this.fearTimer > 0.22 || this.stress > 86 / this.personalityProfile().fear) { - if (this.enterPanic) this.enterPanic({ reason: "\u9003\u3052\u3066\u3044\u308b", fear: 0.65, wake: true, cause: "fear_or_stress" }); - else this.setActionState?.("panic", { target: this.panicDestination(), reason: "\u9003\u3052\u3066\u3044\u308b", wake: true }); - return; - } - - const sleepBias = this.personalityProfile().sleep; - const wakeLocked = (this.awakeLockTimer || 0) > 0 && !wasSleeping; - const keepSleeping = wasSleeping && (personalSleep || this.energy < 66 + 8 * sleepBias); - if (!wakeLocked && (keepSleeping || (personalSleep && this.energy < 90 + 8 * sleepBias) || this.energy < 28 + 8 * sleepBias)) { - let bed = this.isSleepFurniture(previousTarget) ? previousTarget : null; - if (!bed && friendNearby && this.isSleepFurniture(friendNearby.target) && (friendNearby.state === "sleep" || friendNearby.state === "seek_bed")) bed = friendNearby.target; - if (!bed) bed = this.world.bestBedFor(this, wasSleeping ? 480 : 420); - if (bed && !wasSleeping && this.energy > 18) { - const spot = this.sleepSpotFor(bed); - if (distXY(this.x, this.y, spot.x, spot.y) > this.radius + 10) { - this.setActionState?.("seek_bed", { target: bed, reason: `${this.sleepFurnitureLabel(bed)}へ向かっている` }); - return; - } - } - this.startSleeping?.(bed, bed ? (bed.type === "nest_box" ? "巣箱の中で眠っている" : "ベッドの近くで眠っている") : "眠っている"); - if (this.world.time > this.nextSleepBubbleAt) { - this.nextSleepBubbleAt = this.world.time + 4.5; - this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); - } - this.thought = bed ? (bed.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u30d9\u30c3\u30c9\u306e\u8fd1\u304f\u3067\u7720\u3063\u3066\u3044\u308b") : "\u7720\u3063\u3066\u3044\u308b"; - return; - } - - const pointer = this.world.pointer; - if (pointer?.inside) { - const pd = distXY(this.x, this.y, pointer.x, pointer.y); - if (pd < 170) { - const hostile = this.mood < 38 || this.stress > 58 || this.type === "angry"; - const friendly = !hostile && this.isAwakeCursorFriendly(); - if (hostile) { - this.setActionState?.("cursor_enemy", { target: { x: pointer.x, y: pointer.y, dead: false }, reason: "カーソルを避けている" }); - this.fearTimer = Math.max(this.fearTimer, 0.18); - if (this.world.time > this.nextFearBubbleAt) { - this.nextFearBubbleAt = this.world.time + 2.4; - this.world.spawnBubble(this.x, this.y - this.radius * 1.2, "!", "rgba(120,62,62,0.78)"); - } - this.thought = "\u30ab\u30fc\u30bd\u30eb\u3092\u907f\u3051\u3066\u3044\u308b"; - return; - } - if (friendly) { - this.setActionState?.("cursor_friend", { target: { x: pointer.x, y: pointer.y, dead: false }, reason: "カーソルについていく" }); - this.goodMode = "smile"; - this.thought = "\u30ab\u30fc\u30bd\u30eb\u306b\u3064\u3044\u3066\u3044\u304f"; - return; - } - } - } - - const playTrait = this.personalityProfile().play ?? 1; - const openPlay = this.shouldApplyPersonalityBehavior("openness", 1); - const closedPlay = this.shouldApplyPersonalityBehavior("openness", -1); - const ballRange = openPlay ? 540 : (closedPlay ? 170 : 240 + 60 * playTrait); - const ball = this.world.nearest(this, ["ball"], ballRange); - if (ball && this.energy > 26 && this.hunger < 98 && !this.shouldAvoidTarget(ball)) { - const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); - const chasers = this.world.countBallChasers ? this.world.countBallChasers(ball, this) : 0; - const baseChance = openPlay ? 0.74 : (closedPlay ? 0.025 : 0.08); - const movingBonus = clamp(ballSpeed / 120, 0, 0.42); - const socialBonus = this.loneliness > 48 ? 0.10 : 0; - if (chasers < 5 && (openPlay || ballSpeed > 7 || Math.random() < baseChance * playTrait + movingBonus + socialBonus)) { - this.setActionState?.("play_ball", { target: ball, reason: openPlay ? "ボールに引き寄せられている" : "転がるものが気になっている" }); - this.goodMode = "smile"; - return; - } - } - - const parent = this.parentToFollow(); - if (parent && !this.shouldAvoidTarget(parent)) { - const d = dist(this, parent); - if (d > 34 || (this.hunger < 78 && this.energy > 24)) { - this.setActionState?.("follow_parent", { target: parent, reason: "親についていく" }); - this.bubble("?", 4.0); - return; - } - } - - if (friendNearby && this.hunger > 58 && friendNearby.target && (this.isZunchiSlave ? ["zunchi"] : ["sweet", "grass", "ant_corpse"]).includes(friendNearby.target.type) && !friendNearby.target.dead && !this.shouldAvoidTarget(friendNearby.target)) { - this.setActionState?.("seek_food", { target: friendNearby.target, reason: "友達と一緒に食べに行く" }); - this.foodReactTimer = Math.max(this.foodReactTimer, 0.24); - return; - } - - if (this.hunger > 62) { - const itemTypes = this.isZunchiSlave ? ["zunchi"] : ["sweet", ...((window.TarinaiFoodRegistry?.typesByInterest?.("grass_like", "corpse")) || ["grass", "ant_corpse"])]; - const item = this.world.nearest(this, itemTypes, 420); - if (item) { - this.setActionState?.("seek_food", { target: item, reason: "食べ物を探している" }); - this.foodReactTimer = Math.max(this.foodReactTimer, 0.30); - return; - } - } - - const socialSeek = this.shouldApplyPersonalityBehavior("sociability", 1); - const lonerSeek = this.shouldApplyPersonalityBehavior("sociability", -1); - if (this.loneliness > 63 - (socialSeek ? 16 : 0) + (lonerSeek ? 10 : 0)) { - const other = this.world.nearestOther(this, socialSeek ? 390 : (lonerSeek ? 190 : 300)); - if (other) { - this.setActionState?.("seek_friend", { target: other, reason: "仲間を探している" }); - return; - } - } - - if (this.stress > 74 / this.personalityProfile().fear || this.fearTimer > 0.22) { - if (this.enterPanic) this.enterPanic({ reason: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.55, wake: true, cause: "stress_panic" }); - else this.setActionState?.("panic", { target: this.panicDestination(), reason: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); - return; - } - - if (Math.random() < dt * 0.018) this.wanderAngle += rand(-1.7, 1.7); - this.goIdle(`${this.need}がたりない`); + + const choice = chooseTopNeedRandom(needs); + let action = chooseActionForNeed(choice.need, this, this.world); + if (!action) action = TARINAI_ACTIONS.find(a => a.id === "wander_lightly"); + const reasonText = buildReasonText(choice.need, choice.tiedNeeds, action); + this.intent = { + need: choice.need, + tiedNeeds: [...choice.tiedNeeds], + actionId: action?.id || "idle", + actionLabel: action?.label || "\u5f85\u3063\u3066\u3044\u308b", + reasonText, + }; + const ran = action?.run?.(this, this.world); + if (!ran) this.goIdle?.(this.intent.actionLabel); + this.thought = reasonText; + return; }, makePoop(force = 1) { @@ -835,8 +945,8 @@ if (this.hasLodgedPinEffect?.("blocksZunchi")) { this.digest = rand(0.4, 1.6); this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1); - this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "おしり鋲でずんちが溜まってつらい" : "おしり鋲でずんちを我慢している"; - if ((this.oshiriByoZunchiStock || 0) >= 6) this.stress = clamp((this.stress || 0) + 1.8, 0, 130); + this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044" : "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b"; + if ((this.oshiriByoZunchiStock || 0) >= 6) applyNeedShock(this, { health: 8 }); return; } this.digest = rand(0.4, 1.6); @@ -864,7 +974,7 @@ if (((it.type === "sweet" && canSweetBite) || (it.type === "food" && canBite)) && this.foodItemHasServingLeft(it)) { const eating = this.consumeFoodServing(it); if (eating > 0) { - this.beginEating(it, `${toolLabel(it.type)}を食べている`); + this.beginEating(it, `${toolLabel(it.type)}\u3092\u98df\u3079\u3066\u3044\u308b`); this.vx *= Math.pow(0.25, dt * 60); this.vy *= Math.pow(0.25, dt * 60); const hungerRelief = this.foodHungerReliefFor(it.type, eating, it.type === "sweet" ? 0.75 : 0.95); @@ -872,7 +982,7 @@ this.recoverHealth(eating * (it.type === "sweet" ? 0.28 : 0.46)); this.affection += it.type === "sweet" ? eating * 0.03 : eating * 0.012; if (it.type === "sweet") { - this.stress = clamp(this.stress - eating * 0.42, 0, 130); + applyNeedRelief(this, { food: -eating * 0.4, fulfill: -eating * 0.2 }); this.fearTimer = Math.max(0, this.fearTimer - eating * 0.035); if (this.explosionDisease) this.recoverExplosionDisease("\u305a\u3093\u3060\u9905\u3067\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f"); if (this.fightDisease) this.recoverFightDisease("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"); @@ -899,7 +1009,7 @@ if (canSweetBite && (it.type === "love_mochi" || it.type === "fight_mochi" || it.type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(it.type))) && this.foodItemHasServingLeft(it)) { const eating = this.consumeFoodServing(it); if (eating <= 0) continue; - this.beginEating(it, `${toolLabel(it.type)}を食べている`); + this.beginEating(it, `${toolLabel(it.type)}\u3092\u98df\u3079\u3066\u3044\u308b`); this.vx *= Math.pow(0.25, dt * 60); this.vy *= Math.pow(0.25, dt * 60); const hungerRelief = this.foodHungerReliefFor(it.type, eating, 0.82); @@ -942,7 +1052,7 @@ if (canBite && it.type === "grass") { const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1)); const bite = Math.min(it.amount, rand(4.2, 7.2) * servingScale); - this.beginEating(it, "草を食べている"); + this.beginEating(it, "\u8349\u3092\u98df\u3079\u3066\u3044\u308b"); this.grassEatTimer = Math.max(this.grassEatTimer, 0.22); this.vx *= Math.pow(0.20, dt * 60); this.vy *= Math.pow(0.20, dt * 60); @@ -951,7 +1061,7 @@ it.growth = clamp((it.growth ?? it.amount / 120) - bite / 150, 0.04, 1.2); this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * 0.46) : (this.hunger -= bite * 0.46); this.recoverHealth(bite * 0.18); - this.stress -= dt * 0.5; + applyNeedRelief(this, { food: -dt * 2, fulfill: -dt * 1 }); this.eatTimer = Math.max(this.eatTimer, 0.22); this.eatCooldown = rand(0.68, 1.15); this.bubble("\u3082\u3050", 2.4, "rgba(72,96,48,0.78)"); @@ -968,13 +1078,13 @@ if (canBite && it.type === "ant_corpse") { const bite = Math.min(it.amount, rand(3.2, 5.4)); if (bite > 0) { - this.beginEating(it, "アリの死骸を食べている"); + this.beginEating(it, "\u30a2\u30ea\u306e\u6b7b\u9ab8\u3092\u98df\u3079\u3066\u3044\u308b"); this.vx *= Math.pow(0.18, dt * 60); this.vy *= Math.pow(0.18, dt * 60); it.amount -= bite; this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * 0.62) : (this.hunger -= bite * 0.62); this.recoverHealth(bite * 0.12); - this.stress = clamp(this.stress + bite * 0.05 * this.personalityProfile().fear, 0, 130); + applyNeedShock(this, { safety: bite * 0.12 }); this.eatTimer = Math.max(this.eatTimer, 0.26); this.eatCooldown = rand(0.75, 1.25); this.bubble("\u3082\u3050", 2.2, "rgba(72,60,48,0.78)"); @@ -990,20 +1100,20 @@ if (it.type === "zunchi") { const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1); if (!this.isZunchiSlave) { - this.stress = clamp(this.stress + touch * dt * 3.4 * this.personalityProfile().fear, 0, 130); + applyNeedShock(this, { health: touch * dt * 7, safety: touch * dt * 4 }); if (touch > 0.08) this.adjustPersonality("neuroticism", -dt * touch * 0.0045, "after repeated contact with poop."); } this.zunchiStain = clamp((this.zunchiStain || 0) + touch * dt * (this.isZunchiSlave ? 5.2 : 9.5), 0, 100); if (canZunchiBite) { const servingScale = Number.isFinite(it.foodServingScale) ? it.foodServingScale : (it.toolSize === "large" ? 1.18 : (it.toolSize === "small" ? 0.82 : 1)); const bite = Math.min(it.amount, (this.isZunchiSlave ? rand(4.8, 7.4) : rand(3.4, 5.8)) * servingScale); - this.beginEating(it, this.isZunchiSlave ? "ずんちどれいなのでずんちを食べている" : "ずんちを食べている"); + this.beginEating(it, this.isZunchiSlave ? "\u305a\u3093\u3061\u3069\u308c\u3044\u306a\u306e\u3067\u305a\u3093\u3061\u3092\u98df\u3079\u3066\u3044\u308b" : "\u305a\u3093\u3061\u3092\u98df\u3079\u3066\u3044\u308b"); this.vx *= Math.pow(0.18, dt * 60); this.vy *= Math.pow(0.18, dt * 60); it.amount -= bite; this.applyFoodHungerRelief ? this.applyFoodHungerRelief(bite * (this.isZunchiSlave ? 0.66 : 0.18)) : (this.hunger -= bite * (this.isZunchiSlave ? 0.66 : 0.18)); this.recoverHealth(bite * (this.isZunchiSlave ? 0.10 : 0.02)); - this.stress = clamp(this.stress + bite * (this.isZunchiSlave ? 0.06 : 0.52) * this.personalityProfile().fear, 0, 130); + applyNeedShock(this, { health: bite * (this.isZunchiSlave ? 0.2 : 1.2), safety: bite * (this.isZunchiSlave ? 0.1 : 0.6) }); if (!this.isZunchiSlave) this.adjustPersonality("neuroticism", -bite * 0.0015, "after repeated contact with poop."); this.zunchiStain = clamp((this.zunchiStain || 0) + bite * (this.isZunchiSlave ? 0.75 : 1.75), 0, 100); this.lastMealColor = "#6b8d3f"; @@ -1018,7 +1128,7 @@ } if (it.type === "water" || it.type === "water_bowl") { - this.thought = it.type === "water_bowl" ? "水の皿で水を飲んでいる" : "水を浴びて落ち着いている"; + this.thought = it.type === "water_bowl" ? "\u6c34\u306e\u76bf\u3067\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b" : "\u6c34\u3092\u6d74\u3073\u3066\u843d\u3061\u7740\u3044\u3066\u3044\u308b"; this.target = it; this.applyWaterEffect(dt, it.type); if ((this.world?.time || 0) > (this.nextDrinkSound || -999)) { @@ -1030,24 +1140,25 @@ } if (it.type === "stone") { - if (this.state === "idle" && (this.need === "あたたかさ" || this.stress > 48)) this.thought = "石の近くで落ち着いている"; - this.stress -= dt * 0.85; + if (this.state === "idle" && (this.need === "\u3042\u305f\u305f\u304b\u3055" || this.stress > 48)) this.thought = "\u77f3\u306e\u8fd1\u304f\u3067\u843d\u3061\u7740\u3044\u3066\u3044\u308b"; + applyNeedRelief(this, { safety: -dt * 4, fulfill: -dt * 2 }); this.energy += dt * 0.18; if (this.need === "\u3042\u305f\u305f\u304b\u3055") this.affection += dt * 0.25; } if (this.isSleepFurniture(it)) { if (this.energy < 74 + 6 * this.personalityProfile().sleep) { + it.use?.(this, this.world); const occ = this.world.bedOccupancy(it); const comfort = this.world.bedComfort(it); - this.startSleeping?.(it, it.type === "nest_box" ? "巣箱の中で休んでいる" : "寝床の近くで休んでいる"); + this.startSleeping?.(it, it.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u4f11\u3093\u3067\u3044\u308b" : "\u5bdd\u5e8a\u306e\u8fd1\u304f\u3067\u4f11\u3093\u3067\u3044\u308b"); if (this.world.time > this.nextSleepBubbleAt) { this.nextSleepBubbleAt = this.world.time + 4.5; const crowded = occ > 5; this.world.spawnBubble(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); } this.energy += dt * (1.9 + comfort * 1.4); - this.stress += dt * (occ > 5 ? 0.55 : -1.3 * comfort); + if (occ > 5) applyNeedShock(this, { safety: dt * 3 }); else applyNeedRelief(this, { sleep: -dt * 6 * comfort, safety: -dt * 4 * comfort }); it.wear = clamp((it.wear || 0) + dt * 0.0008, 0, 1); } } @@ -1056,7 +1167,7 @@ const touch = clamp(1 - d / (this.radius + it.r + 12), 0, 1); if (this.state === "play_ball" || this.target === it || this.shouldApplyPersonalityBehavior("openness", 1)) { this.goodMode = "smile"; - this.stress = clamp(this.stress - dt * (0.9 + touch * 1.8), 0, 130); + applyNeedRelief(this, { fulfill: -dt * (4 + touch * 8), social: -dt * (1 + touch * 3) }); this.loneliness = clamp(this.loneliness - dt * (0.4 + touch), 0, 110); if (this.state === "play_ball") this.foodReactTimer = Math.max(this.foodReactTimer, 0.10); } @@ -1065,19 +1176,19 @@ if (it.type === "splat") { const touch = clamp(1 - d / (this.radius + it.r + 16), 0, 1); if (touch > 0) { - this.stress = clamp(this.stress + touch * dt * 2.8 * this.personalityProfile().fear, 0, 130); + applyNeedShock(this, { safety: touch * dt * 5, health: touch * dt * 3 }); if (touch > 0.06) this.adjustPersonality("neuroticism", -dt * touch * 0.0042, "after repeated contact with corpses"); } } if (it.type === "trace") { this.loneliness -= dt * 0.25; - this.stress += dt * 0.06 * this.personalityProfile().fear; + applyNeedShock(this, { social: -dt * 1, safety: dt * 0.8 }); } } this.hunger = clamp(this.hunger, 0, 115); this.energy = clamp(this.energy, 0, 100); - this.stress = clamp(this.stress, 0, 130); + this.stress = calculateStressFromNeeds(this.needs || createDefaultNeeds()); this.loneliness = clamp(this.loneliness, 0, 110); } })); diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index c8af767..8905b6e 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -35,12 +35,12 @@ nonSlave.vx += nx * dt * 34; nonSlave.vy += ny * dt * 34; if (nonSlave.state === "idle" || nonSlave.state === "seek_friend") { - if (nonSlave.enterPanic) nonSlave.enterPanic({ threat: slave, reason: "ずんちどれいを怖がっている", fear: 0.38, wake: false, cause: "zunchi_slave_nearby" }); - else { nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear); nonSlave.target = slave; nonSlave.thought = "ずんちどれいを怖がっている"; } + if (nonSlave.enterPanic) nonSlave.enterPanic({ threat: slave, reason: "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044", fear: 0.38, wake: false, cause: "zunchi_slave_nearby" }); + else { nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear); nonSlave.target = slave; nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044"; } } else { nonSlave.fearTimer = Math.max(nonSlave.fearTimer || 0, 0.38 * nonSlave.personalityProfile().fear); nonSlave.target = slave; - nonSlave.thought = "ずんちどれいを怖がっている"; + nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044"; } nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid"); slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid"); @@ -66,15 +66,15 @@ this.affection += dt * 0.28 * this.trait.social; this.adjustRelation(o, dt * 0.18 * pdef.relation * (rel.fear > 10 ? 0.35 : 1), -dt * 0.018, "nearby"); if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD) { - this.stress = clamp(this.stress - dt * 0.62 * this.trait.social, 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(this, { social: -dt * 5 * this.trait.social, fulfill: -dt * 2 }); this.affection = clamp(this.affection + dt * 0.18, 0, 80); if (this.world.relationNotice(this.id, o.id, "friend-calm", 55)) this.world.log(`${this.name}\u306f${o.name}\u3068\u3044\u308b\u3068\u5c11\u3057\u843d\u3061\u7740\u304f\u3002`, "relation", { participants: [o, this] }); } if ((rel.affinity || 0) >= FRIEND_AFFINITY_THRESHOLD && this.world.relationNotice(this.id, o.id, "friend", 32)) { this.world.log(`${this.name}\u306f\u3088\u304f${o.name}\u306e\u8fd1\u304f\u306b\u3044\u308b\u3002`, "relation", { participants: [o, this] }); } - if (this.type === "angry") this.stress += dt * 0.08; - if (this.type === "teary" && o.type === "teary") this.stress -= dt * 0.12; + if (this.type === "angry" && typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 1.5, safety: dt * 1 }); + if (this.type === "teary" && o.type === "teary" && typeof applyNeedRelief === "function") applyNeedRelief(this, { social: -dt * 2 }); } const fightingThis = this.fightTimer > 0 && (this.fightTargetId === o.id || (this.fightTargetIds || []).includes(o.id)); @@ -86,8 +86,8 @@ this.vy -= ny * rand(24, 44); o.vx += nx * rand(34, 64); o.vy += ny * rand(34, 64); - this.stress = clamp(this.stress + 1.6, 0, 130); - o.stress = clamp(o.stress + 2.8, 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 8, health: 4 }); + if (typeof applyNeedShock === "function") applyNeedShock(o, { safety: 12, health: 6 }); const damageToThis = o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2; const damageToOther = this.outgoingDamage ? this.outgoingDamage(1.6) : 1.6; this.world.emit?.("fight:hit", { attacker: o, target: this, amount: damageToThis, cause: "\u55a7\u5629" }); @@ -106,7 +106,7 @@ o.fallDir = nx > 0 ? 1 : -1; this.world.spawnFallEffect(o.x, o.y + o.radius * 0.45, 0.55); } - if (o.energy + o.mood * 0.25 < this.energy + this.mood * 0.25 - 8) { + if (o.energy < this.energy - 8) { this.world.maybeDefeatFromFightDamage?.(o, this, Math.max(damageToOther, 2.4)); } this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5); @@ -122,7 +122,7 @@ const aggressiveOther = o.shouldApplyPersonalityBehavior?.("aggression", 1); const calmThis = this.shouldApplyPersonalityBehavior("aggression", -1); const calmOther = o.shouldApplyPersonalityBehavior?.("aggression", -1); - const fightRisk = (this.mood < 55 || o.mood < 55 || this.stress > 46 || o.stress > 46 || this.type === "angry" || o.type === "angry" || closeCount > 3 || aggressiveThis || aggressiveOther || battleDrug || o.isZunchiSlave || this.isZunchiSlave); + const fightRisk = (this.stress > 46 || o.stress > 46 || (this.needs?.safety || 0) > 60 || (o.needs?.safety || 0) > 60 || this.type === "angry" || o.type === "angry" || closeCount > 3 || aggressiveThis || aggressiveOther || battleDrug || o.isZunchiSlave || this.isZunchiSlave); const fearBrake = battleDrug ? clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 220, 0.74, 1.15) : clamp(1 - ((relA.fear || 0) + (relB.fear || 0)) / 120, 0.34, 1); const friendBrake = battleDrug ? clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 180, 0.72, 1.12) : clamp(1 - Math.max(relA.affinity || 0, relB.affinity || 0) / 90, 0.48, 1); const personalityRisk = this.personalityProfile().fight * o.personalityProfile().fight * (calmThis ? 0.62 : 1) * (calmOther ? 0.62 : 1); @@ -145,7 +145,7 @@ } if (closeCount > 8) { - this.stress += dt * 0.9 * this.personalityProfile().fear; + if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 5, safety: dt * 3 * this.personalityProfile().fear }); this.energy -= dt * 0.25; this.fearTimer = Math.max(this.fearTimer, 0.18); } @@ -174,7 +174,7 @@ const bedCrowd = this.isSleepFurniture(this.target) ? this.world.bedOccupancy(this.target) : 0; const crowdedSleep = bedCrowd > 5; this.energy = clamp(this.energy + dt * (this.target ? 1.8 + bedComfort * 1.35 : 2.1), 0, 100); - this.stress = clamp(this.stress + dt * (crowdedSleep ? 0.18 * Math.max(1, bedCrowd - 5) : -0.12 * bedComfort), 0, 130); + if (crowdedSleep && typeof applyNeedShock === "function") applyNeedShock(this, { safety: dt * Math.max(1, bedCrowd - 5) }); else if (typeof applyNeedRelief === "function") applyNeedRelief(this, { sleep: -dt * 3 * bedComfort, safety: -dt * 1.5 * bedComfort }); this.hunger = clamp(this.hunger + dt * 0.018, 0, 115); if (this.target && !this.target.dead && this.target.type === "nest_box") { this.updateNestBoxPresence(dt); @@ -259,7 +259,8 @@ ? (this.insideNestBoxId === this.target.id || d <= Math.max(this.radius + 34, this.target.r * 1.08) || bedDist <= Math.max(this.target.r * 1.05, 62)) : (d <= Math.max(this.radius + 8, 26) || bedDist <= Math.max(this.target.r * 0.58, 20)); if (reachedSleepFurniture) { - this.startSleeping?.(this.target, this.target.type === "nest_box" ? "巣箱の中で休んでいる" : "眠る場所に着いたので休んでいる"); + this.target.use?.(this, this.world); + this.startSleeping?.(this.target, this.target.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u4f11\u3093\u3067\u3044\u308b" : "\u5bdd\u308b\u5834\u6240\u306b\u7740\u3044\u305f\u306e\u3067\u4f11\u3093\u3067\u3044\u308b"); if (this.target.type === "nest_box") this.enterNestBox(this.target, dt); return; } @@ -366,14 +367,6 @@ else if (this.stress >= 124) { reason = this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a"; } - else if (this.mood <= 20) { - const lowMood = clamp((20 - this.mood) / 20, 0, 1); - const bodyLoad = clamp((this.hunger - 72) / 42, 0, 1) * 0.018 + clamp((28 - this.energy) / 28, 0, 1) * 0.020 + clamp((this.stress - 86) / 44, 0, 1) * 0.022; - const deathRisk = 0.010 + lowMood * 0.060 + bodyLoad; - if (Math.random() < Math.max(0, dt) * deathRisk) { - reason = this.dominantDeathCause("", { lowMood: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a"; - } - } if (reason) this.die(reason); }, diff --git a/js/ui_selected.js b/js/ui_selected.js index 1faf3f5..88b9048 100644 --- a/js/ui_selected.js +++ b/js/ui_selected.js @@ -192,8 +192,10 @@ function renderSelected() { personalityTags.join(",") ].join(","); const itemEffectSummary = t.activeItemEffectSummary ? t.activeItemEffectSummary() : []; + const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"]; const snapshot = [ t.id, t.type, t.name, generationDisplay, t.state, t.thought, targetLabel(t.target), + t.intent?.need || "", t.intent?.actionId || "", t.intent?.actionLabel || "", t.intent?.reasonText || "", needKeys.map(key => `${key}:${t.needs?.[key] ?? 0}`).join(","), fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness), fmt(t.energy), fmt(t.stress), fmt(t.mood), t.parentNames?.join("+") || "", t.children?.length || 0, fmt(t.lack), @@ -239,6 +241,7 @@ function renderSelected() { const basicHtml = `
\u4e16\u4ee3${escapeHtml(generationDisplay || "\u4e0d\u660e")}
\u72b6\u614b${escapeHtml(stateLabel(t.state))}
+
\u3044\u307e${escapeHtml(t.intent?.actionLabel || stateLabel(t.state))}
\u7406\u7531${escapeHtml(reasonLabel(t))}
\u5bfe\u8c61${escapeHtml(targetLabel(t.target))}
\u5e74\u9f62${fmt(t.age)} / ${fmt(t.lifeSpan)}
@@ -247,13 +250,16 @@ function renderSelected() {
\u79fb\u52d5\u901f\u5ea6${geneticPercentText(t, "speed")}
\u30b5\u30a4\u30ba${geneticSizeText(t)}
`; + const needRowsHtml = needKeys.map(key => { + const label = (typeof TARINAI_NEED_LABELS !== "undefined" && TARINAI_NEED_LABELS[key]) || key; + const value = typeof getNeedDisplayValue === "function" ? getNeedDisplayValue(t.needs?.[key] || 0) : Math.round((t.needs?.[key] || 0) / 10); + const chosen = t.intent?.need === key ? ` data-chosen-need="true"` : ""; + return `
${escapeHtml(label)}${escapeHtml(value)}
`; + }).join(""); const healthHtml = [ - selectedInfoRowHtml("\u7dcf\u5408\u7684\u6c17\u5206", fmt(t.mood), "mood"), - selectedInfoRowHtml("\u7a7a\u8179", fmt(t.hunger), "hunger"), - selectedInfoRowHtml("\u5bc2\u3057\u3055", fmt(t.loneliness), "loneliness"), + needRowsHtml, selectedInfoRowHtml("\u4f53\u529b", fmt(t.energy), "energy"), selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"), - selectedInfoRowHtml("\u305a\u3093\u3061\u6c5a\u308c", fmt(t.zunchiStain || 0), "zunchi"), ].join(""); const itemEffectHtml = itemEffectSummary.length ? itemEffectSummary.map(e => `
${escapeHtml(e.label)}${escapeHtml(e.detail)}
`).join("") @@ -297,6 +303,7 @@ function stateLabel(s) { } function reasonLabel(t) { + if (t?.intent?.reasonText) return t.intent.reasonText; return window.TEXT_CATALOG?.reasonLabel?.(t) || "なし"; } diff --git a/js/version.js b/js/version.js index ec564a1..0a7e113 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "15.24.0"; - const APP_BUILD = "safe-refactor-ui-fix"; + const APP_VERSION = "15.24.2"; + const APP_BUILD = "needs-compliance-fix"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index 843e9b0..d1fd863 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -68,7 +68,7 @@ if (t.enterPanic) t.enterPanic({ target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", fear: 1.2, surpriseTimer: 0.95, wake: true, cause: "firecracker_wakeup" }); else { t.setActionState?.("panic", { target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", wake: true }); t.surpriseTimer = Math.max(t.surpriseTimer, 0.95); t.fearTimer = Math.max(t.fearTimer, 1.2); } } - t.addStress ? t.addStress(rand(8, 15), { threshold: 8 }) : (t.stress = clamp(t.stress + rand(8, 15), 0, 130)); + if (t.addStress) t.addStress(rand(8, 15), { threshold: 8 }); t.fearTimer = Math.max(t.fearTimer, 0.58); const dx = t.x - x; const dy = t.y - y; @@ -76,7 +76,7 @@ if (d > blastRadius) continue; const p = clamp(1 - d / blastRadius, 0, 1); t.damage(5 + p * 18, "\u7206\u7af9"); - t.addStress ? t.addStress(22 * p, { threshold: 8 }) : (t.stress = clamp(t.stress + 22 * p, 0, 130)); + if (t.addStress) t.addStress(22 * p, { threshold: 8 }); t.hurtTimer = Math.max(t.hurtTimer, 1.5 + p * 1.8); t.surpriseTimer = Math.max(t.surpriseTimer, 0.85); if (t.enterPanic) t.enterPanic({ target: { x, y }, reason: "爆竹の爆風でパニックになっている", fear: 1.7 + p, wake: true, cause: "firecracker_blast" }); @@ -106,6 +106,7 @@ if (!t.dead && p > 0.22 && Math.random() < (t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance)) t.infectExplosionDisease?.(it); if (!t.dead && Math.random() < 0.45) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); } + if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it); this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u7af9", { push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * (it.blastScale || 1), }); @@ -166,7 +167,7 @@ if (d > blastRadius) continue; const q = clamp(1 - d / blastRadius, 0, 1); o.damage(5 + q * 18, "\u7206\u767a\u75c5"); - o.addStress ? o.addStress(22 * q, { threshold: 8 }) : (o.stress = clamp(o.stress + 22 * q, 0, 130)); + if (o.addStress) o.addStress(22 * q, { threshold: 8 }); let nx = dx / d; let ny = dy / d; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { @@ -365,7 +366,7 @@ }, isSleepFurniture(it) { - return Boolean(it && (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(it.type) : (it.type === "bed" || it.type === "nest_box"))); + return Boolean(it && (it.roles?.sleepPlace || (typeof isSleepFurnitureType === "function" ? isSleepFurnitureType(it.type) : (it.type === "bed" || it.type === "nest_box")))); }, isTarinaiHiddenInNestBox(t) { @@ -412,8 +413,10 @@ } return best; }; - // Prefer an available nest box, then fall back to a hay bed. - return choose("nest_box") || choose("bed"); + const owned = (this.nearbyItems(t.x, t.y, maxDist) || []) + .filter(bed => bed && !bed.dead && bed.ownerId === t.id && bed.roles?.sleepPlace) + .sort((a, b) => dist(t, a) - dist(t, b))[0] || null; + return owned || choose("nest_box") || choose("bed") || choose("grass_bed"); }, relationNotice(aId, bId, kind, cooldown = 30) { @@ -525,7 +528,7 @@ loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear); this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); winner.affection = clamp(winner.affection + 0.8, 0, 80); - winner.stress = clamp(winner.stress - rand(9, 18), 0, 130); + if (typeof applyNeedRelief === "function") applyNeedRelief(winner, { safety: -rand(12, 22), fulfill: -rand(8, 16) }); if (this.relationNotice(winner.id, loser.id, "fight-result", 8)) { this.log(`${loser.name}\u306f${winner.name}\u306b\u8ca0\u3051\u3001\u305d\u306e\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "fight", { participants: [loser, winner] }); } @@ -574,7 +577,7 @@ t.damage(dealtDamage, reason); if (isGenkotsu) { t.fearTimer = Math.max(t.fearTimer || 0, 4.0 + p * 2.2); - t.stress = clamp((t.stress || 0) + 10 + p * 18, 0, 130); + if (typeof applyNeedShock === "function") applyNeedShock(t, { safety: 18 + p * 30, health: 8 + p * 15 }); t.adjustPersonality?.("openness", -(0.020 + p * 0.030), "after being struck by genkotsu."); t.thought = "げんこつがこわい"; } else { @@ -586,6 +589,7 @@ t.fallDir = nx >= 0 ? 1 : -1; hit += 1; } + if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, it.x, it.y, radius, damage, it); const antHit = this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, { push: p => push * (0.62 + p * 1.25), }); @@ -817,7 +821,7 @@ this.applyImpactDamage(t, damage, "\u885d\u7a81"); const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0); const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18); - t.addStress ? t.addStress(stressGain, { threshold: 8 }) : (t.stress = clamp(t.stress + stressGain, 0, 130)); + if (t.addStress) t.addStress(stressGain, { threshold: 8 }); if (t.enterPanic) t.enterPanic({ target: { x: ball.x, y: ball.y }, reason: "高速のボールにぶつかってパニックになっている", fear: 0.95, stress: 0, wake: true, cause: "fast_ball_hit" }); else t.setActionState?.("panic", { target: { x: ball.x, y: ball.y }, reason: "高速のボールにぶつかってパニックになっている", wake: true }); t.hurtTimer = Math.max(t.hurtTimer, damage > 16 ? 2.4 : 1.35); @@ -855,7 +859,7 @@ t.energy = clamp(t.energy - (playIntent ? 0.20 : 0.08), 0, 100); const playRelief = playful && playIntent ? 2.8 : playful ? 1.5 : playIntent ? 0.75 : 0.20; const sensitiveBump = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) && !playIntent ? clamp(ballSpeed / 230, 0.6, 2.6) : 0; - t.stress = clamp(t.stress - playRelief + sensitiveBump, 0, 130); + if (typeof applyNeedShock === "function" && sensitiveBump > playRelief) applyNeedShock(t, { safety: (sensitiveBump - playRelief) * 4 }); else if (typeof applyNeedRelief === "function") applyNeedRelief(t, { fulfill: -playRelief * 6, social: -playRelief * 2 }); if (playful && playIntent) { t.loneliness = clamp(t.loneliness - 0.9, 0, 100); t.affection = clamp(t.affection + 0.18, 0, 100); diff --git a/js/world_family_social.js b/js/world_family_social.js index c210a5a..fb3130c 100644 --- a/js/world_family_social.js +++ b/js/world_family_social.js @@ -730,8 +730,8 @@ b.fightTargetIds = b.fightTargetIds.slice(-4); a.fightTargetId = a.fightTargetIds[0]; b.fightTargetId = b.fightTargetIds[0]; - a.addStress ? a.addStress(rand(8, 18), { threshold: 8 }) : (a.stress = clamp(a.stress + rand(8, 18), 0, 130)); - b.addStress ? b.addStress(rand(8, 18), { threshold: 8 }) : (b.stress = clamp(b.stress + rand(8, 18), 0, 130)); + if (a.addStress) a.addStress(rand(8, 18), { threshold: 8 }); + if (b.addStress) b.addStress(rand(8, 18), { threshold: 8 }); const damageToA = b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7); const damageToB = a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7); a.damage(damageToA, "\u55a7\u5629"); diff --git a/service-worker.js b/service-worker.js index a062aa1..6430d70 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "15.24.0"; +const APP_VERSION = "15.24.2"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.