From ad2cc76429276394e57909c8af9eb8d4a37f4554 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Tue, 30 Jun 2026 13:40:36 +0900 Subject: [PATCH] aa --- app_manifest.json | 2 +- css/components.css | 6 +- css/mobile.css | 2 +- css/panel.css | 36 ++++ index.html | 304 ++++++++++++++-------------- js/health.js | 2 +- js/item_lifecycle_support.js | 1 + js/item_render_runtime.js | 54 ++++- js/item_tool_definitions.js | 10 +- js/item_type_initializers.js | 5 +- js/save_codec.js | 4 +- js/save_schema.js | 2 +- js/sim_core.js | 17 +- js/snapshot_system.js | 18 +- js/tarinai_action_definitions.js | 27 +-- js/tarinai_behavior_text.js | 4 +- js/tarinai_contact_item_system.js | 21 ++ js/tarinai_identity_social.js | 18 +- js/tarinai_item_targeting.js | 6 +- js/tarinai_needs_items.js | 61 ++++-- js/tarinai_render.js | 27 +-- js/tarinai_seed_factory.js | 2 +- js/tarinai_social_action_runtime.js | 129 ++++++++++-- js/tarinai_social_move_life.js | 86 ++++---- js/tarinai_sunbath_system.js | 3 +- js/tarinai_update_step_frame.js | 22 +- js/text_catalog.js | 2 +- js/ui_selected.js | 14 +- js/version.js | 4 +- js/world_environment.js | 42 ++-- js/world_family_social.js | 28 ++- js/world_placement_log.js | 9 +- scripts/regression_check.py | 11 +- service-worker.js | 2 +- 34 files changed, 616 insertions(+), 365 deletions(-) diff --git a/app_manifest.json b/app_manifest.json index ebe8554..0e1a0fe 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.15.84", + "version": "39.15.90", "css": [ "css/base.css", "css/layout.css", diff --git a/css/components.css b/css/components.css index 512dbb1..49fbfb5 100644 --- a/css/components.css +++ b/css/components.css @@ -2,9 +2,9 @@ .tool-tip-popover { position: fixed; z-index: 2147483000; - width: max-content; - min-width: 160px; - max-width: min(320px, calc(100vw - 18px)); + width: fit-content; + min-width: min(160px, calc(100vw - 18px)); + max-width: min(620px, calc(100vw - 18px)); box-sizing: border-box; padding: 7px 9px; border-radius: 10px; diff --git a/css/mobile.css b/css/mobile.css index c79cdf6..dd123e7 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -192,7 +192,7 @@ body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; } } } body.touch-mobile-ui .tool-tip-popover { - max-width: min(240px, calc(100vw - 24px)); + max-width: min(520px, calc(100vw - 24px)); } /* v39.15.36: mobile navigation redesign without family input mode */ diff --git a/css/panel.css b/css/panel.css index 1843516..80cfa3e 100644 --- a/css/panel.css +++ b/css/panel.css @@ -452,3 +452,39 @@ body::before { letter-spacing: -0.08em !important; text-align: center !important; } + +/* v39.15.85: push family-tree personality tags to the node's right edge. */ +.lineage-family-tree .tree-node .family-personality-tags, +.family-personality-tags { + margin-left: auto !important; + margin-right: 0 !important; + justify-self: end !important; +} +.lineage-family-tree .tree-node .family-copy { + width: 100% !important; + max-width: 100% !important; +} +.lineage-family-tree .tree-node .family-copy strong, +.lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) { + max-width: calc(100% - 46px) !important; +} +.lineage-family-tree .tree-node .family-copy > .family-personality-tags { + max-width: 86px !important; + width: 86px !important; +} + +/* v39.15.87: let family personality tags use the full node width. */ +.lineage-family-tree .tree-node .family-copy > .family-personality-tags, +.lineage-family-tree .tree-node .family-personality-tags, +.family-personality-tags { + width: 100% !important; + max-width: 100% !important; + margin-left: 0 !important; + margin-right: 0 !important; + justify-self: stretch !important; + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; +} +.lineage-family-tree .tree-node .family-copy strong, +.lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) { + max-width: 100% !important; +} diff --git a/index.html b/index.html index e663d11..30ac200 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -218,152 +218,152 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/health.js b/js/health.js index 2d7ce63..ca31364 100644 --- a/js/health.js +++ b/js/health.js @@ -128,7 +128,7 @@ const HEALTH = (() => { } if (direct && direct !== CAUSES.stress) return direct; - const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || null; + const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? (CONFIG.standardTemperature ?? 15), t) || null; const recentTemperatureDamageAt = Number(t.lastTemperatureDamageAt || -999); const recentTemperatureDamage = (t.world?.time || 0) - recentTemperatureDamageAt <= 36; if (recentTemperatureDamage && tempStatus && (tempStatus.harmExcess || 0) > 0 && (t.energy <= 26 || t.stress >= 112 || t.mood <= 18)) { diff --git a/js/item_lifecycle_support.js b/js/item_lifecycle_support.js index dbb97fc..90d3394 100644 --- a/js/item_lifecycle_support.js +++ b/js/item_lifecycle_support.js @@ -16,6 +16,7 @@ function duplicatorLoadTypeForItem(item) { const type = String(item.type || ""); if (!type || type === "grass") return ""; if ((isPinType(type)) && item.pinState === "lodged") return ""; + if (type === "slave_chain" || type === "golden_crown") return ""; // Explicit list first. \u3053\u308c\u3067\u300c\u305a\u3093\u3060\u9905\u300d\u300c\u3078\u3053\u9905\u300d\u300c\u3051\u3093\u304b\u9905\u300d\u304c // serving-food \u5224\u5b9a\u3084 medicine role \u306e\u63fa\u308c\u306b\u5de6\u53f3\u3055\u308c\u305a\u8907\u88fd\u6a5f\u3078\u5165\u308b\u3002 if (DUPLICATOR_STORABLE_TYPES.has(type)) return type; diff --git a/js/item_render_runtime.js b/js/item_render_runtime.js index e9001f1..ebc9f71 100644 --- a/js/item_render_runtime.js +++ b/js/item_render_runtime.js @@ -80,7 +80,8 @@ draw(ctx, t, lighting = null) { if (owner && world?.isTarinaiHiddenInNestBox?.(owner)) return; } const servingRatio = isServingFoodType(this.type) ? ((this.foodServingsRemaining ?? this.amount) / Math.max(1, this.foodServingsMax || foodServingsForSize(this.toolSize || "medium"))) : null; - const visibleAlpha = isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1); + const forceOpaque = this.type === "slave_chain" || this.type === "golden_crown"; + const visibleAlpha = forceOpaque ? 1 : (isServingFoodType(this.type) ? clamp(servingRatio ?? 1, 0.24, 1) : clamp(this.amount / 40, 0.24, 1)); const dropT = this.dropMax > 0 ? clamp(this.dropTimer / this.dropMax, 0, 1) : 0; const dropFallHeight = this.type === "genkotsu" ? Math.max(360, this.r * 5.8) : 170; const dropY = dropT > 0 ? -dropFallHeight * dropT : 0; @@ -207,6 +208,57 @@ draw(ctx, t, lighting = null) { ctx.stroke(); } ctx.restore(); + + } else if (this.type === "slave_chain") { + const r = this.r || 15; + ctx.save(); + ctx.rotate(-0.35 + Math.sin(this.seed || 0) * 0.18); + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.strokeStyle = styleNight ? "rgba(122,118,112,0.94)" : "rgba(92,86,78,0.94)"; + ctx.lineWidth = Math.max(2.2, r * 0.18); + for (let i = -1; i <= 1; i++) { + ctx.save(); + ctx.translate(i * r * 0.52, Math.sin(i + this.seed) * r * 0.08); + ctx.rotate(i % 2 ? 0.65 : -0.65); + ctx.beginPath(); + ctx.ellipse(0, 0, r * 0.36, r * 0.20, 0, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + ctx.strokeStyle = "rgba(230,224,214,0.46)"; + ctx.lineWidth = Math.max(1.0, r * 0.06); + ctx.beginPath(); + ctx.moveTo(-r * 0.95, -r * 0.24); + ctx.lineTo(r * 0.86, -r * 0.18); + ctx.stroke(); + ctx.restore(); + } else if (this.type === "golden_crown") { + const r = this.r || 16; + ctx.save(); + ctx.rotate(0.08 + Math.sin(this.seed || 0) * 0.05); + ctx.fillStyle = styleNight ? "#c28a22" : "#f0bf3f"; + ctx.strokeStyle = styleNight ? "rgba(82,54,12,0.88)" : "rgba(124,78,12,0.82)"; + ctx.lineWidth = Math.max(1.4, r * 0.10); + ctx.beginPath(); + ctx.moveTo(-r * 0.92, r * 0.36); + ctx.lineTo(-r * 0.72, -r * 0.38); + ctx.lineTo(-r * 0.32, r * 0.02); + ctx.lineTo(0, -r * 0.62); + ctx.lineTo(r * 0.32, r * 0.02); + ctx.lineTo(r * 0.72, -r * 0.38); + ctx.lineTo(r * 0.92, r * 0.36); + ctx.closePath(); + ctx.fill(); ctx.stroke(); + ctx.fillStyle = "rgba(255,246,172,0.92)"; + for (const px of [-0.72, 0, 0.72]) { + ctx.beginPath(); + ctx.arc(r * px, px === 0 ? -r * 0.54 : -r * 0.32, Math.max(1.4, r * 0.09), 0, Math.PI * 2); + ctx.fill(); + } + ctx.fillStyle = "rgba(255,238,128,0.94)"; + ctx.fillRect(-r * 0.78, r * 0.20, r * 1.56, r * 0.24); + ctx.restore(); } else if (this.type === "water") { drawOvalWaterDropSprite(ctx, this.r * visualFieldScaleFor("water"), visualPaletteFor("water")); } else if (this.type === "grass") { diff --git a/js/item_tool_definitions.js b/js/item_tool_definitions.js index 2f4eb4f..1c0ed7e 100644 --- a/js/item_tool_definitions.js +++ b/js/item_tool_definitions.js @@ -33,6 +33,8 @@ const TOOL_DEFINITIONS = Object.freeze({ giant_drug: { id: "giant_drug", itemType: "giant_drug", label: "\u5de8\u5927\u85ac", placeable: true, scalable: true, radius: 22, amount: 52, icon: "assets/ui/tool_giant_drug.webp", tooltip: "\u4f53\u3092\u5927\u304d\u304f\u5f37\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u304c\u5927\u304d\u3044\u3002" }, dwarf_drug: { id: "dwarf_drug", itemType: "dwarf_drug", label: "\u77ee\u5c0f\u85ac", placeable: true, scalable: true, radius: 8, amount: 52, icon: "assets/ui/tool_dwarf_drug.webp", tooltip: "\u4f53\u3092\u5c0f\u3055\u304f\u5f31\u304f\u3059\u308b\u304c\u3001\u8ca0\u62c5\u3082\u5927\u304d\u3044\u3002" }, zunda_juice: { id: "zunda_juice", itemType: "zunda_juice", label: "\u305a\u3093\u3060\u6c41", placeable: true, scalable: true, radius: 15, amount: 70, icon: "assets/ui/tool_zunda_juice.webp", tooltip: "\u98df\u3079\u308b\u3068\u5168\u3066\u306e\u30a2\u30a4\u30c6\u30e0\u52b9\u679c\u3092\u9664\u53bb\u3059\u308b\u3002" }, + slave_chain: { id: "slave_chain", itemType: "slave_chain", label: "奴隷の鎖", placeable: true, scalable: true, radius: 15, amount: 1, iconText: "⛓", tooltip: "触れたたりないを強制的にずんちどれいにする。" }, + golden_crown: { id: "golden_crown", itemType: "golden_crown", label: "黄金の冠", placeable: true, scalable: true, radius: 16, amount: 1, iconText: "♛", tooltip: "触れたたりないを強制的にたりない王にする。" }, grass: { id: "grass", itemType: "grass", label: "\u8349", placeable: true, scalable: true, radius: 17, amount: 120, icon: "assets/ui/tool_grass.webp", tooltip: "\u98df\u3079\u7269\u3002\u305f\u308a\u306a\u3044\u304c\u5b89\u5fc3\u3059\u308b\u3002" }, stone: { id: "stone", itemType: "stone", label: "\u77f3", placeable: true, scalable: true, radius: 20, amount: 999, icon: "assets/ui/tool_stone.webp", tooltip: "\u91cd\u3044\u77f3\u3002\u843d\u3068\u3059\u3068\u5371\u306a\u3044\u3002" }, genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "\u62f3\u3092\u5730\u9762\u306b\u305f\u305f\u304d\u3064\u3051\u308b\u3002" }, @@ -42,7 +44,7 @@ const TOOL_DEFINITIONS = Object.freeze({ stove: { id: "stove", itemType: "stove", label: "ストーブ", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "🔥", collisionShape: "circle", tooltip: "周囲の気温を10℃上げる。効果は重ねられる。" }, ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" }, ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" }, - signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" }, + signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" }, duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 24, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u3001\u85ac\u3001\u92f2\u3092\u30bb\u30c3\u30c8\u3059\u308b\u3068\u7121\u9650\u306b\u4f9b\u7d66\u3067\u304d\u308b\u3002" }, firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" }, pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" }, @@ -66,12 +68,12 @@ const TOOL_DEFINITIONS = Object.freeze({ const ITEM_TRAITS = Object.freeze({ obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "dry_ice", "stove", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]), - food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]), + food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]), hazard: new Set(["firecracker", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]), pin: new Set(["pushpin", "oshibyo"]), kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]), sleepFurniture: new Set(["bed", "nest_box", "grass_bed"]), - draggable: new Set(["ball", "stone", "bed", "nest_box", "dry_ice", "stove", "signboard", "plushie", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]), + draggable: new Set(["ball", "stone", "bed", "nest_box", "dry_ice", "stove", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]), }); @@ -125,7 +127,7 @@ function pinBehaviorFor(type = "") { const TOOL_CATEGORIES = Object.freeze([ { id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose", "clear_tool"] }, { id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] }, - { id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] }, + { id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] }, { id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "bed", "nest_box", "dry_ice", "stove", "ball", "signboard"] }, { id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS }, { id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "shoot", "ant_nest"] }, diff --git a/js/item_type_initializers.js b/js/item_type_initializers.js index edbc3e2..b6d62fb 100644 --- a/js/item_type_initializers.js +++ b/js/item_type_initializers.js @@ -6,7 +6,7 @@ function initializeItemTypeState(item, type, x, y) { if (!item) return item; if (isServingFoodType(type) || ["grass", "ant_corpse", "zunchi"].includes(type)) item.roles.food = true; if (type === "water" || type === "zunda_juice") item.roles.drink = true; - if (["sweet", "water", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true; + if (["sweet", "water", "zunda_juice", "slave_chain", "golden_crown"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true; if (type === "bed" || type === "nest_box") item.roles.sleepPlace = true; if (["firecracker", "genkotsu", "pushpin", "poison_block", "splat"].includes(type)) item.roles.danger = true; if (type === "grass") item.roles.grassMaterial = true; @@ -19,6 +19,9 @@ function initializeItemTypeState(item, type, x, y) { item.lifeSpan = rand(420, 860); item.wither = 0; } + if (type === "slave_chain" || type === "golden_crown") { + item.amount = Math.max(1, Number(item.amount || 0) || 1); + } if (type === "bed") { item.comfort = rand(0.86, 1.14); item.wear = 0; diff --git a/js/save_codec.js b/js/save_codec.js index 33a7ce3..5ba9dea 100644 --- a/js/save_codec.js +++ b/js/save_codec.js @@ -385,7 +385,7 @@ if (tailMask & 1) writer.u(pinIdx); if (tailMask & 2) writer.u(nestIdx); if (tailMask & 4) { writeBitFields(writer, [stateId], [1]); writer.s(stateTarget); } - if (tailMask & 8) for (let i = 0; i < 4; i++) writer.s(genetics[i] || 100); + if (tailMask & 8) for (let i = 0; i < 5; i++) writer.s(genetics[i] ?? (i === 4 ? 0 : 100)); if (tailMask & 16) { writer.u(fightStats[0] || 0); writer.u(fightStats[1] || 0); writer.s(fightStats[2] || 0); } } function readTarinai(reader, coordState = null) { @@ -407,7 +407,7 @@ const pinIdx = (tailMask & 1) ? reader.u() : -1; const nestIdx = (tailMask & 2) ? reader.u() : -1; const stateInfo = (tailMask & 4) ? [readBitFields(reader, [1])[0], reader.s()] : [0, -1]; - const genetics = (tailMask & 8) ? [reader.s(), reader.s(), reader.s(), reader.s()] : []; + const genetics = (tailMask & 8) ? [reader.s(), reader.s(), reader.s(), reader.s(), reader.s()] : []; const fightStats = (tailMask & 16) ? [reader.u(), reader.u(), reader.s()] : []; return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats]; } diff --git a/js/save_schema.js b/js/save_schema.js index dcf933f..715610e 100644 --- a/js/save_schema.js +++ b/js/save_schema.js @@ -21,7 +21,7 @@ "grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h", - "splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner", "dry_ice", "stove", "glass_wall" + "splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner", "dry_ice", "stove", "glass_wall", "slave_chain", "golden_crown" ]); function typeIds(names) { diff --git a/js/sim_core.js b/js/sim_core.js index 11915f8..0a87529 100644 --- a/js/sim_core.js +++ b/js/sim_core.js @@ -65,7 +65,7 @@ function stableUnit(seed, salt = "") { } -const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]; +const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul", "temperatureOffset"]; const GENETIC_LIMITS = Object.freeze({ lifeSpanMul: [0.82, 1.24], attackMul: [0.80, 1.24], @@ -74,8 +74,12 @@ const GENETIC_LIMITS = Object.freeze({ }); function clampGeneticValue(key, value) { + const fallback = key === "temperatureOffset" ? 0 : 1; + const n = Number(value); + const finite = Number.isFinite(n) ? n : fallback; + if (key === "temperatureOffset") return finite; const range = GENETIC_LIMITS[key] || [0.75, 1.25]; - return clamp(Number(value) || 1, range[0], range[1]); + return clamp(finite, range[0], range[1]); } function randomGenetics(seed = "") { @@ -84,6 +88,7 @@ function randomGenetics(seed = "") { attackMul: clampGeneticValue("attackMul", 0.80 + stableUnit(seed, "gene-attack") * 0.44), speedMul: clampGeneticValue("speedMul", 0.86 + stableUnit(seed, "gene-speed") * 0.32), sizeMul: clampGeneticValue("sizeMul", 0.84 + stableUnit(seed, "gene-size") * 0.34), + temperatureOffset: clampGeneticValue("temperatureOffset", -5 + stableUnit(seed, "gene-temperature-offset") * 10), }; } @@ -102,8 +107,12 @@ function inheritedGenetics(a, b, seed = "") { const gb = normalizeGenetics(b?.genetics, b?.familyKey || b?.id || "b"); const out = {}; for (const key of GENETIC_KEYS) { - const base = ((ga[key] || 1) + (gb[key] || 1)) / 2; - const mutation = (stableUnit(seed, `gene-mut-${key}`) - 0.5) * 0.10; + const fallback = key === "temperatureOffset" ? 0 : 1; + const av = Number.isFinite(Number(ga[key])) ? Number(ga[key]) : fallback; + const bv = Number.isFinite(Number(gb[key])) ? Number(gb[key]) : fallback; + const base = (av + bv) / 2; + const mutationScale = key === "temperatureOffset" ? 2.5 : 0.10; + const mutation = (stableUnit(seed, `gene-mut-${key}`) - 0.5) * mutationScale; out[key] = clampGeneticValue(key, base + mutation); } return out; diff --git a/js/snapshot_system.js b/js/snapshot_system.js index b9f9f8d..4aa25e2 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -86,12 +86,22 @@ } function geneticsToArray(g = {}) { const normalized = typeof global.normalizeGenetics === "function" ? global.normalizeGenetics(g, "") : g; - return ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"].map(k => q(Number(normalized?.[k]) || 1, 100, 100)); + return [ + q(Number(normalized?.lifeSpanMul) || 1, 100, 100), + q(Number(normalized?.attackMul) || 1, 100, 100), + q(Number(normalized?.speedMul) || 1, 100, 100), + q(Number(normalized?.sizeMul) || 1, 100, 100), + q(Number(normalized?.temperatureOffset) || 0, 10, 0), + ]; } function arrayToGenetics(arr = []) { - const keys = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]; - const out = {}; - keys.forEach((k, i) => { out[k] = u(arr[i], 100, 1); }); + const out = { + lifeSpanMul: u(arr[0], 100, 1), + attackMul: u(arr[1], 100, 1), + speedMul: u(arr[2], 100, 1), + sizeMul: u(arr[3], 100, 1), + temperatureOffset: u(arr[4], 10, 0), + }; return typeof global.normalizeGenetics === "function" ? global.normalizeGenetics(out, "") : out; } function flagPack(t = {}) { diff --git a/js/tarinai_action_definitions.js b/js/tarinai_action_definitions.js index cbec2ee..020d45f 100644 --- a/js/tarinai_action_definitions.js +++ b/js/tarinai_action_definitions.js @@ -210,8 +210,8 @@ function createTemperatureComfortActionSpec() { label: "快適な温度帯へ移動している", phrase: "快適な温度帯を探している", text(t, world) { - const felt = world?.temperatureAt?.(t?.x, t?.y) ?? t?.feltTemperature ?? t?.tempComfort ?? (CONFIG.standardTemperature ?? 15); - const status = world?.temperatureStatusFor?.(felt) || t?.temperatureStatus || null; + const felt = world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t?.x, t?.y, t) ?? t?.feltTemperature ?? t?.tempComfort ?? (CONFIG.standardTemperature ?? 15); + const status = world?.temperatureStatusFor?.(felt, t) || t?.temperatureStatus || null; return status?.direction === "cold" ? "寒いので温まろうとしている。" : "暑いので涼もうとしている。"; }, weight: 42, @@ -219,20 +219,20 @@ function createTemperatureComfortActionSpec() { return world?.findComfortTemperatureSpot?.(t, { force: !!ctx.force, requireComfort: true }) || null; }, canStart(t, world, ctx = {}) { - const felt = world?.temperatureAt?.(t.x, t.y); - const status = world?.temperatureStatusFor?.(Number.isFinite(Number(felt)) ? felt : (t?.feltTemperature ?? CONFIG.standardTemperature ?? 15)); + const felt = world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t.x, t.y, t); + const status = world?.temperatureStatusFor?.(Number.isFinite(Number(felt)) ? felt : (t?.feltTemperature ?? CONFIG.standardTemperature ?? 15), t); if (!status || status.comfortable || (status.discomfort || 0) < 2.5) return false; return !!(ctx.target ?? this.selectTarget(t, world, ctx)); }, start(t, world, ctx = {}) { - const status = world?.temperatureStatusFor?.(world?.temperatureAt?.(t.x, t.y) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15)); + const status = world?.temperatureStatusFor?.(world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t.x, t.y, t) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15), t); const label = status?.direction === "cold" ? "あたたかい場所へ移動している" : (status?.direction === "hot" ? "涼しい場所へ移動している" : this.label); const target = ctx.target ?? this.selectTarget(t, world, ctx); return moveToOrUse(t, target, "seek_temperature", label); }, tick(t, world, dt, needs, ctx = {}) { - const felt = world?.temperatureAt?.(t.x, t.y) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15); - const status = world?.temperatureStatusFor?.(felt); + const felt = world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t.x, t.y, t) ?? t?.feltTemperature ?? (CONFIG.standardTemperature ?? 15); + const status = world?.temperatureStatusFor?.(felt, t); if (status?.comfortable || (status?.discomfort || 0) <= 1.5) { if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { health: 10, safety: 8 }, "temperature"); t.goIdle?.("快適な温度になった"); @@ -287,7 +287,7 @@ function createWanderActionSpec() { } function selectFightRivalTarget(tarinai, world, range = 360) { - const candidate = conflictTargetFor(tarinai, world, (tarinai?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(tarinai) ? 18 : 34); + const candidate = conflictTargetFor(tarinai, world, (tarinai?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(tarinai) ? 18 : 14); const current = currentBehaviorTarget(tarinai); return isLiveTarinaiEntity(candidate?.target) ? candidate.target @@ -395,17 +395,12 @@ function createFightRivalActionSpec() { canStart(t, world, ctx = {}) { if ((t.fightCooldown || 0) > 0.04 || (t.postConflictPeaceTimer || 0) > 0.04) return false; const forced = (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t); - const candidate = conflictTargetFor(t, world, forced ? 24 : 42); - if (!forced) { - const parts = t?.socialReasonParts || {}; - const conflict = Number(parts.conflict || 0) || 0; - const socialPull = Math.max(Number(parts.bond || 0) || 0, Number(parts.mate || 0) || 0, Number(parts.family || 0) || 0); - if (!candidate?.target || conflict < 22 || conflict < socialPull * 0.68) return false; - } + const candidate = conflictTargetFor(t, world, forced ? 24 : 7); + if (!candidate?.target && !ctx.target) return false; return !!(ctx.target ?? candidate?.target ?? this.selectTarget(t, world, ctx)); }, start(t, world, ctx = {}) { - const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 18 : 26); + const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 18 : 7); const rival = ctx.target ?? this.selectTarget(t, world, ctx); if (!isLiveTarinaiEntity(rival)) return false; t.conflictTargetId = rival.id; diff --git a/js/tarinai_behavior_text.js b/js/tarinai_behavior_text.js index 689c2af..bd52e09 100644 --- a/js/tarinai_behavior_text.js +++ b/js/tarinai_behavior_text.js @@ -99,8 +99,8 @@ function causePhraseForReason(chosenNeed, action, tarinai = null, options = {}) function buildReasonText(chosenNeed, tiedNeeds, action, tarinai = null, world = null, options = {}) { if (action?.id === "seek_comfort_temperature") { - const felt = tarinai?.world?.temperatureAt?.(tarinai?.x, tarinai?.y) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15); - const status = tarinai?.world?.temperatureStatusFor?.(felt) || tarinai?.temperatureStatus || null; + const felt = tarinai?.world?.feltTemperatureFor?.(tarinai) ?? world?.temperatureAt?.(tarinai?.x, tarinai?.y, tarinai) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15); + const status = tarinai?.world?.temperatureStatusFor?.(felt, tarinai) || tarinai?.temperatureStatus || null; return status?.direction === "cold" ? "寒いので温まろうとしている。" : "暑いので涼もうとしている。"; } const actionText = actionTextForReason(action, tarinai); diff --git a/js/tarinai_contact_item_system.js b/js/tarinai_contact_item_system.js index 3e04dfe..809475b 100644 --- a/js/tarinai_contact_item_system.js +++ b/js/tarinai_contact_item_system.js @@ -11,6 +11,27 @@ if (!it || it.dead) return false; const d = it?.type === "duplicator" ? foodInteractionDistance(tarinai, it) : dist(tarinai, it); const contactReach = foodInteractionReach(tarinai, it, "food") + (it === tarinai.target && foodActionActive && it?.type !== "duplicator" ? 4 : 0); if (d > contactReach) return false; + + if (it.type === "slave_chain" || it.type === "golden_crown") { + const itemLabel = typeof toolLabel === "function" ? toolLabel(it.type) : (it.type === "slave_chain" ? "奴隷の鎖" : "黄金の冠"); + if (it.type === "slave_chain") { + tarinai.isTarinaiChampion = false; + tarinai.tarinaiChampionSince = 0; + tarinai.becomeZunchiSlave?.({ locked: true, forcedItem: true }); + tarinai.bubble("鎖", 2.4, "rgba(74,64,56,0.82)"); + tarinai.world?.log?.(`${tarinai.name}は${itemLabel}に触れてずんちどれいになった。`, "event", { participants: [tarinai], sound: false }); + } else { + tarinai.becomeTarinaiChampion?.({ force: true, forcePersonality: true, forcedItem: true }); + tarinai.bubble("王", 2.4, "rgba(166,112,22,0.84)"); + tarinai.world?.log?.(`${tarinai.name}は${itemLabel}に触れてたりない王になった。`, "event", { participants: [tarinai], sound: false }); + } + global.TarinaiStructureLifecycle?.deleteItem?.(tarinai.world, it, { reason: `consumed-${it.type}`, userReason: `${itemLabel}が使われた`, wake: false, panicOwner: false }); + tarinai.world?.recordFamily?.(tarinai); + tarinai.world?.markFamilyTreeDirty?.(`consume-${it.type}`); + if (global.uiCache) global.uiCache.selectedSnapshot = ""; + return true; + } + if (it.type === "water") { tarinai.thought = "\u6c34\u3092\u6d74\u3073\u3066\u843d\u3061\u7740\u3044\u3066\u3044\u308b"; tarinai.target = it; diff --git a/js/tarinai_identity_social.js b/js/tarinai_identity_social.js index 865a431..0665dad 100644 --- a/js/tarinai_identity_social.js +++ b/js/tarinai_identity_social.js @@ -467,23 +467,13 @@ temperatureDiscomfortSprite(baseId = "") { const world = this.world || null; - const felt = world?.temperatureAt?.(this.x, this.y) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15); - const status = this.temperatureStatus || world?.temperatureStatusFor?.(felt) || null; + const felt = world?.feltTemperatureFor?.(this) ?? world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15); + const status = this.temperatureStatus || world?.temperatureStatusFor?.(felt, this) || null; if (!status || status.comfortable || (status.discomfort || 0) < 2.5) return ""; const pool = (status.direction === "cold" ? ["cold_1", "cold_2", "cold_3"] : ["hot_1", "hot_2", "hot_3"]).filter(id => SPRITES.some(s => s.id === id)); if (!pool.length) return ""; - const mapped = ({ - smile: 0, - normal_happy: 0, - normal_tongue: 0, - jito: 1, - normal_smirk: 1, - pokan: 2, - angry: 2, - hungry_70: 2, - })[String(baseId || "")]; - const fallbackIndex = status.harmful ? 2 : ((status.discomfort || 0) >= 7 ? 1 : 0); - const index = clamp(Number.isFinite(mapped) ? mapped : fallbackIndex, 0, pool.length - 1); + const u = typeof stableUnit === "function" ? stableUnit(this.familyKey || this.id || this.birthSeed || baseId || "temperature", `temperature-sprite:${status.direction}`) : Math.random(); + const index = Math.max(0, Math.min(pool.length - 1, Math.floor(u * pool.length))); return pool[index] || pool[0] || ""; }, diff --git a/js/tarinai_item_targeting.js b/js/tarinai_item_targeting.js index 44fa094..e46e247 100644 --- a/js/tarinai_item_targeting.js +++ b/js/tarinai_item_targeting.js @@ -25,7 +25,7 @@ function effectiveFoodRoleMatches(item, role) { if (!type) return false; if (role === "food") return ["sweet", "food", "grass", "ant_corpse", "zunchi"].includes(type) || (isServingFoodType(type)); if (role === "drink") return type === "water" || type === "zunda_juice"; - if (role === "medicine") return type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (isParamEffectItemType(type)); + if (role === "medicine") return type === "sweet" || type === "water" || type === "zunda_juice" || ["sleep_drug", "slave_chain", "golden_crown"].includes(type) || (isParamEffectItemType(type)); return false; } @@ -146,7 +146,7 @@ function foodRoleSearchTypes(role = "") { } else if (role === "drink") { for (const type of ["water", "zunda_juice", "duplicator"]) set.add(type); } else if (role === "medicine") { - for (const type of ["sweet", "water", "zunda_juice", "sleep_drug", "love_mochi", "fight_mochi", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "duplicator"]) set.add(type); + for (const type of ["sweet", "water", "zunda_juice", "sleep_drug", "love_mochi", "fight_mochi", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown", "duplicator"]) set.add(type); if (typeof PARAM_EFFECT_ITEM_TYPES !== "undefined") for (const type of PARAM_EFFECT_ITEM_TYPES) set.add(type); } return set; @@ -301,7 +301,7 @@ function evaluateConflictUrge(tarinai, world, maxDist = 96) { }; } } - return best && best.score > (best.forced ? 4 : 8) ? best : null; + return best && best.score >= (best.forced ? 4 : 7) ? best : null; } function conflictTargetFor(tarinai, world, minScore = 34) { diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index b3d761c..190750c 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -76,9 +76,9 @@ function updateNeeds(tarinai, world, dt = 0) { if ((tarinai.hurtTimer || 0) > 0.04 || ((tarinai.stuckPushpinId || "") && (lodgedBehavior?.panicOnAttach || (lodgedBehavior?.damagePerTick || 0) > 0 || (lodgedBehavior?.damageOnAttach || 0) > 0))) raw.health += 36; const danger = findNearbyDanger(world, tarinai, 170); if (danger) raw.safety += danger.kind ? 78 : 48; - const feltTemperature = world?.temperatureAt?.(tarinai.x, tarinai.y); + const feltTemperature = world?.feltTemperatureFor?.(tarinai) ?? world?.temperatureAt?.(tarinai.x, tarinai.y, tarinai); if (Number.isFinite(Number(feltTemperature))) { - const tempStatus = world.temperatureStatusFor?.(feltTemperature) || null; + const tempStatus = world.temperatureStatusFor?.(feltTemperature, tarinai) || null; tarinai.feltTemperature = Number(feltTemperature); tarinai.tempComfort = Number(feltTemperature); tarinai.temperatureStatus = tempStatus; @@ -100,7 +100,7 @@ function updateNeeds(tarinai, world, dt = 0) { if (conflict?.target && (conflict.forced || conflict.score >= 14)) { tarinai.conflictTargetId = conflict.target.id; tarinai.conflictUrge = Math.max(Number(tarinai.conflictUrge || 0) || 0, conflict.score); - const conflictPart = conflict.score * 0.56; + const conflictPart = conflict.score; socialParts.conflict += conflictPart; raw.social += conflictPart; if (conflict.defensive) raw.safety += conflict.score * 0.24; @@ -255,16 +255,37 @@ function actionSelectionBonus(action, need, tarinai, options = {}) { } +function socialFightMateActionWeights(tarinai) { + const parts = tarinai?.socialReasonParts || {}; + const prof = tarinai?.personalityProfile?.() || {}; + const cur = tarinai?.currentPersonality || {}; + const aggression = Number(cur.aggression ?? prof.fight ?? 0) || 0; + const openness = Number(cur.openness ?? prof.play ?? 0) || 0; + const sociability = Number(cur.sociability ?? prof.social ?? 0) || 0; + let fightWeight = 4; + let mateWeight = 6; + fightWeight *= clamp(1 + aggression * 0.55, 0.25, 2.8); + mateWeight *= clamp(1 + openness * 0.34 + sociability * 0.20, 0.34, 2.4); + fightWeight *= 1 + Math.min(1.9, Math.max(0, Number(parts.conflict || 0) || 0) / 34); + mateWeight *= 1 + Math.min(1.9, Math.max(0, Number(parts.mate || 0) || 0) / 34); + fightWeight *= clamp(1 - Math.max(0, Number(parts.bond || 0) || 0) / 180, 0.55, 1.04); + if ((tarinai?.fightMochiTimer || 0) > 0.04) { fightWeight *= 3.4; mateWeight *= 0.55; } + if ((tarinai?.loveMochiTimer || 0) > 0.04) { mateWeight *= 3.15; fightWeight *= 0.58; } + const total = Math.max(0, fightWeight) + Math.max(0, mateWeight); + return { fightWeight, mateWeight, fightProbability: total > 0 ? fightWeight / total : 0.4 }; +} + + // regression guard baseline: return ({ conflict: 34, mate: 24, family: 12, bond: 14 // regression guard baseline: subValue >= threshold && subValue >= socialPull * 0.82 function socialSubNeedStartThreshold(subNeed = "") { - return ({ conflict: 26, mate: 24, family: 12, bond: 14, fearSocial: 18 })[subNeed] ?? 16; + return ({ conflict: 18, mate: 24, family: 12, bond: 14, fearSocial: 18 })[subNeed] ?? 16; } function shouldStartActionBySubNeed(tarinai, action, needs) { const need = action?.need || "fulfill"; if (action?.id === "seek_comfort_temperature") { - const status = tarinai?.temperatureStatus || tarinai?.world?.temperatureStatusFor?.(tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15)); + const status = tarinai?.temperatureStatus || tarinai?.world?.temperatureStatusFor?.(tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15), tarinai); return Boolean(status && (status.discomfort || 0) >= 2.5); } if (need !== "social" || !action?.subNeed) return shouldStartNeedAction(tarinai, need, needs); @@ -280,14 +301,13 @@ function shouldStartActionBySubNeed(tarinai, action, needs) { : 0; const threshold = Math.max(6, socialSubNeedStartThreshold(subNeed) - modifier); const isContinuingSameAction = (getTarinaiBehaviorId(tarinai)) === action.id; - if (subNeed !== "conflict" && subValue >= threshold) return true; - if (subNeed !== "conflict" && isContinuingSameAction && subValue >= threshold * 0.55) return true; - if (subNeed === "mate") return total >= needThreshold("social", "start") && subValue >= threshold * 0.62 && ((tarinai?.loveMochiTimer || 0) > 0.04 || (tarinai?.reproductionTimer || 0) <= 6 || Number(parts.mate || 0) >= Number(parts.bond || 0) - 2); - if (subNeed === "conflict") { - if ((tarinai?.fightMochiTimer || 0) > 0.04) return total >= needThreshold("social", "start") && subValue >= threshold * 0.60; - const socialPull = Math.max(Number(parts.bond || 0), Number(parts.mate || 0), Number(parts.family || 0)); - return total >= needThreshold("social", "start") && subValue >= threshold * 0.82 && subValue >= socialPull * 0.68; + if (subNeed === "mate" || subNeed === "conflict") { + const relationDrive = Math.max(Number(parts.mate || 0) || 0, Number(parts.conflict || 0) || 0, total * 0.36); + const drugBoosted = (tarinai?.loveMochiTimer || 0) > 0.04 || (tarinai?.fightMochiTimer || 0) > 0.04; + return total >= needThreshold("social", "start") && (drugBoosted || relationDrive >= Math.min(socialSubNeedStartThreshold("mate"), socialSubNeedStartThreshold("conflict")) * 0.48); } + if (subValue >= threshold) return true; + if (isContinuingSameAction && subValue >= threshold * 0.55) return true; return total >= needThreshold("social", "start") && subValue >= threshold * 0.36; } @@ -329,8 +349,8 @@ function choosePriorityNeedAction(needs, tarinai, world) { let priority = actionPriority(action); const needValue = Number(needs?.[need] || 0) || 0; if (action.id === "seek_comfort_temperature") { - const felt = tarinai?.world?.temperatureAt?.(tarinai?.x, tarinai?.y) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15); - const status = tarinai?.world?.temperatureStatusFor?.(felt) || tarinai?.temperatureStatus || null; + const felt = tarinai?.world?.feltTemperatureFor?.(tarinai) ?? tarinai?.world?.temperatureAt?.(tarinai?.x, tarinai?.y, tarinai) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15); + const status = tarinai?.world?.temperatureStatusFor?.(felt, tarinai) || tarinai?.temperatureStatus || null; priority = (status?.harmful || (status?.harmExcess || 0) > 0) ? 92 : 53; } if (action.id === "eat_food") { @@ -354,7 +374,16 @@ function choosePriorityNeedAction(needs, tarinai, world) { const samePriority = weighted.filter(e => e.priority === selectedEntry.priority); let action = selectedEntry.action; - if (samePriority.length > 1 && samePriority.includes(selectedEntry)) { + const sameSocial = samePriority.filter(e => e.need === "social"); + const normalFightEntry = sameSocial.find(e => e.action?.id === "fight_rival"); + const normalMateEntry = sameSocial.find(e => e.action?.id === "approach_mate"); + if (!isCurrentBehaviorForced(tarinai) && normalFightEntry && normalMateEntry && samePriority.includes(selectedEntry)) { + const weights = socialFightMateActionWeights(tarinai); + const bucket = Math.floor((Number(world?.time || 0) || 0) / 3); + const pickFight = deterministicChance(world, "social-fight-vs-mate-weighted", weights.fightProbability, tarinai, normalFightEntry.action.id, normalMateEntry.action.id, bucket); + selectedEntry = pickFight ? normalFightEntry : normalMateEntry; + action = selectedEntry.action; + } else if (samePriority.length > 1 && samePriority.includes(selectedEntry)) { const relationSameBand = samePriority.some(e => e.need === "social") ? samePriority.filter(e => e.need === "social") : samePriority; @@ -944,6 +973,8 @@ function resolveNeedsCore(dt) { if (!Number.isFinite(this.world?.w) || !Number.isFinite(this.world?.h)) return false; if ((this.entryTimer || 0) > 0.04) return false; if (!this.isSunbathTime()) return false; + const currentFelt = this.world?.feltTemperatureFor?.(this) ?? this.world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15); + if (Number.isFinite(Number(currentFelt)) && Number(currentFelt) >= 22) return false; if ((this.sunbathCooldown || 0) > 0) return false; if (this.stuckPushpinId || this.currentLodgedPin?.()) return false; if (["fight", "panic", "cursor_enemy", "eat", "seek_food", "seek_water", "seek_bed", "sleep", "birth_ritual", "ant_attack", "intimidate", "ant_intimidate"].includes(this.state)) return false; diff --git a/js/tarinai_render.js b/js/tarinai_render.js index fdd2c7f..3911a42 100644 --- a/js/tarinai_render.js +++ b/js/tarinai_render.js @@ -186,7 +186,7 @@ const intimidateShake = this.intimidateTimer > 0.04 ? Math.sin(t * 44 + this.age) * this.radius * 0.09 : 0; const fearShakeY = this.intimidatedTimer > 0.04 ? Math.sin(t * 52 + this.age * 0.7) * this.radius * 0.11 : 0; const birthShake = this.birthRitualTimer > 0.04 ? Math.sin(t * 34 + this.age) * this.radius * 0.07 : 0; - const coldStatus = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || null; + const coldStatus = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15), this) || null; const coldShakeActive = Boolean(coldStatus && coldStatus.direction === "cold" && (coldStatus.discomfort || 0) > 0.5 && this.state !== "sunbath"); const coldShakeStrength = coldShakeActive ? clamp((coldStatus.discomfort || 0) / 12, 0.25, 1.25) : 0; const coldShakeX = coldShakeActive ? (Math.sin(t * 46 + this.age * 0.9) + Math.sin(t * 71 + this.age * 0.31) * 0.55) * this.radius * 0.035 * coldShakeStrength : 0; @@ -328,29 +328,6 @@ ctx.restore(); } - if (showDecor && coldShakeActive) { - ctx.save(); - ctx.globalAlpha = 0.22 * nestHideAlpha * clamp(coldShakeStrength, 0.25, 1.15); - ctx.strokeStyle = "rgba(108, 166, 220, 0.86)"; - ctx.lineWidth = Math.max(1.0, this.radius * 0.045); - ctx.lineCap = "round"; - const side = Math.max(10, drawW * 0.36); - const top = -drawH * 0.28; - const amp = Math.max(2, this.radius * 0.08); - for (const dir of [-1, 1]) { - for (let i = 0; i < 2; i++) { - const ox = dir * (side + i * drawW * 0.055); - const oy = top + i * drawH * 0.22 + Math.sin(t * 24 + i + this.age) * amp * 0.35; - ctx.beginPath(); - ctx.moveTo(ox, oy - amp); - ctx.lineTo(ox + dir * amp * 0.55, oy); - ctx.lineTo(ox, oy + amp); - ctx.stroke(); - } - } - ctx.restore(); - } - if (this.world.selected === this) { ctx.save(); const pulse = 0.5 + 0.5 * Math.sin(this.world.time * 7.5); @@ -600,7 +577,7 @@ }, contains(x, y) { - if (this.insideNestBoxId || (this.nestFade || 0) >= 0.92) return false; + if (this.insideNestBoxId || (this.nestFade || 0) > 0.05) return false; const hit = this.world.screenSizeToWorld ? this.world.screenSizeToWorld(this.radius * 1.9) : this.radius * 1.9; return distXY(this.x, this.y, x, y) < hit; } diff --git a/js/tarinai_seed_factory.js b/js/tarinai_seed_factory.js index 86bfce8..8c92b87 100644 --- a/js/tarinai_seed_factory.js +++ b/js/tarinai_seed_factory.js @@ -103,7 +103,7 @@ function seededGenetics(seed = "") { if (typeof normalizeGenetics === "function") return normalizeGenetics(null, seed); - return { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 }; + return { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1, temperatureOffset: 0 }; } function seededAdultScale(seed = "", genetics = null) { diff --git a/js/tarinai_social_action_runtime.js b/js/tarinai_social_action_runtime.js index 4d53d9b..17d6372 100644 --- a/js/tarinai_social_action_runtime.js +++ b/js/tarinai_social_action_runtime.js @@ -11,6 +11,93 @@ function mateTargetFor(t, world, maxDist = 520) { return world.nearestOther?.(t, maxDist, ok) || null; } +function socialFightMateWeights(t, other, world, options = {}) { + if (!t || !other || !world || t === other || t.dead || other.dead) return { fightWeight: 0, mateWeight: 0, fightEligible: false, mateEligible: false, fightProbability: 0 }; + const d = Number.isFinite(Number(options.distance)) ? Number(options.distance) : dist(t, other); + const fightMochi = (t.fightMochiTimer || 0) > 0.04 || (other.fightMochiTimer || 0) > 0.04 || t.hasFightMochiEffect?.() || other.hasFightMochiEffect?.(); + const loveMochi = (t.loveMochiTimer || 0) > 0.04 || (other.loveMochiTimer || 0) > 0.04 || t.hasLoveMochiEffect?.() || other.hasLoveMochiEffect?.(); + const aProf = t.personalityProfile?.() || {}; + const bProf = other.personalityProfile?.() || {}; + const aCur = t.currentPersonality || {}; + const bCur = other.currentPersonality || {}; + const avg = (a, b, fallback = 0) => ((Number.isFinite(Number(a)) ? Number(a) : fallback) + (Number.isFinite(Number(b)) ? Number(b) : fallback)) * 0.5; + const aggression = avg(aCur.aggression ?? aProf.fight, bCur.aggression ?? bProf.fight, 0); + const openness = avg(aCur.openness ?? aProf.play, bCur.openness ?? bProf.play, 0); + const sociability = avg(aCur.sociability ?? aProf.social, bCur.sociability ?? bProf.social, 0); + const fearTrait = avg(aProf.fear, bProf.fear, 1); + const relationA = typeof t.relationTo === "function" ? t.relationTo(other.id) : {}; + const relationB = typeof other.relationTo === "function" ? other.relationTo(t.id) : {}; + const affinity = Math.max(Number(relationA.affinity || 0) || 0, Number(relationB.affinity || 0) || 0); + const fear = Math.max(Number(relationA.fear || 0) || 0, Number(relationB.fear || 0) || 0); + const postBirthPeace = (t.postBirthPeaceTimer || 0) > 0.04 || (other.postBirthPeaceTimer || 0) > 0.04; + const postConflictPeace = (t.postConflictPeaceTimer || 0) > 0.04 || (other.postConflictPeaceTimer || 0) > 0.04; + const familyFightBlocked = world.areParentChild?.(t, other) || world.areCoParents?.(t, other); + const fightEligible = Boolean(world.canFightPair?.(t, other)) + && !familyFightBlocked + && !postBirthPeace + && !postConflictPeace + && (t.fightCooldown || 0) <= 0.04 + && (other.fightCooldown || 0) <= 0.04 + && (t.fightTimer || 0) <= 0.04 + && (other.fightTimer || 0) <= 0.04 + && (t.birthRitualTimer || 0) <= 0.04 + && (other.birthRitualTimer || 0) <= 0.04 + && (world.fightPairCooldownRemaining?.(t, other) || 0) <= 0.04 + && d <= Math.max(Number(options.fightRange || 58) || 58, (t.radius || 20) + (other.radius || 20) + 18); + const sameStatus = !!t.isZunchiSlave === !!other.isZunchiSlave; + const mateWindow = Number.isFinite(Number(options.mateTimerWindow)) ? Number(options.mateTimerWindow) : 10; + const requireBirthReady = options.requireBirthReady !== false; + const timerOk = requireBirthReady ? ((t.reproductionTimer || 0) <= 0.04 && (other.reproductionTimer || 0) <= 0.04) : ((t.reproductionTimer || 0) <= mateWindow && (other.reproductionTimer || 0) <= mateWindow); + const mateDistanceOk = d <= Math.max(loveMochi ? 54 : 44, (t.radius || 20) + (other.radius || 20) + 14); + const enough = loveMochi ? ((t.hunger || 0) < 90 && (other.hunger || 0) < 92 && (t.energy || 0) > 18 && (other.energy || 0) > 18) : ((t.hunger || 0) < 55 && (other.hunger || 0) < 60 && (t.energy || 0) > 44 && (other.energy || 0) > 44); + const lonelyBond = loveMochi ? ((t.loneliness || 0) > 4 || (other.loneliness || 0) > 4 || (t.affection || 0) > 0 || (other.affection || 0) > 0) : ((t.loneliness || 0) > 24 || (other.loneliness || 0) > 24); + const mateEligible = sameStatus + && timerOk + && mateDistanceOk + && !world.areParentChild?.(t, other) + && !t.sleepDisease && !other.sleepDisease && !t.fightDisease && !other.fightDisease + && (t.fightTimer || 0) <= 0.04 && (other.fightTimer || 0) <= 0.04 + && (t.birthRitualTimer || 0) <= 0.04 && (other.birthRitualTimer || 0) <= 0.04 + && (!requireBirthReady || (enough && lonelyBond)); + let fightWeight = fightEligible ? 4 : 0; + let mateWeight = mateEligible ? 6 : 0; + if (fightWeight > 0) { + const stressPressure = clamp((((t.stress || 0) + (other.stress || 0)) * 0.5 - 36) / 72, 0, 1.8); + const safetyPressure = clamp((((t.needs?.safety || 0) + (other.needs?.safety || 0)) * 0.5 - 36) / 72, 0, 1.4); + fightWeight *= clamp(1 + aggression * 0.55, 0.25, 2.8); + fightWeight *= 1 + stressPressure * 0.58 + safetyPressure * 0.24; + if (t.type === "angry" || other.type === "angry") fightWeight *= 1.42; + if (t.isZunchiSlave || other.isZunchiSlave) fightWeight *= 1.22; + if (fightMochi) fightWeight *= 3.4; + if (loveMochi) fightWeight *= 0.58; + fightWeight *= clamp(1 - affinity / 150, 0.34, 1.08); + fightWeight *= clamp(1 - fear / 170, fightMochi ? 0.72 : 0.42, 1.05); + fightWeight *= clamp(1.14 - Math.max(0, fearTrait - 1) * 0.16, 0.62, 1.22); + if (options.mode === "conflict") fightWeight *= 1.35; + } + if (mateWeight > 0) { + const stressPressure = clamp((((t.stress || 0) + (other.stress || 0)) * 0.5 - 42) / 84, 0, 1.4); + mateWeight *= clamp(1 + openness * 0.34 + sociability * 0.22, 0.34, 2.4); + mateWeight *= clamp(1 - stressPressure * 0.22, 0.55, 1.0); + mateWeight *= 1 + Math.min(0.70, affinity / 140); + if (loveMochi) mateWeight *= 3.15; + if (fightMochi) mateWeight *= 0.52; + if (options.mode === "mate") mateWeight *= 1.42; + } + const total = Math.max(0, fightWeight) + Math.max(0, mateWeight); + return { fightWeight, mateWeight, fightEligible, mateEligible, fightProbability: total > 0 ? fightWeight / total : 0, totalWeight: total }; +} + +function pickSocialFightMate(world, key, weights, ...seeds) { + const fightWeight = Math.max(0, Number(weights?.fightWeight || 0) || 0); + const mateWeight = Math.max(0, Number(weights?.mateWeight || 0) || 0); + if (fightWeight <= 0 && mateWeight <= 0) return "none"; + if (fightWeight > 0 && mateWeight <= 0) return "fight"; + if (mateWeight > 0 && fightWeight <= 0) return "mate"; + const bucket = Math.floor((Number(world?.time || 0) || 0) / 3); + return deterministicChance(world, key || "social-fight-mate-choice", fightWeight / (fightWeight + mateWeight), ...seeds, bucket) ? "fight" : "mate"; +} + function triggerFriendContact(t, other, world, dt = 0) { if (!t || !other || !world) return false; const amount = 0.30 + Math.max(0, Number(dt) || 0) * 0.85; @@ -34,22 +121,14 @@ function updateSocialContactBehavior(t, world, dt, mode = "bond") { const near = dist(t, other) <= Math.max(48, (t.radius || 20) + (other.radius || 20) + 12); if (!near) return moveToOrUse(t, other, mode === "family" ? "follow_parent" : "seek_friend", mode === "family" ? "\u5bb6\u65cf\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b" : "\u4ef2\u9593\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b"); - const conflict = conflictTargetFor(t, world, 30); - const forcedConflict = (t.fightMochiTimer || 0) > 0.04 || (other.fightMochiTimer || 0) > 0.04; - const socialPartsForContact = t.socialReasonParts || {}; - const conflictDominant = Number(socialPartsForContact.conflict || 0) >= Math.max(Number(socialPartsForContact.bond || 0), Number(socialPartsForContact.mate || 0), Number(socialPartsForContact.family || 0)) + 8; - if (forcedConflict || (conflict?.target === other && conflict.score >= 34 && conflictDominant)) { + const weights = socialFightMateWeights(t, other, world, { mode, requireBirthReady: true, distance: dist(t, other) }); + const choice = pickSocialFightMate(world, "social-contact-weighted-fight-mate", weights, t, other, mode); + if (choice === "fight") { + t.conflictTargetId = other.id; + other.conflictTargetId = t.id; return updateFightBehavior(t, world, dt) || true; } - const mate = mateTargetFor(t, world, 150); - const parts = t.socialReasonParts || {}; - const selfBehaviorId = getTarinaiBehaviorId(t); - const otherBehaviorId = getTarinaiBehaviorId(other); - const mateIntent = mode === "mate" || selfBehaviorId === "approach_mate" || otherBehaviorId === "approach_mate" || (t.loveMochiTimer || 0) > 0.04 || (other.loveMochiTimer || 0) > 0.04; - const mateDominant = Number(parts.mate || 0) >= Math.max(Number(parts.bond || 0), Number(parts.conflict || 0)) + 10; - if (mate === other && (t.reproductionTimer || 0) <= 8 && (other.reproductionTimer || 0) <= 10 && (mateIntent || mateDominant)) { - if (world.startBirthRitual?.(t, other)) return "finished"; - } + if (choice === "mate" && world.startBirthRitual?.(t, other)) return "finished"; return triggerFriendContact(t, other, world, dt); } @@ -58,9 +137,17 @@ function updateMateBehavior(t, world, dt) { if (!other) return false; const d = dist(t, other); if (d <= Math.max(50, (t.radius || 20) + (other.radius || 20) + 14)) { - const otherWantsMate = (getTarinaiBehaviorId(other)) === "approach_mate" || (other.loveMochiTimer || 0) > 0.04 || (other.reproductionTimer || 0) <= 4; - const selfForced = isCurrentBehaviorForced(t) || (t.loveMochiTimer || 0) > 0.04; - if ((otherWantsMate || selfForced || (t.reproductionTimer || 0) <= 4) && world.startBirthRitual?.(t, other)) return "finished"; + const forced = isCurrentBehaviorForced(t); + if (!forced) { + const weights = socialFightMateWeights(t, other, world, { mode: "mate", requireBirthReady: true, distance: d }); + const choice = pickSocialFightMate(world, "mate-contact-weighted-fight-mate", weights, t, other); + if (choice === "fight") { + t.conflictTargetId = other.id; + other.conflictTargetId = t.id; + return updateFightBehavior(t, world, dt) || true; + } + if (choice === "mate" && world.startBirthRitual?.(t, other)) return "finished"; + } else if (world.startBirthRitual?.(t, other)) return "finished"; moveToOrUse(t, other, "seek_friend", "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b"); if (typeof applyNeedShock === "function") applyNeedShock(other, { social: dt * 10 }, t); return true; @@ -87,13 +174,17 @@ function updateFightBehavior(t, world, dt) { t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b", sleeping: false }); return true; } - const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 24 : 42); + const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 24 : 14); const current = currentBehaviorTarget(t); const rival = isLiveTarinaiEntity(candidate?.target) ? candidate.target : (isLiveTarinaiEntity(current) ? current : nearestForcedFightTarget(t, world, isCurrentBehaviorForced(t) ? 260 : 220)); if (!isLiveTarinaiEntity(rival)) return false; t.conflictTargetId = rival.id; - if (dist(t, rival) > Math.max(42, (t.radius || 20) + (rival.radius || 20) + 12)) return moveToOrUse(t, rival, "seek_enemy", "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u306b\u5411\u304b\u3063\u3066\u3044\u308b"); + const nearDistance = dist(t, rival); + if (nearDistance > Math.max(42, (t.radius || 20) + (rival.radius || 20) + 12)) return moveToOrUse(t, rival, "seek_enemy", "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u306b\u5411\u304b\u3063\u3066\u3044\u308b"); if (candidate?.forced || isCurrentBehaviorForced(t) || (t.fightMochiTimer || 0) > 0.04 || (rival.fightMochiTimer || 0) > 0.04) return !!world.startForcedFight?.(t, rival); + const weights = socialFightMateWeights(t, rival, world, { mode: "conflict", requireBirthReady: true, distance: nearDistance }); + const choice = pickSocialFightMate(world, "fight-contact-weighted-fight-mate", weights, t, rival); + if (choice === "mate" && world.startBirthRitual?.(t, rival)) return true; world.startConflict?.(t, rival); return true; } diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index a38681f..d2b7a98 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -4,6 +4,22 @@ (function (global) { const Tarinai = global.Tarinai; if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_social_move_life.js"); + function fightHeadbuttDamage(attacker, target, worldRef, salt = "") { + const profile = attacker?.personalityProfile?.() || {}; + const aggression = Number(profile.aggression || 1) || 1; + const fear = Number(profile.fear || 1) || 1; + const stress = Number(attacker?.stress || 0) || 0; + const mood = Number(attacker?.mood || 50) || 50; + const personalityCorrection = (aggression - 1) * 4.0 - (fear - 1) * 1.8; + const stateCorrection = clamp((stress - 50) * 0.035 + (50 - mood) * 0.020, -3.2, 4.2); + const typeCorrection = attacker?.type === "angry" ? 2.0 : (attacker?.type === "teary" ? -1.2 : 0); + const drugCorrection = (attacker?.fightMochiTimer || 0) > 0.04 ? 3.0 : 0; + const jitter = deterministicRange(worldRef, `headbutt-damage-jitter-${salt}`, -1.4, 1.4, attacker, target); + const base = 15; + const raw = Math.max(1, base + personalityCorrection + stateCorrection + typeCorrection + drugCorrection + jitter); + return attacker?.outgoingDamage ? attacker.outgoingDamage(raw) : raw; + } + Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ interactWithOthers(dt) { let closeCount = 0; @@ -133,8 +149,8 @@ o.vy += ny * deterministicRange(this.world, "headbutt-other-y", 34, 64, this, o); 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; + const damageToThis = fightHeadbuttDamage(o, this, this.world, "to-this"); + const damageToOther = fightHeadbuttDamage(this, o, this.world, "to-other"); this.world.emit?.("fight:hit", { attacker: o, target: this, amount: damageToThis, cause: "\u55a7\u5629" }); this.world.emit?.("fight:hit", { attacker: this, target: o, amount: damageToOther, cause: "\u55a7\u5629" }); this.damage(damageToThis, "\u55a7\u5629"); @@ -155,57 +171,29 @@ this.world.maybeDefeatFromFightDamage?.(o, this, Math.max(damageToOther, 2.4)); } this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5); - this.nextHeadbutt = this.world.time + deterministicRange(this.world, "next-headbutt", 0.34, 0.55, this, o); + this.nextHeadbutt = this.world.time + deterministicRange(this.world, "next-headbutt", 0.28, 0.46, this, o); } } - const relA = this.relationTo(o.id); - const relB = o.relationTo(this.id); - const battleDrug = this.hasFightMochiEffect() || o.hasFightMochiEffect(); - const loveDrug = this.hasLoveMochiEffect() || o.hasLoveMochiEffect(); - const aggressiveThis = this.shouldApplyPersonalityBehavior("aggression", 1); - const aggressiveOther = o.shouldApplyPersonalityBehavior?.("aggression", 1); - const calmThis = this.shouldApplyPersonalityBehavior("aggression", -1); - const calmOther = o.shouldApplyPersonalityBehavior?.("aggression", -1); - 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); - const conflictBias = (battleDrug ? 2.9 : 1) * (o.isZunchiSlave ? 2.2 : 1) * (this.isZunchiSlave ? 1.15 : 1); - const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0; - const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false; - const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false; - // regression guard baseline: dt * (battleDrug ? 0.23 : 0.085) - const fightStartChance = dt * (battleDrug ? 0.28 : 0.16) * personalityRisk * fearBrake * friendBrake * conflictBias; - if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && deterministicChance(this.world, "social-fight-start", fightStartChance, this, o)) { - this.conflictTargetId = o.id; - o.conflictTargetId = this.id; - const urge = 34 + (battleDrug ? 34 : 0) + (mixedZunchiSlave ? 10 : 0); - this.conflictUrge = Math.max(this.conflictUrge || 0, urge); - o.conflictUrge = Math.max(o.conflictUrge || 0, urge * 0.82); - if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * urge * (battleDrug ? 0.32 : 0.18), safety: dt * (battleDrug ? 4 : 0.9) }, o); - if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * urge * (battleDrug ? 0.24 : 0.14), safety: dt * (battleDrug ? 4 : 0.9) }, this); - } - - const compatibleBirthStatus = this.isZunchiSlave === o.isZunchiSlave; - if (compatibleBirthStatus && !this.sleepDisease && !o.sleepDisease && !this.fightDisease && !o.fightDisease && d < (loveDrug ? 52 : 42) && this.reproductionTimer <= 0 && o.reproductionTimer <= 0 && this.fightTimer <= 0 && o.fightTimer <= 0) { - const enough = loveDrug ? (this.hunger < 90 && o.hunger < 92 && this.energy > 18 && o.energy > 18) : (this.hunger < 55 && o.hunger < 60 && this.energy > 44 && o.energy > 44); - const lonelyBond = loveDrug ? (this.loneliness > 4 || o.loneliness > 4 || this.affection > 0 || o.affection > 0) : (this.loneliness > 24 || o.loneliness > 24); - if (enough && lonelyBond) { + const weights = socialFightMateWeights(this, o, this.world, { mode: "contact", requireBirthReady: true, distance: d, fightRange: 58 }); + const combinedStartChance = dt * 0.30 * clamp((weights.totalWeight || 0) / 10, 0, 2.6); + if ((weights.totalWeight || 0) > 0 && deterministicChance(this.world, "social-weighted-fight-mate-start", combinedStartChance, this, o)) { + const choice = pickSocialFightMate(this.world, "social-weighted-fight-mate-pick", weights, this, o); + if (choice === "fight") { + this.conflictTargetId = o.id; + o.conflictTargetId = this.id; + const urge = 26 + Math.min(64, weights.fightWeight * 4.5) + (mixedZunchiSlave ? 10 : 0); + this.conflictUrge = Math.max(this.conflictUrge || 0, urge); + o.conflictUrge = Math.max(o.conflictUrge || 0, urge * 0.82); + if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * urge * 0.18, safety: dt * 1.0 }, o); + if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * urge * 0.14, safety: dt * 1.0 }, this); + this.world.startConflict?.(this, o); + } else if (choice === "mate") { if (typeof applyNeedShock === "function") { - applyNeedShock(this, { social: dt * (loveDrug ? 34 : 14) }, o); - applyNeedShock(o, { social: dt * (loveDrug ? 34 : 14) }, this); - } - if (loveDrug && typeof queueForcedTarinaiBehavior === "function") { - queueForcedTarinaiBehavior(this, "approach_mate", { target: o, source: "love_mochi", priority: 150, ttl: 10, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" }); - queueForcedTarinaiBehavior(o, "approach_mate", { target: this, source: "love_mochi", priority: 150, ttl: 10, causeText: "\u3078\u3053\u9905\u306e\u52b9\u679c" }); - } - const selfBehaviorId = getTarinaiBehaviorId(this); - const otherBehaviorId = getTarinaiBehaviorId(o); - const mateIntent = selfBehaviorId === "approach_mate" || otherBehaviorId === "approach_mate" || loveDrug; - if (mateIntent && deterministicChance(this.world, "birth-ritual-start", dt * (loveDrug ? 0.78 : 0.30), this, o)) { - this.world.startBirthRitual(this, o); + applyNeedShock(this, { social: dt * 18 }, o); + applyNeedShock(o, { social: dt * 18 }, this); } + this.world.startBirthRitual?.(this, o); } } } @@ -535,7 +523,7 @@ temperatureDeathReason() { - const status = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || null; + const status = this.temperatureStatus || this.world?.temperatureStatusFor?.(this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15), this) || null; const recent = (this.world?.time || 0) - Number(this.lastTemperatureDamageAt || -999) <= 36; if (recent && status && (status.harmExcess || 0) > 0) return status.direction === "cold" ? "凍結" : "熱中症"; return ""; diff --git a/js/tarinai_sunbath_system.js b/js/tarinai_sunbath_system.js index c0e87ae..8aefdaa 100644 --- a/js/tarinai_sunbath_system.js +++ b/js/tarinai_sunbath_system.js @@ -23,7 +23,8 @@ this.sunbathFrameTimer = 0.5; } this.energy = clamp((this.energy || 0) + dt * 0.03, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100)); - if (!this.isSunbathTime() || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0; + const felt = this.world?.feltTemperatureFor?.(this) ?? this.world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15); + if (!this.isSunbathTime() || Number(felt) >= 22 || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0; if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") { this.finishSunbath?.(); this._activeSunbathSpriteId = ""; diff --git a/js/tarinai_update_step_frame.js b/js/tarinai_update_step_frame.js index bae2e85..1cd2c76 100644 --- a/js/tarinai_update_step_frame.js +++ b/js/tarinai_update_step_frame.js @@ -71,8 +71,10 @@ } global.TarinaiCursorCareSystem.updateOne(t, dt); const wasFighting = t.fightTimer > 0.04; + const endingFightTargetIds = wasFighting ? [...new Set([t.fightTargetId, ...(Array.isArray(t.fightTargetIds) ? t.fightTargetIds : [])].filter(Boolean))] : []; t.fightTimer = Math.max(0, t.fightTimer - dt); t.fightCooldown = Math.max(0, t.fightCooldown - dt); + t.postConflictPeaceTimer = Math.max(0, (t.postConflictPeaceTimer || 0) - dt); t.groundStumbleCooldown = Math.max(0, (t.groundStumbleCooldown || 0) - dt); t.defeatedTimer = Math.max(0, t.defeatedTimer - dt); if (wasFighting && t.fightTimer <= 0.04 && t.defeatedById) { @@ -88,6 +90,19 @@ t.fightTargetIds = []; t.fightTargetId = null; } + if (wasFighting && t.fightTimer <= 0.04) { + const rivals = endingFightTargetIds.map(id => t.world?.liveTarinaiById?.(id)).filter(Boolean); + const cooldown = CONFIG.fightCooldown + detRange(t.world, "fight-ended-cooldown", 8, 14, t, endingFightTargetIds.join("/")); + t.fightCooldown = Math.max(t.fightCooldown || 0, cooldown); + t.postConflictPeaceTimer = Math.max(t.postConflictPeaceTimer || 0, detRange(t.world, "fight-ended-peace", 5.5, 9.0, t, endingFightTargetIds.join("/"))); + for (const rival of rivals) t.world?.markFightPairCooldown?.(t, rival, cooldown + 2); + if (!t.defeatedById) { + t.fightTargetIds = []; + t.fightTargetId = null; + t.counterAttackFromId = null; + if (t.state === "fight" || t.state === "seek_enemy") t.setActionState?.("idle", { target: null, reason: "喧嘩が終わって少し距離を置いている。", sleeping: false, clearTarget: true }); + } + } t.grassEatTimer = Math.max(0, t.grassEatTimer - dt); t.hurtTimer = Math.max(0, t.hurtTimer - dt); t.fallTimer = Math.max(0, t.fallTimer - dt); @@ -121,13 +136,13 @@ t.tempTimer += dt; if (t.tempTimer >= 0.55) { - const felt = t.world?.temperatureAt?.(t.x, t.y) ?? (CONFIG.standardTemperature ?? 15); + const felt = t.world?.feltTemperatureFor?.(t) ?? t.world?.temperatureAt?.(t.x, t.y, t) ?? (CONFIG.standardTemperature ?? 15); t.tempComfort = felt; t.feltTemperature = felt; - t.temperatureStatus = t.world?.temperatureStatusFor?.(felt) || null; + t.temperatureStatus = t.world?.temperatureStatusFor?.(felt, t) || null; t.tempTimer = 0; } - const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.tempComfort ?? (CONFIG.standardTemperature ?? 15)) || { discomfort: 0, stressExcess: 0, harmExcess: 0, direction: "comfort" }; + const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.tempComfort ?? (CONFIG.standardTemperature ?? 15), t) || { discomfort: 0, stressExcess: 0, harmExcess: 0, direction: "comfort" }; const coldDiscomfort = tempStatus.direction === "cold" ? Math.max(0, tempStatus.discomfort || 0) : 0; const hungerScale = t.metabolicHungerMultiplier ? t.metabolicHungerMultiplier() : (typeof tarinaiMetabolicHungerScale === "function" ? tarinaiMetabolicHungerScale(t) : 1); t.hunger += CONFIG.hungerPerMinute * minute * t.trait.hunger * hungerScale * (1 + coldDiscomfort * 0.018); @@ -151,7 +166,6 @@ if ((tempStatus.harmExcess || 0) > 0) { const harm = tempStatus.harmExcess || 0; t.energy -= dt * (0.10 + harm * 0.075 + harm * harm * 0.006); - t.hurtTimer = Math.max(t.hurtTimer || 0, Math.min(1.2, 0.28 + harm * 0.035)); t.lastDamageCause = tempStatus.direction === "cold" ? "凍結" : "熱中症"; t.lastTemperatureDamageAt = t.world?.time || 0; } diff --git a/js/text_catalog.js b/js/text_catalog.js index 82cf78d..7d9b47d 100644 --- a/js/text_catalog.js +++ b/js/text_catalog.js @@ -141,7 +141,7 @@ TEXT_CATALOG.stateLabel = function stateLabelFromCatalog(s, t = null) { TEXT_CATALOG.temperatureEndureText = function temperatureEndureText(t = null) { if (!t || !t.world) return ""; - const status = t.temperatureStatus || t.world.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? t.world.temperatureAt?.(t.x, t.y) ?? (CONFIG.standardTemperature ?? 15)); + const status = t.temperatureStatus || t.world.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? t.world.temperatureAt?.(t.x, t.y, t) ?? (CONFIG.standardTemperature ?? 15), t); if (!status || status.comfortable || (status.discomfort || 0) <= 0.5) return ""; if (t.world.hasReachableComfortTemperatureSpot?.(t)) return ""; return status.direction === "cold" ? "寒さに耐えている。" : "暑さに耐えている。"; diff --git a/js/ui_selected.js b/js/ui_selected.js index fa5c5e1..79b2e03 100644 --- a/js/ui_selected.js +++ b/js/ui_selected.js @@ -78,6 +78,15 @@ function geneticSizeText(t) { return `${geneticPercentText(t, "size")} / ${now.toFixed(2)}\u2192${adult.toFixed(2)} (${growth}%)`; } +function geneticComfortTemperatureText(t) { + const raw = t?.genetics?.temperatureOffset; + const offset = Number.isFinite(Number(raw)) ? Number(raw) : 0; + const min = (CONFIG.temperatureComfortMin ?? 10) + offset; + const max = (CONFIG.temperatureComfortMax ?? 25) + offset; + const sign = offset >= 0 ? "+" : ""; + return `${min.toFixed(1)}〜${max.toFixed(1)}℃ (${sign}${offset.toFixed(1)}℃)`; +} + function personalityDailyChangeHtml(t) { const state = typeof personalityDailyState === "function" ? personalityDailyState(t) : (t?.personalityDaily || { day: world?.day || 1, byKey: {}, byCause: {} }); const dayLabel = `D${state.day || world?.day || 1}`; @@ -217,7 +226,7 @@ function renderSelected() { t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0), t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0, personalitySnapshot, personalityDailySnapshot(t), - ["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), fmt((t.growth || 0) * 100), + ["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), geneticComfortTemperatureText(t), fmt((t.growth || 0) * 100), itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","), selectedInventoryItems(t).join(","), (t.records || []).slice(0, 7).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~") @@ -258,6 +267,7 @@ function renderSelected() {
\u653b\u6483\u529b${geneticPercentText(t, "attack")}
\u79fb\u52d5\u901f\u5ea6${geneticPercentText(t, "speed")}
\u30b5\u30a4\u30ba${geneticSizeText(t)}
+
快適温度${geneticComfortTemperatureText(t)}
`; const needRowsHtml = needKeys.map(key => { const label = (typeof TARINAI_NEED_LABELS !== "undefined" && TARINAI_NEED_LABELS[key]) || key; @@ -270,7 +280,7 @@ function renderSelected() { `
${needRowsHtml}
`, selectedInfoRowHtml("\u4f53\u529b", `${fmt(t.energy)} / ${fmt(typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100))}`, "energy"), selectedInfoRowHtml("\u30b9\u30c8\u30ec\u30b9", fmt(t.stress), "stress"), - selectedInfoRowHtml("体感気温", `${(world.temperatureAt?.(t.x, t.y) ?? t.feltTemperature ?? CONFIG.standardTemperature ?? 15).toFixed(1)}℃`, "temperature"), + selectedInfoRowHtml("体感気温", `${(world.feltTemperatureFor?.(t) ?? world.temperatureAt?.(t.x, t.y, t) ?? t.feltTemperature ?? CONFIG.standardTemperature ?? 15).toFixed(1)}℃`, "temperature"), selectedInfoRowHtml("\u6e80\u8179\u5ea6", fmt(Math.max(0, Math.min(100, 100 - (Number(t.hunger) || 0)))), "fullness"), ].join(""); const itemEffectHtml = itemEffectSummary.length diff --git a/js/version.js b/js/version.js index a23252b..9573db0 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.15.84"; - const APP_BUILD = "tarinai-cleanup-unused-cross-domain-v39-15-84"; + const APP_VERSION = "39.15.90"; + const APP_BUILD = "tarinai-fight-cooldown-damage-distributed-v39-15-90"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world_environment.js b/js/world_environment.js index 69d5f00..3f0d3fe 100644 --- a/js/world_environment.js +++ b/js/world_environment.js @@ -360,7 +360,7 @@ return this.manualTemperature; }, - temperatureAt(x, y) { + temperatureAt(x, y, actor = null) { let temp = Number.isFinite(Number(this.currentTemperature)) ? Number(this.currentTemperature) : this.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15); if (this.temperatureAuto === false) temp = this.temperatureNumber(this.manualTemperature); if ((this.groundType || "soil") === "ice") temp -= 5; @@ -384,13 +384,26 @@ temp += clamp(toward, -nestWarmthLimit, nestWarmthLimit); break; } + if (actor && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3; return this.temperatureNumber(temp); }, - temperatureStatusFor(temp = CONFIG.standardTemperature ?? 15) { + feltTemperatureFor(actor, x = actor?.x, y = actor?.y) { + if (!actor) return this.temperatureAt(x, y); + return this.temperatureAt(x, y, actor); + }, + + temperatureComfortOffsetFor(actor = null) { + const raw = actor?.genetics?.temperatureOffset; + const n = Number(raw); + return Number.isFinite(n) ? n : 0; + }, + + temperatureStatusFor(temp = CONFIG.standardTemperature ?? 15, actor = null) { const value = this.temperatureNumber(temp); - const min = CONFIG.temperatureComfortMin ?? 10; - const max = CONFIG.temperatureComfortMax ?? 25; + const offset = this.temperatureComfortOffsetFor(actor); + const min = (CONFIG.temperatureComfortMin ?? 10) + offset; + const max = (CONFIG.temperatureComfortMax ?? 25) + offset; const discomfort = value < min ? min - value : (value > max ? value - max : 0); const direction = value < min ? "cold" : (value > max ? "hot" : "comfort"); const stressExcess = Math.max(0, discomfort - (CONFIG.temperatureStressMargin ?? 5)); @@ -409,10 +422,11 @@ findComfortTemperatureSpot(actor, opts = {}) { if (!actor || actor.dead) return null; - const current = this.temperatureAt(actor.x, actor.y); - const currentStatus = this.temperatureStatusFor(current); + const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt(actor.x, actor.y, actor); + const currentStatus = this.temperatureStatusFor(current, actor); if (!opts.force && currentStatus.comfortable) return null; - const targetTemp = ((CONFIG.temperatureComfortMin ?? 10) + (CONFIG.temperatureComfortMax ?? 25)) * 0.5; + const comfortOffset = this.temperatureComfortOffsetFor(actor); + const targetTemp = ((CONFIG.temperatureComfortMin ?? 10) + (CONFIG.temperatureComfortMax ?? 25)) * 0.5 + comfortOffset; const radius = actor.radius || actor.r || 20; const rings = [72, 128, 196, 288, 392]; const dirs = 18; @@ -442,8 +456,8 @@ let bestScore = Infinity; for (const c of candidates) { if (this.pointBlockedByObstacle?.(c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor })) continue; - const temp = this.temperatureAt(c.x, c.y); - const status = this.temperatureStatusFor(temp); + const temp = this.temperatureAt(c.x, c.y, actor); + const status = this.temperatureStatusFor(temp, actor); if (currentStatus.direction === "cold" && temp <= current + 0.6 && !status.comfortable) continue; if (currentStatus.direction === "hot" && temp >= current - 0.6 && !status.comfortable) continue; const d = distXY(actor.x, actor.y, c.x, c.y); @@ -455,7 +469,7 @@ } } if (!best) return null; - const bestStatus = this.temperatureStatusFor(best.temperature); + const bestStatus = this.temperatureStatusFor(best.temperature, actor); if (opts.requireComfort && !bestStatus.comfortable) return null; if (!bestStatus.comfortable && bestStatus.discomfort >= currentStatus.discomfort - 0.65) return null; best.temperatureStatus = bestStatus; @@ -469,12 +483,12 @@ sleepTargetTemperatureBlocked(actor, item) { if (!actor || !item || item.dead) return false; if (!(item.type === "grass_bed" || item.type === "bed" || item.type === "nest_box")) return false; - const current = this.temperatureAt?.(actor.x, actor.y); - const currentStatus = this.temperatureStatusFor?.(current); + const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt?.(actor.x, actor.y, actor); + const currentStatus = this.temperatureStatusFor?.(current, actor); if (!currentStatus || currentStatus.comfortable) return false; if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false; - const temp = this.temperatureAt?.(item.x, item.y); - const status = this.temperatureStatusFor?.(temp); + const temp = this.temperatureAt?.(item.x, item.y, actor); + const status = this.temperatureStatusFor?.(temp, actor); return Boolean(status && !status.comfortable); }, diff --git a/js/world_family_social.js b/js/world_family_social.js index 0e9128c..bb12515 100644 --- a/js/world_family_social.js +++ b/js/world_family_social.js @@ -351,7 +351,7 @@ if (id === "athletic") return "ice"; if (id === "war") return "foot_massage"; if (id === "happy") return "blanket"; - return ""; + return "soil"; } global.TarinaiResetPresets = Object.freeze({ ids: Object.keys(RESET_PRESETS), meta: RESET_PRESETS, normalize: normalizeResetPresetId }); @@ -525,7 +525,12 @@ if (Math.abs((Number(entry.scale) || 0) - nextScale) > 0.002) changed = true; if (Math.abs((Number(entry.adultScale) || 0) - nextAdultScale) > 0.002) changed = true; if (Math.abs((Number(entry.growth) || 0) - nextGrowth) > 0.02) changed = true; - for (const key of ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"]) if (Math.abs((Number(entry.genetics?.[key]) || 1) - (Number(nextGenetics?.[key]) || 1)) > 0.002) changed = true; + for (const key of ["lifeSpanMul", "attackMul", "speedMul", "sizeMul", "temperatureOffset"]) { + const fallback = key === "temperatureOffset" ? 0 : 1; + const prevGene = Number.isFinite(Number(entry.genetics?.[key])) ? Number(entry.genetics?.[key]) : fallback; + const nextGene = Number.isFinite(Number(nextGenetics?.[key])) ? Number(nextGenetics?.[key]) : fallback; + if (Math.abs(prevGene - nextGene) > 0.002) changed = true; + } if (Math.abs((Number(entry.lifeSpan) || 0) - (Number(t.lifeSpan) || 0)) > 1) changed = true; if ((Number(entry.totalFightWins) || 0) !== nextFightWins) changed = true; if ((Number(entry.totalFightLosses) || 0) !== nextFightLosses) changed = true; @@ -618,7 +623,12 @@ const b = next[key]; let equal = false; if (key === "genetics") { - equal = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"].every(gk => Math.abs((Number(a?.[gk]) || 1) - (Number(b?.[gk]) || 1)) <= 0.002); + equal = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul", "temperatureOffset"].every(gk => { + const fallback = gk === "temperatureOffset" ? 0 : 1; + const av = Number.isFinite(Number(a?.[gk])) ? Number(a?.[gk]) : fallback; + const bv = Number.isFinite(Number(b?.[gk])) ? Number(b?.[gk]) : fallback; + return Math.abs(av - bv) <= 0.002; + }); } else { equal = Array.isArray(a) && Array.isArray(b) ? sameList(a, b) : a === b; } @@ -1179,6 +1189,7 @@ if ((a.fightMochiTimer || 0) > 0.04 || (b.fightMochiTimer || 0) > 0.04) return this.startForcedFight(a, b); if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return false; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false; + if ((a.postConflictPeaceTimer || 0) > 0.04 || (b.postConflictPeaceTimer || 0) > 0.04) return false; if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false; if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return false; const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0; @@ -1266,6 +1277,7 @@ if (!a || !b || a.dead || b.dead) return false; if (!this.canFightPair(a, b) && !this.canFightPair(b, a)) return false; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false; + if ((a.postConflictPeaceTimer || 0) > 0.04 || (b.postConflictPeaceTimer || 0) > 0.04) return false; if (this.isAlreadyFightingPair?.(a, b)) return true; const allowForcedStart = Boolean(opts?.forced); if (!allowForcedStart && ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04 || (a.intimidateTimer || 0) > 0.04 || (b.intimidateTimer || 0) > 0.04)) return false; @@ -1305,14 +1317,8 @@ b.fightTargetId = b.fightTargetIds[0]; if (a.addStress) a.addStress(deterministicRange(this, "fight-start-stress-a", 8, 18, a, b), { threshold: 8 }); if (b.addStress) b.addStress(deterministicRange(this, "fight-start-stress-b", 8, 18, a, b), { threshold: 8 }); - const rawDamageToA = deterministicRange(this, "fight-pulse-damage-a", 2, 7, a, b); - const rawDamageToB = deterministicRange(this, "fight-pulse-damage-b", 2, 7, a, b); - const damageToA = b.outgoingDamage ? b.outgoingDamage(rawDamageToA) : rawDamageToA; - const damageToB = a.outgoingDamage ? a.outgoingDamage(rawDamageToB) : rawDamageToB; - a.damage(damageToA, "\u55a7\u5629"); - b.damage(damageToB, "\u55a7\u5629"); - this.maybeDefeatFromFightDamage?.(a, b, damageToA); - this.maybeDefeatFromFightDamage?.(b, a, damageToB); + // Damage is applied by repeated headbutt pulses while the fight is active. + // Do not front-load or end-load a lump sum when the fight state starts/settles. const dx = b.x - a.x, dy = b.y - a.y; const d = Math.hypot(dx, dy) || 1; a.vx -= dx / d * deterministicRange(this, "fight-pulse-knockback-ax", 32, 62, a, b); a.vy -= dy / d * deterministicRange(this, "fight-pulse-knockback-ay", 32, 62, a, b); diff --git a/js/world_placement_log.js b/js/world_placement_log.js index d016e12..61b3ba3 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -1338,7 +1338,7 @@ let foundItem = null, foundItemD = Infinity; for (let i = this.items.length - 1; i >= 0; i--) { const it = this.items[i]; - if (!it || it.dead || it.type === "trace" || it.type === "splat") continue; + if (!it || it.dead || it.type === "trace" || it.type === "splat" || it.type === "plushie") continue; let hit = false; let d = distXY(x, y, it.x, it.y); if (global.TarinaiMechanicalSystem.isMechanicalType(it.type)) { @@ -1352,7 +1352,6 @@ } if (hit && d < foundItemD) { foundItem = it; foundItemD = d; } } - if (foundItem?.type === "plushie") return foundItem; if (foundItem && isPinType(foundItem.type) && foundItem.pinState === "ball_lodged") { const carrier = (this.items || []).find(it => it && !it.dead && it.type === "ball" && it.id === foundItem.pinBallId); if (carrier) return carrier; @@ -1458,12 +1457,12 @@ for (let i = this.items.length - 1; i >= 0; i--) { const it = this.items[i]; if (!it || it.dead || it.type !== "plushie") continue; - const hit = Math.max(34, (it.r || 7) * 4.8); + const hit = Math.max(58, (it.r || 7) * 7.8); const d = distXY(x, y, it.x, it.y); if (d <= hit && d < plushieTargetD) { plushieTarget = it; plushieTargetD = d; } } if (plushieTarget && typeof plushieTarget.fling === "function") { - if (plushieTarget.fling(this, x, y, { source: "poke", speed: 720, life: 2.8 })) { + if (plushieTarget.fling(this, x, y, { source: "poke", speed: 860, life: 3.0 })) { this.effects?.push(new Effect("ring", x, y, { size: 24, life: 0.18, color: "rgba(190,142,92,0.34)" })); this.drawListDirty = true; audio.poke?.(); @@ -1482,7 +1481,7 @@ } if (ballTarget && this.pokeBall(ballTarget, x, y)) return; - const target = [...this.tarinai].reverse().find(t => t.contains(x, y)); + const target = [...this.tarinai].reverse().find(t => !this.isTarinaiHiddenInNestBox?.(t) && t.contains(x, y)); if (target) { target.poke(); if (!target.dead) this.log(`${target.name}\u306f\u3064\u3064\u304b\u308c\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "observe", { participants: [target] }); diff --git a/scripts/regression_check.py b/scripts/regression_check.py index 1fa806f..be901ec 100644 --- a/scripts/regression_check.py +++ b/scripts/regression_check.py @@ -330,9 +330,10 @@ def check_social_balance_guards() -> None: "intimidate_enemy: 0", "approach_mate: 72", "approach_friend: 66", - "return ({ conflict: 26, mate: 24, family: 12, bond: 14", - "if (subNeed !== \"conflict\" && subValue >= threshold) return true", - "subValue >= threshold * 0.82 && subValue >= socialPull * 0.68", + "return ({ conflict: 18, mate: 24, family: 12, bond: 14", + "const relationDrive = Math.max(Number(parts.mate || 0)", + "drugBoosted || relationDrive >= Math.min", + "social-fight-vs-mate-weighted", ] for token in required_need_tokens: if token not in needs: @@ -350,8 +351,8 @@ def check_social_balance_guards() -> None: for token in required_world_tokens: if token not in world_social: fail(f"fight re-entry guard missing: {token}") - if "dt * (battleDrug ? 0.23 : 0.085)" not in social_move: - fail("ordinary proximity fight probability guard missing") + if "social-weighted-fight-mate-start" not in social_move or "pickSocialFightMate" not in social_move: + fail("ordinary proximity weighted fight/mate guard missing") ok("social balance and fight re-entry guards present") diff --git a/service-worker.js b/service-worker.js index aa06822..a39af9b 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,6 +1,6 @@ "use strict"; -const APP_VERSION = "39.15.84"; +const APP_VERSION = "39.15.90"; 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.