From d14550dad6aa42bbf9efab6d526129c619cf45ac Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Sat, 11 Jul 2026 21:13:39 +0900 Subject: [PATCH 1/6] rr --- .tmp-edge-profile2/Crashpad/settings.dat | Bin 288 -> 0 bytes .../Crashpad/throttle_store.dat | 1 - AI_MAP.md | 15 +- BUILD_NOTE.txt | 21 +- FILES.json | 2 +- GAME_FEATURES.md | 44 +- README.md | 7 +- app_manifest.json | 3 +- css/base.css | 97 ++ css/panel.css | 24 +- index.html | 356 +++--- js/collision_footprint_system.js | 2 + js/collision_response_system.js | 36 +- js/constraint_system.js | 16 +- js/fire_runtime_system.js | 6 +- js/ground_types.js | 2 +- js/health.js | 7 +- js/item_dynamic_ball_system.js | 21 +- js/item_dynamic_tool_system.js | 6 +- js/item_effect_definitions.js | 43 +- js/item_food_definitions.js | 4 +- js/item_lifecycle_step_frame.js | 1 + js/item_lifecycle_support.js | 2 +- js/item_render_runtime.js | 98 +- js/item_tool_definitions.js | 26 +- js/item_tool_metadata.js | 13 +- js/item_type_initializers.js | 43 + js/item_update_policy.js | 4 +- js/item_visual_definitions.js | 18 + js/items.js | 32 +- js/placement_preview_system.js | 11 +- js/render.js | 50 +- js/robot_cleaner_system.js | 6 +- js/save_system.js | 16 +- js/signal_system.js | 60 +- js/simulation_ambient_system.js | 9 +- js/simulation_maintenance_system.js | 1 + js/snapshot_system.js | 27 +- js/structure_lifecycle.js | 158 ++- js/structures.js | 2 +- js/tarinai.js | 1 + js/tarinai_action_definitions.js | 2 +- js/tarinai_behavior_text.js | 2 +- js/tarinai_consumable_behavior.js | 17 +- js/tarinai_direct_feeding_system.js | 162 +++ js/tarinai_disease_nest.js | 13 +- js/tarinai_food_interaction_system.js | 14 +- js/tarinai_food_prototype_mixin.js | 2 +- js/tarinai_item_effects.js | 7 +- js/tarinai_item_targeting.js | 8 +- js/tarinai_local_environment_system.js | 60 + js/tarinai_needs_core.js | 2 +- js/tarinai_needs_items.js | 42 +- js/tarinai_social_action_runtime.js | 1 + js/tarinai_social_move_life.js | 26 +- js/tarinai_sunbath_system.js | 4 +- js/tarinai_update_step_frame.js | 15 +- js/tarinai_update_step_movement.js | 2 - js/ui_bind.js | 10 +- js/ui_charts.js | 58 +- js/ui_input_mouse.js | 1 + js/ui_input_shared.js | 96 +- js/ui_layout_dialogs.js | 2 +- js/ui_log.js | 2 +- js/ui_selected.js | 2 +- js/ui_tools.js | 88 +- js/version.js | 4 +- js/weather_system.js | 1 + js/world.js | 2 + js/world_combat_effects.js | 37 +- js/world_environment.js | 12 +- js/world_family_social.js | 24 +- js/world_placement_log.js | 187 ++- js/world_reset_presets.js | 100 +- js/world_temperature_system.js | 48 +- js/world_tool_actions.js | 19 +- scripts/regression_check.py | 1023 +++++++++++++++++ service-worker.js | 4 +- 78 files changed, 2827 insertions(+), 563 deletions(-) delete mode 100644 .tmp-edge-profile2/Crashpad/settings.dat delete mode 100644 .tmp-edge-profile2/Crashpad/throttle_store.dat create mode 100644 js/tarinai_direct_feeding_system.js diff --git a/.tmp-edge-profile2/Crashpad/settings.dat b/.tmp-edge-profile2/Crashpad/settings.dat deleted file mode 100644 index 3b20fded8081a26d1f1bcfc18d3914abdd4f64f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 288 ocmXR;32 たりない観察 - - - - - - - - + + + + + + + +
@@ -107,15 +107,15 @@
気温10℃
-
-
上限設定0は上限なし
+
+
上限設定0は上限なし
- - + +
-
たりない 0 / ∞ オブジェクト 0 / ∞
+
たりない 0 / ∞ オブジェクト 0 / ∞
-
+
@@ -181,6 +181,7 @@ +
@@ -261,168 +262,169 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/collision_footprint_system.js b/js/collision_footprint_system.js index c9503a5..3c87417 100644 --- a/js/collision_footprint_system.js +++ b/js/collision_footprint_system.js @@ -100,6 +100,8 @@ for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) { if (!other || other === item || other.dead) continue; if (isSoftPlacementIgnoredType(other.type)) continue; + const freeStacking = global.TarinaiItemToolMetadata?.ITEM_TRAITS?.freeStackingMovable; + if (freeStacking?.has(item.type) && freeStacking.has(other.type)) continue; if (!important.has(other.type)) continue; const otherRects = placementRectsFor(worldRef, other); const otherRadius = itemFootprintRadius(worldRef, other, otherRects); diff --git a/js/collision_response_system.js b/js/collision_response_system.js index 69a8356..9aaf40f 100644 --- a/js/collision_response_system.js +++ b/js/collision_response_system.js @@ -17,7 +17,7 @@ } return (worldRef.items || []).filter(it => it && !it.dead && isBallLikeType(it.type)); } - function ballLikeLabel(item) { return item?.type === "balloon" ? "水風船" : "ボール"; } + function ballLikeLabel(item) { return item?.type === "balloon" ? "\u6c34\u98a8\u8239" : "\u30dc\u30fc\u30eb"; } function resolveBallBallCollisions(worldRef, dt) { @@ -130,7 +130,8 @@ const sy = ball.y - py; const segLenSq = sx * sx + sy * sy; for (const t of worldRef.nearbyTarinai(ball.x, ball.y, range)) { - if (!t || t.dead || t.state === "sleep" || worldRef.isTarinaiHiddenInNestBox(t)) continue; + if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue; + const sleepingContact = t.state === "sleep" || t.sleeping; let hitX = ball.x; let hitY = ball.y; if (segLenSq > 1) { @@ -235,6 +236,35 @@ ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38); continue; } + + if (sleepingContact) { + const toward = (ball.vx || 0) * nx + (ball.vy || 0) * ny; + const bodyImpulse = clamp(10 + Math.max(0, incomingSpeed) * 0.34 + overlapPush * 1.8, 10, 96); + t.vx = clamp((t.vx || 0) - nx * bodyImpulse, -220, 220); + t.vy = clamp((t.vy || 0) - ny * bodyImpulse, -220, 220); + if (toward < 0) { + const restitution = isBalloon ? 0.34 : 0.56; + ball.vx = (ball.vx || 0) - (1 + restitution) * toward * nx; + ball.vy = (ball.vy || 0) - (1 + restitution) * toward * ny; + } else { + ball.vx = (ball.vx || 0) + nx * Math.max(8, bodyImpulse * 0.28); + ball.vy = (ball.vy || 0) + ny * Math.max(8, bodyImpulse * 0.28); + } + const separation = Math.max(0, overlapPush + 1.5); + if (separation > 0) { + ball.x = clamp((ball.x || 0) + nx * separation * 0.72, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); + ball.y = clamp((ball.y || 0) + ny * separation * 0.72, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); + t.x = clamp((t.x || 0) - nx * separation * 0.28, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding); + t.y = clamp((t.y || 0) - ny * separation * 0.28, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding); + } + ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 4.2, -38, 38); + ball.lastKickedAt = now; + ball.lastKickerId = t.id; + global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(t, frameDt, "ball-contact"); + worldRef.markSpatialDirty?.("sleeping-ball-contact"); + worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(12, ball.r * 0.82), life: 0.18, color: isBalloon ? "rgba(248,122,166,0.38)" : "rgba(180,205,120,0.44)" })); + continue; + } const relVx = (t.vx || 0) - (ball.vx || 0); const relVy = (t.vy || 0) - (ball.vy || 0); @@ -270,7 +300,7 @@ t.nextPlayBubbleAt = now + deterministicRange(worldRef, "ball-play-bubble-delay", 2.0, 3.8, ball, t); worldRef.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)"); } - if (playful && worldRef.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) worldRef.log(`${t.name}は${objectLabel}を追いかけて弾いた。`, "observe", { participants: [t] }); + if (playful && worldRef.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) worldRef.log(`${t.name}\u306f${objectLabel}\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] }); worldRef.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: isBalloon ? "rgba(248,122,166,0.42)" : "rgba(170,210,95,0.50)" })); } } diff --git a/js/constraint_system.js b/js/constraint_system.js index d75a097..51669ed 100644 --- a/js/constraint_system.js +++ b/js/constraint_system.js @@ -783,14 +783,10 @@ return { a, b, x: Number(item.x || 0) || 0, y: Number(item.y || 0) || 0, r: Number(item.r || item.radius || 0) || 0 }; } - function finishLinkSpatialUpdate(item, worldRef, before, reason, threshold, minInterval) { - if (!worldRef || !before) return false; + function finishLinkSpatialUpdate(item, worldRef, reason, threshold, minInterval) { + if (!worldRef || !item) return false; worldRef.drawListDirty = true; - const movedForSpatial = Math.hypot((Number(item.x || 0) || 0) - before.x, (Number(item.y || 0) || 0) - before.y) > threshold - || Math.abs((Number(item.r || item.radius || 0) || 0) - before.r) > threshold; - if (movedForSpatial) markConstraintSpatialDirty(worldRef, item, reason, threshold, minInterval); - else if (worldRef.constraintDirtyStatsThisFrame) worldRef.constraintDirtyStatsThisFrame.coalesced = (worldRef.constraintDirtyStatsThisFrame.coalesced || 0) + 1; - return movedForSpatial; + return markConstraintSpatialDirty(worldRef, item, reason, threshold, minInterval); } function updateFlexibleLink(item, dt, worldRef) { @@ -809,7 +805,7 @@ } length = primeLinkState(item, aa, bb); changed = solveRopeParticleChain(item, aa, bb, dt, length, worldRef) || changed; - if (changed) finishLinkSpatialUpdate(item, worldRef, before, "rope-tension", 0.38, 0.085); + if (changed) finishLinkSpatialUpdate(item, worldRef, "rope-tension", 0.38, 0.085); return changed; } @@ -842,7 +838,7 @@ ep(item, 0).x = fa.x; ep(item, 0).y = fa.y; ep(item, 1).x = fb.x; ep(item, 1).y = fb.y; } - if (changed) finishLinkSpatialUpdate(item, worldRef, before, "rod-constraint", 0.30, 0.070); + if (changed) finishLinkSpatialUpdate(item, worldRef, "rod-constraint", 0.30, 0.070); return changed; } @@ -955,7 +951,7 @@ const residual = Math.abs(distXY(fa.x, fa.y, fb.x, fb.y) - rest); if (changed || residual > 0.8) lset(item, "awakeUntil", Number(worldRef?.time || 0) + 0.36); } - if (changed) finishLinkSpatialUpdate(item, worldRef, before, "spring-constraint", 0.22, 0.035); + if (changed) finishLinkSpatialUpdate(item, worldRef, "spring-constraint", 0.22, 0.035); return changed; } diff --git a/js/fire_runtime_system.js b/js/fire_runtime_system.js index c8af4bd..18f1491 100644 --- a/js/fire_runtime_system.js +++ b/js/fire_runtime_system.js @@ -151,7 +151,7 @@ function updateFirecracker(item, dt, worldRef) { box.nestFireEvacuated = false; box.flameWaterCooldown = 0; box.nestFireEffectCooldown = 0; - worldRef?.log?.("巣箱に火が燃え移った。", "danger"); + worldRef?.log?.("\u5de3\u7bb1\u306b\u706b\u304c\u71c3\u3048\u79fb\u3063\u305f\u3002", "danger"); worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.35, life: 0.30, color: "rgba(255,122,48,0.56)" })); } worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire"); @@ -198,7 +198,7 @@ function updateFirecracker(item, dt, worldRef) { count += 1; } if (count) { - worldRef.log?.(`炎上した巣箱から${count}匹のたりないが追い出された。`, "danger", { participants: occupants }); + worldRef.log?.(`\u708e\u4e0a\u3057\u305f\u5de3\u7bb1\u304b\u3089${count}\u5339\u306e\u305f\u308a\u306a\u3044\u304c\u8ffd\u3044\u51fa\u3055\u308c\u305f\u3002`, "danger", { participants: occupants }); worldRef.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.55, life: 0.32, color: "rgba(255,92,38,0.58)" })); worldRef.markSpatialDirty?.(reason); worldRef.drawListDirty = true; @@ -222,7 +222,7 @@ function updateFirecracker(item, dt, worldRef) { box.hp = 0; box.dead = true; } - worldRef?.log?.("巣箱が焼失した。", "danger"); + worldRef?.log?.("\u5de3\u7bb1\u304c\u713c\u5931\u3057\u305f\u3002", "danger"); worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.45, life: 0.34, color: "rgba(90,54,38,0.58)" })); worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-burnout"); worldRef?.markItemBucketsDirty?.("nest-box-burnout"); diff --git a/js/ground_types.js b/js/ground_types.js index c495c93..fe5e420 100644 --- a/js/ground_types.js +++ b/js/ground_types.js @@ -3,7 +3,7 @@ const GROUND_TYPE_ORDER = window.TarinaiDomainIds.GROUND_IDS; const GROUND_TYPES = Object.freeze({ // \u30c7\u30d5\u30a9\u30eb\u30c8\u5730\u9762ID\u3002\u8868\u793a\u540d\u306f\u300c\u5730\u9762\u300d\u3002 - soil: Object.freeze({ id: "soil", label: "砂", description: "普通の砂地。", stressMultiplier: 1.0, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }), + soil: Object.freeze({ id: "soil", label: "\u7802", description: "\u666e\u901a\u306e\u7802\u5730\u3002", stressMultiplier: 1.0, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }), dirt: Object.freeze({ id: "dirt", label: "\u571f", description: "\u8349\u304c\u3088\u304f\u80b2\u3064\u5730\u9762\u3002", stressMultiplier: 1.0, grassMultiplier: 1.75, grassGrowthMultiplier: 2.45 }), concrete: Object.freeze({ id: "concrete", label: "\u30b3\u30f3\u30af\u30ea\u30fc\u30c8", description: "\u8349\u304c\u751f\u3048\u306a\u3044", stressMultiplier: 1.0, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0 }), blanket: Object.freeze({ id: "blanket", label: "\u3075\u308f\u3075\u308f\u6bdb\u5e03", description: "\u30b9\u30c8\u30ec\u30b9\u304c\u5897\u3048\u306b\u304f\u3044\u3002\u8349\u306f\u751f\u3048\u306a\u3044", stressMultiplier: 0.5, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0 }), diff --git a/js/health.js b/js/health.js index ae86e19..8ba176b 100644 --- a/js/health.js +++ b/js/health.js @@ -32,14 +32,16 @@ const HEALTH = (() => { dwarfDrug: "\u77ee\u5c0f\u85ac", freeze: "\u51CD\u7D50", heatstroke: "\u71B1\u4E2D\u75C7", - cleaning: "掃除", + cleaning: "\u6383\u9664", + electrocution: "\u611f\u96fb", }); const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/; function causeLabel(reason = "") { const text = String(reason || ""); if (!text) return ""; - if (/掃除|ロボ掃除機|robot.?cleaner|cleaned/.test(text)) return CAUSES.cleaning; + if (/\u6383\u9664|\u30ed\u30dc\u6383\u9664\u6a5f|robot.?cleaner|cleaned/.test(text)) return CAUSES.cleaning; + if (/\u611f\u96fb|\u901a\u96fb\u4e2d\u306e\u96fb\u7dda|electrocution|electric.?shock|shocked/i.test(text)) return CAUSES.electrocution; if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, ""); if (/\u51CD\u7D50|\u4F4E\u6E29|\u5BD2\u3055|cold|freeze|freezing/i.test(text)) return CAUSES.freeze; if (/\u71B1\u4E2D\u75C7|\u9AD8\u6E29|\u6691\u3055|heat|hot/i.test(text)) return CAUSES.heatstroke; @@ -273,6 +275,7 @@ const HEALTH = (() => { const jitter = (Math.random() - 0.5) * Math.max(4, (t.radius || 22) * 0.25); const x = t.x - side * (t.radius || 22) * (0.72 + Math.random() * 0.24); const y = t.y + (t.radius || 22) * (0.24 + Math.random() * 0.25) + jitter; + // Damage-triggered release is involuntary and must never be redirected to a toilet. t.world.spawnZunchi?.(x, y, t); t.poopCount = (t.poopCount || 0) + 1; t.digest = Math.max(0, Number(t.digest || 0) - 1.0); diff --git a/js/item_dynamic_ball_system.js b/js/item_dynamic_ball_system.js index 3008637..b9dcde0 100644 --- a/js/item_dynamic_ball_system.js +++ b/js/item_dynamic_ball_system.js @@ -91,8 +91,8 @@ const searchRadius = Math.max(150, (item.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 120); const items = worldRef.nearbyItems(item.x, item.y, searchRadius) || []; const seen = new Set(items); - // 大きな機構は中心が近傍セル外でも当たる可能性がある。 - // 全item走査ではなく、機構type bucketだけを追加確認する。 + // \u5927\u304d\u306a\u6a5f\u69cb\u306f\u4e2d\u5fc3\u304c\u8fd1\u508d\u30bb\u30eb\u5916\u3067\u3082\u5f53\u305f\u308b\u53ef\u80fd\u6027\u304c\u3042\u308b\u3002 + // \u5168item\u8d70\u67fb\u3067\u306f\u306a\u304f\u3001\u6a5f\u69cbtype bucket\u3060\u3051\u3092\u8ffd\u52a0\u78ba\u8a8d\u3059\u308b\u3002 const mechanicalTypes = ["rotator", "poison_block", "reciprocator"]; for (const type of mechanicalTypes) { const bucket = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType(type) : (worldRef.items || []); @@ -448,7 +448,7 @@ } } - const WATER_BALLOON_SURPRISE_TEXT = "水風船が割れておどろいている。"; + const WATER_BALLOON_SURPRISE_TEXT = "\u6c34\u98a8\u8239\u304c\u5272\u308c\u3066\u304a\u3069\u308d\u3044\u3066\u3044\u308b\u3002"; function tarinaiOpennessValue(t) { const v = (typeof global.effectivePersonalityValue === "function") @@ -577,7 +577,7 @@ const d = Math.max(1, distXY(balloon.x, balloon.y, t.x, t.y)); const strength = clamp(1 - d / 165, 0, 1); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.34 + strength * 0.44); - if (typeof t.addStress === "function") t.addStress(0.18 + strength * 0.42, "水風船が割れて驚いた"); + if (typeof t.addStress === "function") t.addStress(0.18 + strength * 0.42, "\u6c34\u98a8\u8239\u304c\u5272\u308c\u3066\u9a5a\u3044\u305f"); if (typeof t.spawnBubble === "function" && strength > 0.35) t.spawnBubble("!", { life: 0.72 }); const awayX = (t.x - balloon.x) / d; const awayY = (t.y - balloon.y) / d; @@ -628,13 +628,13 @@ worldRef?.effects?.push(new Effect("fight", balloon.x, balloon.y, { size: deterministicRange(worldRef || balloon, "water-balloon-pop-size", 4, 10, balloon, i), life: 0.28, color: "rgba(95,204,246,0.72)", vx: Math.cos(a) * s, vy: Math.sin(a) * s })); } spawnWaterFromBalloon(balloon, worldRef); - const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "火" - : (cause?.type === "firecracker" ? "爆竹" - : (cause?.type === "shoot" ? "射撃" - : (cause?.type === "genkotsu" ? "げんこつ" - : (cause && typeof isPinType === "function" && isPinType(cause.type) ? "鋲" : "強い衝撃")))); + const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "\u706b" + : (cause?.type === "firecracker" ? "\u7206\u7af9" + : (cause?.type === "shoot" ? "\u5c04\u6483" + : (cause?.type === "genkotsu" ? "\u3052\u3093\u3053\u3064" + : (cause && typeof isPinType === "function" && isPinType(cause.type) ? "\u92f2" : "\u5f37\u3044\u885d\u6483")))); if (worldRef?.relationNotice?.(balloon.id || "balloon", cause?.id || sourceLabel, "water-balloon-pop", 1.4)) { - worldRef.log?.(`水風船が${sourceLabel}に触れて割れた。`, "accident", { participants: [] }); + worldRef.log?.(`\u6c34\u98a8\u8239\u304c${sourceLabel}\u306b\u89e6\u308c\u3066\u5272\u308c\u305f\u3002`, "accident", { participants: [] }); } applyWaterBalloonSurprise(balloon, worldRef); worldRef?.markSpatialDirty?.("water-balloon-pop"); @@ -643,6 +643,7 @@ global.TarinaiItemDynamicBallSystem = Object.freeze({ update, updateFan, + fanWindAt, updateWaterDropMotion, resolveObstacleCollisions, popBalloon, diff --git a/js/item_dynamic_tool_system.js b/js/item_dynamic_tool_system.js index 98b59b0..beff6fe 100644 --- a/js/item_dynamic_tool_system.js +++ b/js/item_dynamic_tool_system.js @@ -40,7 +40,7 @@ } - const STICKY_BOMB_PANIC_TEXT = "粘着ボムが付いてパニックになっている。"; + const STICKY_BOMB_PANIC_TEXT = "\u7c98\u7740\u30dc\u30e0\u304c\u4ed8\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b\u3002"; const STICKY_BOMB_FUSE_SECONDS = 12.0; const STICKY_BOMB_BLAST_SCALE = 0.68; // identical to TOOL_SIZE_SCALES.small @@ -83,7 +83,7 @@ panicStickyBombCarrier(carrier, item, worldRef, true); if (old && old !== carrier) { old.surpriseTimer = Math.max(old.surpriseTimer || 0, 0.18); - old.thought = "粘着ボムが移った"; + old.thought = "\u7c98\u7740\u30dc\u30e0\u304c\u79fb\u3063\u305f"; } if (worldRef?.effects && typeof Effect !== "undefined") { worldRef.effects.push(new Effect("ring", item.x, item.y, { size: Math.max(16, (item.r || 16) * 1.45), life: 0.18, color: reason === "transfer" ? "rgba(255,210,72,0.52)" : "rgba(255,128,78,0.48)" })); @@ -104,7 +104,7 @@ item.amount = 0; item.hp = 0; audio.explode?.(); - worldRef?.log?.("粘着ボムが爆発した。", "accident"); + worldRef?.log?.("\u7c98\u7740\u30dc\u30e0\u304c\u7206\u767a\u3057\u305f\u3002", "accident"); return true; } diff --git a/js/item_effect_definitions.js b/js/item_effect_definitions.js index 4320ab8..a685372 100644 --- a/js/item_effect_definitions.js +++ b/js/item_effect_definitions.js @@ -42,6 +42,14 @@ sleep_drug: { label: "\u306d\u3080\u308a\u85ac", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"], }, + first_aid: { + label: "\u6551\u6025\u85ac", color: "rgba(210, 72, 82, 0.82)", kind: "instant", + tags: ["item", "heal"], display: () => "\u5373\u6642 / HP\u56de\u5fa9\u306e\u307f", + }, + sedative: { + label: "\u93ae\u9759\u85ac", color: "rgba(92, 142, 174, 0.82)", kind: "timed", defaultDuration: 42, + tags: ["item", "behavior"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u6050\u6016\u3068\u3051\u3093\u304b\u6c17\u5206\u3092\u6291\u3048\u308b`, + }, mystery_drug: { label: "\u602a\u3057\u3044\u85ac", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"], }, @@ -103,6 +111,14 @@ for (const id of ["love_mochi", "fight_mochi", "sleep_drug"]) { Object.assign(rawDefinitions[id], { mysteryEligible: true, zundaJuiceRemovable: id !== "sleep_drug" }); } + Object.assign(rawDefinitions.first_aid, { mysteryEligible: false, zundaJuiceRemovable: false }); + Object.assign(rawDefinitions.sedative, { + modifiers: { fearMultiplier: 0.45, fightWeightMultiplier: 0.28, speedMultiplier: 0.72 }, + visualEffectId: "sedative", + bubblePolicy: "none", + mysteryEligible: true, + zundaJuiceRemovable: true, + }); Object.assign(rawDefinitions.zunda_juice, { removesItemEffects: true, mysteryEligible: false }); Object.assign(rawDefinitions.love_mochi, { @@ -150,6 +166,31 @@ return tarinai.infectSleepDisease?.(context.item || null) || true; }, }); + Object.assign(rawDefinitions.first_aid, { + onApply(tarinai, context = {}) { + const max = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100); + const scale = Math.max(0.35, Number(context.scale ?? 1) || 1); + const amount = Math.max(18, Math.round(max * 0.38 * scale)); + tarinai.recoverHealth?.(amount); + tarinai.hpBarTimer = Math.max(tarinai.hpBarTimer || 0, 2.4); + tarinai.spawnPowerItemEffect?.("first_aid", 0.42); + tarinai.bubble?.("HP+", 2.2, "rgba(210,72,82,0.82)", { force: true }); + return true; + }, + }); + Object.assign(rawDefinitions.sedative, { + onApply(tarinai, context = {}) { + const duration = Math.round((this.defaultDuration || 42) * Math.max(0.35, Number(context.scale ?? 1) || 1)); + tarinai.setTimedItemEffect?.("sedative", duration); + tarinai.fearTimer = Math.max(0, (tarinai.fearTimer || 0) - 2.4); + tarinai.intimidatedTimer = Math.max(0, (tarinai.intimidatedTimer || 0) - 1.8); + tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 10); + tarinai.postConflictPeaceTimer = Math.max(tarinai.postConflictPeaceTimer || 0, 14); + if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { safety: -18, social: -2 }); + tarinai.spawnPowerItemEffect?.("sedative", 0.36); + return true; + }, + }); Object.assign(rawDefinitions.laxative, { onApply(tarinai, context = {}) { return tarinai.setTimedItemEffect?.("laxative", Math.round((this.defaultDuration || 60) * (context.scale ?? 1))); @@ -249,7 +290,7 @@ return this.timerValue(tarinai, id) > 0.04; } activeSummary(tarinai = null) { - const order = ["protein", "niteropu", "mercury", "giant_drug", "dwarf_drug", "laxative", "ammo", "love_mochi", "fight_mochi"]; + const order = ["protein", "niteropu", "mercury", "giant_drug", "dwarf_drug", "laxative", "ammo", "sedative", "love_mochi", "fight_mochi"]; return order .filter(id => this.isActive(tarinai, id)) .map(id => ({ diff --git a/js/item_food_definitions.js b/js/item_food_definitions.js index f868588..7cc140f 100644 --- a/js/item_food_definitions.js +++ b/js/item_food_definitions.js @@ -4,11 +4,13 @@ (function (global) { const rawDefinitions = { - food: { label: "\u98df\u3079\u7269", nutrition: 18.0, hungerRelief: 1.0, interest: "normal", decayRateMultiplier: 1.0 }, + food: { label: "\u30da\u30c3\u30c8\u30d5\u30fc\u30c9", nutrition: 20.0, hungerRelief: 1.08, interest: "normal", decayRateMultiplier: 1.0 }, sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 16.5, hungerRelief: 0.95, interest: "zunda_high", decayRateMultiplier: 0.82 }, love_mochi: { label: "\u3078\u3053\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "love_mochi" }, fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "fight_mochi" }, sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "sleep_drug" }, + first_aid: { label: "\u6551\u6025\u85ac", nutrition: 1.0, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "first_aid" }, + sedative: { label: "\u93ae\u9759\u85ac", nutrition: 11.0, hungerRelief: 0.12, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "sedative" }, laxative: { label: "\u4e0b\u5264", nutrition: 6.8, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "laxative" }, protein: { label: "\u30d7\u30ed\u30c6\u30a4\u30f3", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "protein" }, niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "niteropu" }, diff --git a/js/item_lifecycle_step_frame.js b/js/item_lifecycle_step_frame.js index 90e4166..a8eddc3 100644 --- a/js/item_lifecycle_step_frame.js +++ b/js/item_lifecycle_step_frame.js @@ -8,6 +8,7 @@ if (!item || item.dead) return { done: true }; item.age += dt; if (item.type === "zunchi") item.spawnGrace = Math.max(0, (item.spawnGrace || 0) - dt); + if (item.type === "toilet") item.toiletFlash = Math.max(0, (item.toiletFlash || 0) - dt * 1.8); if (item.dropTimer > 0) { const beforeDrop = item.dropTimer; item.dropTimer = Math.max(0, item.dropTimer - dt); diff --git a/js/item_lifecycle_support.js b/js/item_lifecycle_support.js index 19da494..ef207a5 100644 --- a/js/item_lifecycle_support.js +++ b/js/item_lifecycle_support.js @@ -6,7 +6,7 @@ // globals instead of carrying helper implementations inline. (function (global) { const DUPLICATOR_STORABLE_TYPES = new Set([ - "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", + "food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "water", "pushpin", "oshibyo" ]); diff --git a/js/item_render_runtime.js b/js/item_render_runtime.js index fd6ae07..6fdd8c7 100644 --- a/js/item_render_runtime.js +++ b/js/item_render_runtime.js @@ -225,6 +225,101 @@ draw(ctx, t, lighting = null) { 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 === "toilet") { + const r = this.r || 48; + const flash = clamp(this.toiletFlash || 0, 0, 1); + const footprint = globalThis.TarinaiToiletSandSystem?.footprint?.(this) || { + centerX: this.x, + centerY: this.y + r * 0.08, + halfWidth: r * 1.55, + halfHeight: r * 0.72, + cornerRadius: r * 0.18, + angle: Number(this.angle || 0) || 0, + }; + const localY = footprint.centerY - this.y; + ctx.save(); + ctx.rotate(footprint.angle || 0); + if (itemShadowsEnabled()) { + ctx.shadowColor = "rgba(54,42,31,0.16)"; + ctx.shadowBlur = 5; + ctx.shadowOffsetY = 3; + } + ctx.fillStyle = styleNight ? "rgba(91,77,68,0.96)" : "rgba(143,116,91,0.96)"; + ctx.strokeStyle = styleNight ? "rgba(45,39,36,0.92)" : "rgba(79,62,49,0.90)"; + ctx.lineWidth = Math.max(1.8, r * 0.060); + roundedRect(ctx, -footprint.halfWidth, localY - footprint.halfHeight, footprint.halfWidth * 2, footprint.halfHeight * 2, footprint.cornerRadius); + ctx.fill(); + ctx.stroke(); + const inset = Math.max(4, r * 0.14); + ctx.fillStyle = styleNight ? "rgba(146,128,91,0.96)" : "rgba(218,190,128,0.98)"; + roundedRect(ctx, -footprint.halfWidth + inset, localY - footprint.halfHeight + inset, (footprint.halfWidth - inset) * 2, (footprint.halfHeight - inset) * 2, Math.max(2, footprint.cornerRadius * 0.62)); + ctx.fill(); + ctx.fillStyle = styleNight ? "rgba(176,156,111,0.72)" : "rgba(244,220,164,0.78)"; + for (const grain of [[-1.08,-0.20,0.055],[-0.62,0.21,0.045],[-0.18,-0.15,0.050],[0.28,0.19,0.042],[0.72,-0.18,0.052],[1.12,0.16,0.040]]) { + ctx.beginPath(); + ctx.arc(r * grain[0], localY + r * grain[1], Math.max(1, r * grain[2]), 0, Math.PI * 2); + ctx.fill(); + } + if (flash > 0.02) { + ctx.globalAlpha *= flash * 0.42; + ctx.fillStyle = "rgba(255,244,204,0.96)"; + roundedRect(ctx, -footprint.halfWidth + inset * 0.55, localY - footprint.halfHeight + inset * 0.55, (footprint.halfWidth - inset * 0.55) * 2, (footprint.halfHeight - inset * 0.55) * 2, Math.max(2, footprint.cornerRadius * 0.78)); + ctx.fill(); + } + ctx.restore(); + } else if (this.type === "rain_shelter") { + const r = this.r || 50; + const footprint = globalThis.TarinaiParasolSystem?.parasolFootprint?.(this) || { + centerX: this.x, + centerY: this.y + r * 0.84, + radiusX: r * 1.58, + radiusY: r * 0.68, + }; + ctx.save(); + ctx.fillStyle = styleNight ? "rgba(18,18,28,0.26)" : "rgba(54,48,54,0.20)"; + ctx.beginPath(); + ctx.ellipse(footprint.centerX - this.x, footprint.centerY - this.y, footprint.radiusX, footprint.radiusY, 0, 0, Math.PI * 2); + ctx.fill(); + if (itemShadowsEnabled()) { + ctx.shadowColor = "rgba(54,42,31,0.18)"; + ctx.shadowBlur = 7; + ctx.shadowOffsetY = 5; + } + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.strokeStyle = styleNight ? "rgba(70,70,78,0.90)" : "rgba(92,86,76,0.82)"; + ctx.lineWidth = Math.max(2.2, r * 0.065); + ctx.beginPath(); + ctx.moveTo(0, -r * 0.55); + ctx.lineTo(0, r * 0.84); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(-r * 0.24, r * 0.84); + ctx.lineTo(r * 0.24, r * 0.84); + ctx.stroke(); + ctx.fillStyle = styleNight ? "rgba(116,76,112,0.96)" : "rgba(225,104,126,0.96)"; + ctx.strokeStyle = styleNight ? "rgba(62,42,68,0.92)" : "rgba(126,54,72,0.84)"; + ctx.lineWidth = Math.max(1.8, r * 0.065); + ctx.beginPath(); + ctx.moveTo(-r * 1.12, -r * 0.24); + ctx.quadraticCurveTo(-r * 0.72, -r * 0.98, 0, -r * 1.10); + ctx.quadraticCurveTo(r * 0.72, -r * 0.98, r * 1.12, -r * 0.24); + ctx.quadraticCurveTo(r * 0.76, -r * 0.06, r * 0.50, -r * 0.24); + ctx.quadraticCurveTo(r * 0.24, -r * 0.04, 0, -r * 0.24); + ctx.quadraticCurveTo(-r * 0.24, -r * 0.04, -r * 0.50, -r * 0.24); + ctx.quadraticCurveTo(-r * 0.76, -r * 0.06, -r * 1.12, -r * 0.24); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.strokeStyle = styleNight ? "rgba(210,184,205,0.44)" : "rgba(255,220,224,0.62)"; + ctx.lineWidth = Math.max(1, r * 0.025); + for (const x of [-0.50, 0, 0.50]) { + ctx.beginPath(); + ctx.moveTo(0, -r * 1.08); + ctx.quadraticCurveTo(r * x * 0.72, -r * 0.68, r * x, -r * 0.24); + ctx.stroke(); + } + ctx.restore(); } else if (this.type === "water") { drawOvalWaterDropSprite(ctx, this.r * visualFieldScaleFor("water"), visualPaletteFor("water")); } else if (this.type === "grass") { @@ -1139,7 +1234,8 @@ draw(ctx, t, lighting = null) { ctx.strokeStyle = active ? "#9b6512" : "#69717a"; ctx.lineWidth=3; ctx.beginPath(); ctx.roundRect?.(-27,-18,54,36,8); if (!ctx.roundRect) { ctx.rect(-27,-18,54,36); } ctx.fill(); ctx.stroke(); ctx.fillStyle = active ? "#fff6c9" : "#7b838d"; ctx.beginPath(); ctx.arc(0,-3,9,0,Math.PI*2); ctx.fill(); - ctx.fillStyle = styleNight ? "#eef2f6" : "#26323e"; ctx.font="bold 10px ui-rounded, sans-serif"; ctx.textAlign="center"; ctx.fillText(`${this.pressureTarget === "item" ? "物" : "た"}${this.pressureThreshold || 1}`,0,14); + const detectorLabel = this.pressureTarget === "time" ? "\u6642\u523b" : `${this.pressureTarget === "item" ? "\u7269" : "\u305f"}${this.pressureThreshold || 1}`; + ctx.fillStyle = styleNight ? "#eef2f6" : "#26323e"; ctx.font="bold 10px ui-rounded, sans-serif"; ctx.textAlign="center"; ctx.fillText(detectorLabel,0,14); ctx.restore(); } else if (this.type === "rotator") { const angle = itemAngleFor(this); diff --git a/js/item_tool_definitions.js b/js/item_tool_definitions.js index f0089da..f27986f 100644 --- a/js/item_tool_definitions.js +++ b/js/item_tool_definitions.js @@ -6,7 +6,7 @@ const TOOL_DEFINITIONS = Object.freeze({ observe: { id: "observe", label: "\u305f\u308a\u306a\u3044\u30c7\u30fc\u30bf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" }, clear_tool: { id: "clear_tool", label: "\u89e3\u9664", placeable: false, scalable: false, iconText: "\u2715", tooltip: "\u9078\u629e\u4e2d\u306e\u9053\u5177\u30dc\u30bf\u30f3\u3092\u89e3\u9664\u3057\u3001\u901a\u5e38\u306e\u89b3\u5bdf\u72b6\u614b\u306b\u623b\u3059\u3002" }, delete: { id: "delete", label: "\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002" }, - area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, iconText: "▦", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" }, + area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, iconText: "\u25a6", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" }, copy: { id: "copy", label: "\u30b3\u30d4\u30fc", placeable: false, scalable: false, iconText: "\u29c9", tooltip: "\u7f6e\u3044\u305f\u30a2\u30a4\u30c6\u30e0\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30b3\u30d4\u30fc\u3057\u3001\u6b21\u306e\u30af\u30ea\u30c3\u30af\u3067\u8a2d\u5b9a\u3054\u3068\u8cbc\u308a\u4ed8\u3051\u308b\u3002" }, undo: { id: "undo", label: "\u623b\u3059", placeable: false, scalable: false, iconText: "\u21b6", tooltip: "\u76f4\u524d\u306e\u64cd\u4f5c\u3092\u53d6\u308a\u6d88\u3059\u3002" }, redo: { id: "redo", label: "\u9032\u3080", placeable: false, scalable: false, iconText: "\u21b7", tooltip: "\u53d6\u308a\u6d88\u3057\u305f\u64cd\u4f5c\u3092\u3084\u308a\u76f4\u3059\u3002" }, @@ -16,9 +16,9 @@ const TOOL_DEFINITIONS = Object.freeze({ water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3084\u706b\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" }, rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" }, - spring: { id: "spring", label: "バネ", placeable: false, scalable: false, iconText: "≋", tooltip: "2つの対象を順にクリックして、伸縮するバネでつなぐ。" }, - wire: { id: "wire", label: "電線", placeable: false, scalable: false, iconText: "⌁", tooltip: "検知器の信号を電気で伝える。触ると感電。" }, - insulated_wire: { id: "insulated_wire", label: "絶縁電線", placeable: false, scalable: false, iconText: "⌁", tooltip: "検知器の信号を電気で伝える。感電しない。" }, + spring: { id: "spring", label: "\u30d0\u30cd", placeable: false, scalable: false, iconText: "\u224b", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u4f38\u7e2e\u3059\u308b\u30d0\u30cd\u3067\u3064\u306a\u3050\u3002" }, + wire: { id: "wire", label: "\u96fb\u7dda", placeable: false, scalable: false, iconText: "\u2301", tooltip: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3092\u96fb\u6c17\u3067\u4f1d\u3048\u308b\u3002\u89e6\u308b\u3068\u611f\u96fb\u3002" }, + insulated_wire: { id: "insulated_wire", label: "\u7d76\u7e01\u96fb\u7dda", placeable: false, scalable: false, iconText: "\u2301", tooltip: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3092\u96fb\u6c17\u3067\u4f1d\u3048\u308b\u3002\u611f\u96fb\u3057\u306a\u3044\u3002" }, shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" }, robot_cleaner: { id: "robot_cleaner", itemType: "robot_cleaner", label: "\u30ed\u30dc\u6383\u9664\u6a5f", placeable: true, scalable: false, radius: 22, amount: 999, iconText: "\u25c9", tooltip: "\u30b4\u30df\u3084\u6c5a\u308c\u3092\u7247\u4ed8\u3051\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" }, zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" }, @@ -43,19 +43,19 @@ const TOOL_DEFINITIONS = Object.freeze({ 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" }, bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.webp", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" }, nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" }, - pipe: { id: "pipe", itemType: "pipe", label: "\u571f\u7ba1", placeable: true, scalable: false, radius: 36, amount: 999, iconText: "\u25ce", collisionShape: "circle", tooltip: "移動できる地下空間。たりないが眠ることもある。" }, + pipe: { id: "pipe", itemType: "pipe", label: "\u571f\u7ba1", placeable: true, scalable: false, radius: 36, amount: 999, iconText: "\u25ce", collisionShape: "circle", tooltip: "\u79fb\u52d5\u3067\u304d\u308b\u5730\u4e0b\u7a7a\u9593\u3002\u305f\u308a\u306a\u3044\u304c\u7720\u308b\u3053\u3068\u3082\u3042\u308b\u3002" }, dry_ice: { id: "dry_ice", itemType: "dry_ice", label: "\u30C9\u30E9\u30A4\u30A2\u30A4\u30B9", placeable: true, scalable: false, radius: 21, amount: 999, iconText: "\uD83E\uDDCA", collisionShape: "circle", tooltip: "\u5468\u56F2\u306E\u6C17\u6E29\u309210\u2103\u4E0B\u3052\u308B\u3002\u52B9\u679C\u306F\u91CD\u306D\u3089\u308C\u308B\u3002" }, stove: { id: "stove", itemType: "stove", label: "\u30B9\u30C8\u30FC\u30D6", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "\uD83D\uDD25", collisionShape: "circle", tooltip: "\u5468\u56F2\u306E\u6C17\u6E29\u309210\u2103\u4E0A\u3052\u308B\u3002\u52B9\u679C\u306F\u91CD\u306D\u3089\u308C\u308B\u3002" }, 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, icon: "assets/ui/tool_ball.webp", iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" }, - balloon: { id: "balloon", itemType: "balloon", label: "水風船", placeable: true, scalable: false, radius: 20, amount: 999, iconText: "💧", collisionShape: "circle", tooltip: "たりないのおもちゃ。割れると水滴が飛び散る。" }, - fan: { id: "fan", itemType: "fan", label: "扇風機", placeable: true, scalable: false, radius: 26, amount: 999, iconText: "🌀", collisionShape: "circle", tooltip: "前方に風を出す。軽いものを押し、少し涼しくする。Q/Eで置く向きを変える。配置後にクリックすると首振り設定を開く。" }, + balloon: { id: "balloon", itemType: "balloon", label: "\u6c34\u98a8\u8239", placeable: true, scalable: false, radius: 20, amount: 999, iconText: "\ud83d\udca7", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u306e\u304a\u3082\u3061\u3083\u3002\u5272\u308c\u308b\u3068\u6c34\u6ef4\u304c\u98db\u3073\u6563\u308b\u3002" }, + fan: { id: "fan", itemType: "fan", label: "\u6247\u98a8\u6a5f", placeable: true, scalable: false, radius: 26, amount: 999, iconText: "\ud83c\udf00", collisionShape: "circle", tooltip: "\u524d\u65b9\u306b\u98a8\u3092\u51fa\u3059\u3002\u8efd\u3044\u3082\u306e\u3092\u62bc\u3057\u3001\u5c11\u3057\u6dbc\u3057\u304f\u3059\u308b\u3002Q/E\u3067\u7f6e\u304f\u5411\u304d\u3092\u5909\u3048\u308b\u3002\u914d\u7f6e\u5f8c\u306b\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u9996\u632f\u308a\u8a2d\u5b9a\u3092\u958b\u304f\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" }, fire: { id: "fire", itemType: "fire", label: "\u708e", placeable: false, scalable: false, radius: 18, amount: 140, iconText: "\uD83D\uDD25", collisionShape: "circle", simulationOnly: true }, flame_firecracker: { id: "flame_firecracker", itemType: "flame_firecracker", label: "\u706b\u708e\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, iconText: "\uD83E\uDDE8", collisionShape: "circle", tooltip: "\u5468\u56f2\u306b\u708e\u3092\u6492\u304d\u6563\u3089\u3059\u3002" }, - sticky_bomb: { id: "sticky_bomb", itemType: "sticky_bomb", label: "粘着ボム", placeable: true, scalable: false, radius: 16, amount: 999, iconText: "\uD83D\uDCA3", collisionShape: "circle", tooltip: "\u4e00\u5b9a\u6642\u9593\u3067\u7206\u767a\u3059\u308b\u7206\u5f3e\u3002\u4ed8\u7740\u3057\u305f\u500b\u4f53\u306f\u30d1\u30cb\u30c3\u30af\u306b\u306a\u308b\u3002\u4ed6\u500b\u4f53\u306b\u89e6\u308c\u308b\u3068\u30dc\u30e0\u304c\u305d\u3063\u3061\u306b\u79fb\u52d5\u3059\u308b\u3002" }, + sticky_bomb: { id: "sticky_bomb", itemType: "sticky_bomb", label: "\u7c98\u7740\u30dc\u30e0", placeable: true, scalable: false, radius: 16, amount: 999, iconText: "\uD83D\uDCA3", collisionShape: "circle", tooltip: "\u4e00\u5b9a\u6642\u9593\u3067\u7206\u767a\u3059\u308b\u7206\u5f3e\u3002\u4ed8\u7740\u3057\u305f\u500b\u4f53\u306f\u30d1\u30cb\u30c3\u30af\u306b\u306a\u308b\u3002\u4ed6\u500b\u4f53\u306b\u89e6\u308c\u308b\u3068\u30dc\u30e0\u304c\u305d\u3063\u3061\u306b\u79fb\u52d5\u3059\u308b\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" }, oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" }, fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" }, @@ -64,16 +64,20 @@ const TOOL_DEFINITIONS = Object.freeze({ bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u5f37\u70c8\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" }, bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u5f37\u70c8\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" }, gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" }, - one_way_fence: { id: "one_way_fence", itemType: "one_way_fence", label: "一方通行柵", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "⇧", collisionShape: "oriented_rect", tooltip: "矢印方向には通過でき、逆方向からは通れない柵。Q/Eで45度、Shift+Q/Eで5度ずつ向きを変える。" }, + one_way_fence: { id: "one_way_fence", itemType: "one_way_fence", label: "\u4e00\u65b9\u901a\u884c\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "\u21e7", collisionShape: "oriented_rect", tooltip: "\u77e2\u5370\u65b9\u5411\u306b\u306f\u901a\u904e\u3067\u304d\u3001\u9006\u65b9\u5411\u304b\u3089\u306f\u901a\u308c\u306a\u3044\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" }, rotator: { id: "rotator", itemType: "rotator", label: "\u56de\u8ee2\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21bb", collisionShape: "custom_rotator", tooltip: "\u56de\u8ee2\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" }, poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 44, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" }, reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" }, - pressure_switch: { id: "pressure_switch", itemType: "pressure_switch", label: "検知器", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "▣", collisionShape: "circle", tooltip: "表示された矩形範囲内にいるたりない、またはアイテム数を検知して電線へ信号を送る。クリックで対象・必要数・範囲を設定。" }, + pressure_switch: { id: "pressure_switch", itemType: "pressure_switch", label: "\u691c\u77e5\u5668", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u25a3", collisionShape: "circle", tooltip: "\u69d8\u3005\u306a\u72b6\u6cc1\u3092\u691c\u77e5\u3057\u3066\u96fb\u7dda\u306b\u4fe1\u53f7\u3092\u9001\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" }, water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" }, trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true }, splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true }, - food: { id: "food", itemType: "food", label: "\u98df\u3079\u7269", placeable: false, scalable: false, radius: 14, amount: 85, simulationOnly: true }, + food: { id: "food", itemType: "food", label: "\u30da\u30c3\u30c8\u30d5\u30fc\u30c9", placeable: true, scalable: true, radius: 14, amount: 85, iconText: "\ud83e\udd63", tooltip: "\u7279\u6b8a\u52b9\u679c\u306e\u306a\u3044\u666e\u901a\u306e\u3054\u306f\u3093\u3002" }, ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true }, + toilet: { id: "toilet", itemType: "toilet", label: "\u30c8\u30a4\u30ec\u306e\u7802", placeable: true, scalable: true, radius: 48, amount: 999, iconText: "\u25a3", tooltip: "\u4e0a\u306b\u6392\u6cc4\u3055\u308c\u305f\u305a\u3093\u3061\u304c\u76f4\u3061\u306b\u6d88\u6ec5\u3059\u308b\u3002" }, + rain_shelter: { id: "rain_shelter", itemType: "rain_shelter", label: "\u30d1\u30e9\u30bd\u30eb", placeable: true, scalable: true, radius: 50, amount: 999, iconText: "\u26f1", tooltip: "\u96e8\u3068\u65e5\u5dee\u3057\u3092\u9632\u304e\u3001\u4f53\u611f\u6c17\u6e29\u3092\u7a4f\u3084\u304b\u306b\u3059\u308b\u3002" }, + first_aid: { id: "first_aid", itemType: "first_aid", label: "\u6551\u6025\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, iconText: "\u271a", tooltip: "\u98df\u3079\u308b\u3068HP\u3092\u5373\u6642\u56de\u5fa9\u3059\u308b\u3002" }, + sedative: { id: "sedative", itemType: "sedative", label: "\u93ae\u9759\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, iconText: "\u25cc", tooltip: "\u98df\u3079\u308b\u3068\u6050\u6016\u3084\u3051\u3093\u304b\u6c17\u5206\u3092\u843d\u3061\u7740\u304b\u305b\u308b\u3002" }, }); diff --git a/js/item_tool_metadata.js b/js/item_tool_metadata.js index f3a4e5d..3a7c722 100644 --- a/js/item_tool_metadata.js +++ b/js/item_tool_metadata.js @@ -3,13 +3,14 @@ // Static item/tool metadata shared by tool definitions and UI helpers. (function (global) { const ITEM_TRAITS = Object.freeze({ - obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]), - 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"]), + obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]), + food_interest: new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]), hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "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", "balloon", "pushpin", "oshibyo", "zunchi"]), + freeStackingMovable: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi", "water", "zunda_juice", "mercury", "genkotsu", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "ant_corpse"]), sleepFurniture: new Set(["bed", "nest_box", "pipe", "grass_bed"]), - draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]), + draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]), }); @@ -25,9 +26,9 @@ 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", "slave_chain", "golden_crown"] }, - { id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "signboard"] }, + { id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] }, + { id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["first_aid", "sedative", "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", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "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", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] }, ]); diff --git a/js/item_type_initializers.js b/js/item_type_initializers.js index 27e740e..9b2d392 100644 --- a/js/item_type_initializers.js +++ b/js/item_type_initializers.js @@ -2,6 +2,39 @@ // Type-specific Item constructor state. Kept out of items.js so construction stays focused on shared fields. +// Shared toilet-sand geometry. Rendering and poop absorption must use the +// same rounded rectangle so the visible sand and active area never diverge. +function toiletSandFootprint(item = null) { + if (!item || item.dead || item.type !== "toilet") return null; + const r = Math.max(24, Number(item.r || 48) || 48); + return { + centerX: Number(item.x || 0), + centerY: Number(item.y || 0) + r * 0.08, + halfWidth: r * 1.55, + halfHeight: r * 0.72, + cornerRadius: r * 0.18, + angle: Number(item.angle || 0) || 0, + }; +} + +function toiletSandContains(item = null, x = 0, y = 0) { + const footprint = toiletSandFootprint(item); + if (!footprint) return false; + const dx = Number(x || 0) - footprint.centerX; + const dy = Number(y || 0) - footprint.centerY; + const cos = Math.cos(footprint.angle); + const sin = Math.sin(footprint.angle); + const lx = dx * cos + dy * sin; + const ly = -dx * sin + dy * cos; + const innerX = Math.max(0, footprint.halfWidth - footprint.cornerRadius); + const innerY = Math.max(0, footprint.halfHeight - footprint.cornerRadius); + const qx = Math.max(0, Math.abs(lx) - innerX); + const qy = Math.max(0, Math.abs(ly) - innerY); + return qx * qx + qy * qy <= footprint.cornerRadius * footprint.cornerRadius; +} + +globalThis.TarinaiToiletSandSystem = Object.freeze({ footprint: toiletSandFootprint, contains: toiletSandContains }); + function initializeItemTypeState(item, type, x, y) { if (!item) return item; if (isServingFoodType(type) || ["grass", "ant_corpse", "zunchi"].includes(type)) item.roles.food = true; @@ -22,6 +55,14 @@ function initializeItemTypeState(item, type, x, y) { if (type === "slave_chain" || type === "golden_crown") { item.amount = Math.max(1, Number(item.amount || 0) || 1); } + if (type === "toilet") { + item.amount = 999; + item.toiletFlash = Math.max(0, Number(item.toiletFlash || 0) || 0); + } + if (type === "rain_shelter") { + item.amount = 999; + item.rainShelterRange = Math.max(76, (item.r || 50) * 1.58); + } if (type === "bed") { item.comfort = rand(0.86, 1.14); item.wear = 0; @@ -220,6 +261,8 @@ function initializeItemTypeState(item, type, x, y) { item.pressureThreshold = 1; item.pressureWidth = 220; item.pressureHeight = 160; + item.pressureTimeStart = 360; + item.pressureTimeEnd = 1080; item.signalActive = false; item.deletable = true; item.amount = 999; diff --git a/js/item_update_policy.js b/js/item_update_policy.js index faf05dd..75c2a9b 100644 --- a/js/item_update_policy.js +++ b/js/item_update_policy.js @@ -10,8 +10,8 @@ const SCHEDULED_ITEM_TYPES = Object.freeze([ "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo", "plushie", "grass_bed", "zunchi", "robot_cleaner", "duplicator", "water", "trace", "splat", "ant_corpse", "food", - "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", - "stone", "bed", "nest_box", "pipe", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", + "sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", + "stone", "bed", "nest_box", "pipe", "toilet", "rain_shelter", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", ]); diff --git a/js/item_visual_definitions.js b/js/item_visual_definitions.js index a508f9d..eeb954b 100644 --- a/js/item_visual_definitions.js +++ b/js/item_visual_definitions.js @@ -39,6 +39,24 @@ "fieldScale": 1, "accessibleShapeTraits": ["tablet", "moon", "purple"] }, + "first_aid": { + "renderer": "split_pill", + "shape": "cross_tablet", + "palette": { "fill": "#fff1f1", "stroke": "rgba(180,55,65,0.78)", "highlight": "rgba(220,64,78,0.82)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["tablet", "cross", "red"] + }, + "sedative": { + "renderer": "split_pill", + "shape": "calm_tablet", + "palette": { "fill": "#edf7ff", "stroke": "rgba(58,104,142,0.78)", "highlight": "rgba(92,142,174,0.82)" }, + "scale": 1, + "uiPreviewScale": 1, + "fieldScale": 1, + "accessibleShapeTraits": ["tablet", "calm", "blue"] + }, "laxative": { "renderer": "laxative_tablet", "shape": "rounded_tablet_zunchi_motif", diff --git a/js/items.js b/js/items.js index 2c7990c..5d87fe6 100644 --- a/js/items.js +++ b/js/items.js @@ -281,7 +281,25 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {}) const localR = Math.max(1, Number(r) || 10); const compact = !!opts.compact; ctx.save(); - if (key === "food" || key === "sweet" || key === "love_mochi" || key === "fight_mochi") { + if (key === "food") { + ctx.fillStyle = "#b97842"; + ctx.strokeStyle = "#754725"; + ctx.lineWidth = Math.max(1.2, localR * 0.14); + ctx.beginPath(); + ctx.ellipse(0, localR * 0.16, localR * 1.18, localR * 0.48, 0, 0, Math.PI * 2); + ctx.fill(); ctx.stroke(); + ctx.fillStyle = "#d9aa70"; + ctx.beginPath(); + ctx.ellipse(0, -localR * 0.02, localR * 0.92, localR * 0.42, 0, 0, Math.PI * 2); + ctx.fill(); + const dots = compact ? 5 : 7; + ctx.fillStyle = "#7d4d2c"; + for (let i = 0; i < dots; i++) { + ctx.beginPath(); + ctx.arc(randSeed(localSeed + i, -localR * 0.62, localR * 0.62), randSeed(localSeed + i + 9, -localR * 0.25, localR * 0.20), Math.max(1.1, localR * 0.14), 0, Math.PI * 2); + ctx.fill(); + } + } else if (key === "sweet" || key === "love_mochi" || key === "fight_mochi") { const sweet = key === "sweet"; const loveMochi = key === "love_mochi"; const fightMochi = key === "fight_mochi"; @@ -319,14 +337,6 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {}) ctx.moveTo(-localR * 0.44, localR * 0.42); ctx.lineTo(localR * 0.44, -localR * 0.42); ctx.stroke(); - } else { - ctx.fillStyle = "#fff7dc"; - const dots = compact ? 4 : 5; - for (let i = 0; i < dots; i++) { - ctx.beginPath(); - ctx.arc(randSeed(localSeed + i, -localR * 0.55, localR * 0.55), randSeed(localSeed + i + 7, -localR * 0.34, localR * 0.34), Math.max(1.0, localR * 0.12), 0, Math.PI * 2); - ctx.fill(); - } } } else if (key === "grass") { ctx.strokeStyle = "#5fa53f"; @@ -586,6 +596,8 @@ function drawToolItemPreview(ctx, type = "", opts = {}) { nest_box: 0.78, pipe: 0.84, signboard: 0.76, + toilet: 0.45, + rain_shelter: 0.62, duplicator: 0.82, fence_v: 0.70, fence_h: 0.70, @@ -611,7 +623,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) { if (isConsumableSpriteType(type)) scale *= 1.95; else if (["grass", "water", "zunda_juice", "mercury", "fire"].includes(type)) scale *= 1.85; else if (type === "duplicator") scale *= 1.55; - else if (["signboard", "bed", "nest_box", "pipe", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu"].includes(type)) scale *= 1.08; + else if (["signboard", "toilet", "rain_shelter", "bed", "nest_box", "pipe", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu"].includes(type)) scale *= 1.08; else if (isParamEffectItemType(type)) scale *= 1.35; if (watermark) scale *= 1.42; ctx.scale(scale, scale); diff --git a/js/placement_preview_system.js b/js/placement_preview_system.js index 5ceb55e..0cdcd1f 100644 --- a/js/placement_preview_system.js +++ b/js/placement_preview_system.js @@ -151,6 +151,14 @@ const radius = Math.max(14, (num(item.r, itemRadius(item.type, 12)) || itemRadius(item.type, 12)) * (item.type === "bed" ? 1.55 : 1.25)); return { x: global.clamp ? global.clamp(x, pad + radius, (worldRef.w || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.w || 0) - pad - radius, x)), y: global.clamp ? global.clamp(y, pad + radius, (worldRef.h || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.h || 0) - pad - radius, y)) }; } + function overlayForItem(item) { + if (!item || item.dead) return null; + if (item.type === "rain_shelter") { + const footprint = global.TarinaiParasolSystem?.parasolFootprint?.(item) || null; + if (footprint) return { shape: "ellipse", centerX: footprint.centerX, centerY: footprint.centerY, radiusX: footprint.radiusX, radiusY: footprint.radiusY }; + } + return null; + } function geometryFor(worldRef, item) { if (!worldRef || !item) return null; ensureMechanicalPreview(item, worldRef); @@ -174,6 +182,7 @@ r: item.r || radius, rect: geometry.rect || null, rects, + overlay: overlayForItem(item), boundsBlocked: bounds, blocked: opts.skipBlocked ? bounds : (bounds || Boolean(worldRef.placementBlocked?.(item) || worldRef.fencePlacementBlocked?.(item))), }; @@ -203,7 +212,7 @@ const rectsForBounds = geometry.rects && geometry.rects.length ? geometry.rects : (geometry.rect ? [geometry.rect] : null); const ownBounds = extra.boundsRadius ? circleOutside(worldRef, x, y, extra.boundsRadius) : boundsBlocked(worldRef, tmp, { rects: rectsForBounds }); const blocked = ownBounds || Boolean(worldRef.placementBlocked?.(tmp)); - return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], influence: extra.influence || null, blocked, item: tmp }; + return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], overlay: overlayForItem(tmp), influence: extra.influence || null, blocked, item: tmp }; } global.TarinaiPlacementPreviewSystem = Object.freeze({ applyToolOrientation, diff --git a/js/render.js b/js/render.js index 8d74e50..60ab58e 100644 --- a/js/render.js +++ b/js/render.js @@ -178,13 +178,16 @@ function strokeWorldObstacleRect(ctx, rect) { function drawToolTargetHoverHighlight(ctx, world) { if (!world) return; const tool = String(world.tool || ""); - const target = tool === "pinch" ? world.hoverGrabTarget : (tool === "delete" ? world.hoverDeleteTarget : null); + const mode = world.hoverGiveTarget ? "give" : (tool === "pinch" ? "pinch" : (tool === "delete" ? "delete" : "")); + const target = mode === "give" ? world.hoverGiveTarget : (mode === "pinch" ? world.hoverGrabTarget : (mode === "delete" ? world.hoverDeleteTarget : null)); if (!target || target.dead || target.playerHeld || target._heldByPlayer) return; const p = world.pointer || {}; if (!p.inside) return; - const style = tool === "delete" - ? { color: "rgba(224,82,68,0.98)", fill: "rgba(224,82,68,0.13)", label: "\u524a\u9664" } - : { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "\u3064\u307e\u3080" }; + const style = mode === "give" + ? { color: "rgba(224,98,142,0.98)", fill: "rgba(224,98,142,0.14)", label: "\u4e0e\u3048\u308b" } + : (mode === "delete" + ? { color: "rgba(224,82,68,0.98)", fill: "rgba(224,82,68,0.13)", label: "\u524a\u9664" } + : { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "\u3064\u307e\u3080" }); const pulse = 0.62 + Math.sin((world.time || 0) * 9.5) * 0.12; const worldLine = world.screenSizeToWorld ? world.screenSizeToWorld(4.6) : 4.6; const outerLine = world.screenSizeToWorld ? world.screenSizeToWorld(2.1) : 2.1; @@ -489,7 +492,7 @@ function placementPreviewFor(world) { function drawPlacementPreview(ctx, world) { const preview = placementPreviewFor(world); if (!preview) return; - const { type, x, y, r, rect, rects, influence, blocked } = preview; + const { type, x, y, r, rect, rects, overlay, influence, blocked } = preview; const pulse = 0.55 + Math.sin((world.time || 0) * 7.0) * 0.10; ctx.save(); ctx.globalAlpha = blocked ? 0.44 : 0.72; @@ -514,7 +517,12 @@ function drawPlacementPreview(ctx, world) { ctx.stroke(); } }; - if (rect || (rects && rects.length)) { + if (overlay?.shape === "ellipse") { + ctx.beginPath(); + ctx.ellipse(overlay.centerX, overlay.centerY, Math.max(1, overlay.radiusX), Math.max(1, overlay.radiusY), 0, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } else if (rect || (rects && rects.length)) { if (rects && rects.length) for (const rr of rects) drawRectPreview(rr); else drawRectPreview(rect); if (type === "nest_box" && world.nestBoxSolidRects) { @@ -567,12 +575,14 @@ function drawPlacementPreview(ctx, world) { ctx.fill(); ctx.restore(); } + const markerX = overlay?.shape === "ellipse" ? overlay.centerX : x; + const markerY = overlay?.shape === "ellipse" ? overlay.centerY : y; ctx.globalAlpha = blocked ? 0.52 : 0.70; ctx.beginPath(); - ctx.moveTo(x - 8, y); - ctx.lineTo(x + 8, y); - ctx.moveTo(x, y - 8); - ctx.lineTo(x, y + 8); + ctx.moveTo(markerX - 8, markerY); + ctx.lineTo(markerX + 8, markerY); + ctx.moveTo(markerX, markerY - 8); + ctx.lineTo(markerX, markerY + 8); ctx.stroke(); ctx.restore(); } @@ -587,7 +597,7 @@ function drawPressureSwitchRangeOverlay(ctx, world) { : (world.items || []).filter(item => item && !item.dead && item.type === "pressure_switch"); for (const item of switches) { - if (!item || item.dead) continue; + if (!item || item.dead || item.pressureTarget === "time") continue; const isEditing = editing?.item === item; specs.push({ x: Number(item.x || 0) || 0, @@ -638,6 +648,7 @@ function drawOperationToolPreview(ctx, world) { if (item) { const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem.forItem(world, item) || null; const previewRects = placementInfo?.rects || world.solidObstacleRects?.(item) || []; + const previewOverlay = placementInfo?.overlay || null; const blocked = Boolean(placementInfo ? placementInfo.blocked : (world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item))); let copyPreviewDrew = false; if (typeof item.draw === "function") { @@ -666,7 +677,11 @@ function drawOperationToolPreview(ctx, world) { ctx.strokeStyle = blocked ? "rgba(194,70,58,0.88)" : "rgba(110,178,55,0.88)"; ctx.fillStyle = blocked ? "rgba(232,84,72,0.14)" : "rgba(190,236,92,0.15)"; const rects = previewRects; - if (rects.length) { + if (previewOverlay?.shape === "ellipse") { + ctx.beginPath(); + ctx.ellipse(previewOverlay.centerX, previewOverlay.centerY, Math.max(1, previewOverlay.radiusX), Math.max(1, previewOverlay.radiusY), 0, 0, Math.PI * 2); + ctx.fill(); ctx.stroke(); + } else if (rects.length) { for (const rr of rects) { if (rr.oriented) { ctx.save(); ctx.translate(rr.cx, rr.cy); ctx.rotate(rr.angle || 0); @@ -679,7 +694,9 @@ function drawOperationToolPreview(ctx, world) { ctx.beginPath(); ctx.ellipse(p.x, p.y, r * 1.45, r * 0.95, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } ctx.setLineDash([]); - ctx.beginPath(); ctx.moveTo(p.x - 9, p.y); ctx.lineTo(p.x + 9, p.y); ctx.moveTo(p.x, p.y - 9); ctx.lineTo(p.x, p.y + 9); ctx.stroke(); + const markerX = previewOverlay?.shape === "ellipse" ? previewOverlay.centerX : p.x; + const markerY = previewOverlay?.shape === "ellipse" ? previewOverlay.centerY : p.y; + ctx.beginPath(); ctx.moveTo(markerX - 9, markerY); ctx.lineTo(markerX + 9, markerY); ctx.moveTo(markerX, markerY - 9); ctx.lineTo(markerX, markerY + 9); ctx.stroke(); ctx.restore(); } return; @@ -864,7 +881,7 @@ function linkIntersectsVisibleRect(it, worldRef, rect, margin = 18) { } function isBehindSpriteLayerItem(it) { - return Boolean(it && !it.dead && (isLinkRenderItem(it) || it.type === "bed" || it.type === "grass_bed")); + return Boolean(it && !it.dead && (isLinkRenderItem(it) || it.type === "bed" || it.type === "grass_bed" || it.type === "toilet")); } function renderLayerSortY(entity) { @@ -878,7 +895,7 @@ function renderLayerSortY(entity) { if (entity.kind === "queen" || entity.kind === "worker") return y + Math.max(4, Number(entity.size || 10) || 10); const type = String(entity.type || ""); const r = Math.max(0, Number(entity.r || entity.radius || entity.size || 0) || 0); - if (type === "bed" || type === "grass_bed") return y + Math.max(8, r * 0.72); + if (type === "bed" || type === "grass_bed" || type === "toilet") return y + Math.max(8, r * 0.72); if (type === "nest_box") return y + Math.max(10, r * 0.76); if (type === "pipe") return y + Math.max(8, r * 0.64); if (type === "fan") return y + Math.max(10, r * 0.95); @@ -1828,7 +1845,8 @@ function drawPointerItemTooltip(ctx, world) { const w = Math.min(maxBoxW, Math.max(92, textW + 22)); const h = Math.max(38, 16 + lines.length * lineH); const x = clamp(pos.x - w / 2, 8, canvasW - w - 8); - const y = clamp(pos.y - (target.r || 24) * 1.8 - h, 8, canvasH - h - 8); + const targetGapScale = 1.45; + const y = clamp(pos.y - (target.r || 24) * targetGapScale - h, 8, canvasH - h - 8); ctx.fillStyle = "rgba(46,38,30,0.66)"; ctx.strokeStyle = "rgba(255,248,220,0.74)"; ctx.lineWidth = 1.2; diff --git a/js/robot_cleaner_system.js b/js/robot_cleaner_system.js index b604f18..852653f 100644 --- a/js/robot_cleaner_system.js +++ b/js/robot_cleaner_system.js @@ -200,8 +200,8 @@ const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains const type = String(typeOverride || item.robotCleanerTargetType || target.type || ""); const label = robotTargetLabel(type); let removed = false; - const cleanReason = "掃除"; - const cleanLogReason = "ロボ掃除機に吸い込まれた"; + const cleanReason = "\u6383\u9664"; + const cleanLogReason = "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u5438\u3044\u8fbc\u307e\u308c\u305f"; if (type === "tarinai" || isRobotTarinaiTarget(target)) { triggerRobotPanic(item, target, worldRef, true); if (typeof target.die === "function") { @@ -216,7 +216,7 @@ const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains } else { removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, target, { reason: "robot-cleaner", - userReason: type === "grass_bed" ? "ロボ掃除機に片付けられた" : cleanLogReason, + userReason: type === "grass_bed" ? "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u7247\u4ed8\u3051\u3089\u308c\u305f" : cleanLogReason, wake: true, breaker: item, })); diff --git a/js/save_system.js b/js/save_system.js index 4affc28..547eb75 100644 --- a/js/save_system.js +++ b/js/save_system.js @@ -65,12 +65,12 @@ confirmDialog.setAttribute("aria-describedby", "saveLoadConfirmMessage"); confirmDialog.innerHTML = `
-

ロード前の確認

+

\u30ed\u30fc\u30c9\u524d\u306e\u78ba\u8a8d

-

現在の状態は上書きされます。必要であれば先にセーブしてください。

+

\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002\u5fc5\u8981\u3067\u3042\u308c\u3070\u5148\u306b\u30bb\u30fc\u30d6\u3057\u3066\u304f\u3060\u3055\u3044\u3002

- - + +
`; document.body.appendChild(confirmDialog); @@ -80,7 +80,7 @@ function requestLoadConfirmation(message) { const confirmDialog = ensureLoadConfirmDialog(); const messageEl = confirmDialog.querySelector("#saveLoadConfirmMessage"); - if (messageEl) messageEl.textContent = String(message || "保存した状態をロードします。"); + if (messageEl) messageEl.textContent = String(message || "\u4fdd\u5b58\u3057\u305f\u72b6\u614b\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002"); confirmDialog.classList.remove("hidden"); const confirmBtn = confirmDialog.querySelector("[data-save-load-confirm]"); const cancelBtn = confirmDialog.querySelector("[data-save-load-cancel]"); @@ -105,7 +105,7 @@ }; confirmDialog.addEventListener("click", onClick); document.addEventListener("keydown", onKeyDown, true); - cancelBtn?.setAttribute?.("aria-label", "ロードをキャンセル"); + cancelBtn?.setAttribute?.("aria-label", "\u30ed\u30fc\u30c9\u3092\u30ad\u30e3\u30f3\u30bb\u30eb"); }); } @@ -199,7 +199,7 @@ } if (loadBtn) { const slot = Number(loadBtn.dataset.loadSlot); - if (!await requestLoadConfirmation(`スロット${slot}をロードします。`)) return; + if (!await requestLoadConfirmation(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002`)) return; try { await loadSlot(slot); closeDialog(); @@ -255,7 +255,7 @@ const area = dialog.querySelector("#saveHashText"); const text = area?.value || ""; if (!text.trim()) { global.showToast?.("\u30ed\u30fc\u30c9\u3059\u308b\u546a\u6587\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; } - if (!await requestLoadConfirmation("貼り付けた呪文をロードします。")) return; + if (!await requestLoadConfirmation("\u8cbc\u308a\u4ed8\u3051\u305f\u546a\u6587\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002")) return; try { restoreSnapshot(await decodeSnapshot(text), global.world); closeDialog(); diff --git a/js/signal_system.js b/js/signal_system.js index 81aa31d..36df018 100644 --- a/js/signal_system.js +++ b/js/signal_system.js @@ -4,12 +4,33 @@ (function (global) { const WIRE_TYPES = new Set(["wire", "insulated_wire"]); const LINK_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]); + const SIGNAL_SINK_TYPES = new Set(["rotator", "reciprocator", "robot_cleaner", "gate_fence"]); function num(value, fallback = 0) { const n = Number(value); return Number.isFinite(n) ? n : fallback; } function clamp(value, min, max) { return Math.max(min, Math.min(max, num(value, min))); } + function normalizeMinute(value, fallback = 0) { + return Math.max(0, Math.min(1435, Math.round(num(value, fallback) / 5) * 5)); + } + function currentMinuteOfDay(worldRef) { + const dayLength = Math.max(1, num(worldRef?.config?.dayLength ?? global.CONFIG?.dayLength, 120)); + const progress = typeof worldRef?.dayProgress === "function" + ? num(worldRef.dayProgress()) + : ((num(worldRef?.time) % dayLength) + dayLength) % dayLength / dayLength; + return Math.max(0, Math.min(1439, Math.floor((((progress % 1) + 1) % 1) * 1440))); + } + function isMinuteInRange(minute, start, end) { + const now = Math.max(0, Math.min(1439, Number(minute) | 0)); + const from = normalizeMinute(start, 360); + const to = normalizeMinute(end, 1080); + if (from === to) return true; + return from < to ? now >= from && now < to : now >= from || now < to; + } + function normalizePressureTarget(value) { + return value === "time" ? "time" : (value === "item" ? "item" : "tarinai"); + } function isWire(item) { return Boolean(item && !item.dead && WIRE_TYPES.has(item.type)); } function emitEffect(worldRef, type, x, y, options = {}) { @@ -75,16 +96,27 @@ } function updatePressureSwitches(worldRef) { + const minute = currentMinuteOfDay(worldRef); for (const item of worldRef.itemsOfType?.("pressure_switch") || []) { if (!item || item.dead) continue; - item.pressureTarget = item.pressureTarget === "item" ? "item" : "tarinai"; + item.pressureTarget = normalizePressureTarget(item.pressureTarget); item.pressureThreshold = Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0)); item.pressureWidth = clamp(item.pressureWidth || 220, 60, 840); item.pressureHeight = clamp(item.pressureHeight || 160, 60, 840); - const count = countPressureTargets(item, worldRef); - const active = count >= item.pressureThreshold; + item.pressureTimeStart = normalizeMinute(item.pressureTimeStart, 360); + item.pressureTimeEnd = normalizeMinute(item.pressureTimeEnd, 1080); + let count = 0; + let active = false; + if (item.pressureTarget === "time") { + active = isMinuteInRange(minute, item.pressureTimeStart, item.pressureTimeEnd); + count = active ? 1 : 0; + } else { + count = countPressureTargets(item, worldRef); + active = count >= item.pressureThreshold; + } if (active !== Boolean(item.signalActive)) worldRef.drawListDirty = true; item.pressureCount = count; + item.pressureCurrentMinute = minute; item.signalActive = active; } } @@ -138,10 +170,16 @@ } const hasSource = component.some(node => node.type === "pressure_switch"); for (const node of component) { - const sink = node.type === "rotator" || node.type === "reciprocator" || node.type === "robot_cleaner"; - if (sink) { - node._signalDriven = hasSource; - node._signalOn = hasSource && active; + if (!SIGNAL_SINK_TYPES.has(node.type)) continue; + node._signalDriven = hasSource; + node._signalOn = hasSource && active; + if (node.type === "gate_fence" && hasSource) { + const nextOpen = Boolean(active); + if (Boolean(node.gateOpen) !== nextOpen) { + node.gateOpen = nextOpen; + worldRef.drawListDirty = true; + worldRef.markSpatialDirty?.("signal-gate-toggle"); + } } } for (const entry of wires) { @@ -151,7 +189,7 @@ for (const item of worldRef.items || []) { if (!item || item.dead) continue; - if ((item.type === "rotator" || item.type === "reciprocator" || item.type === "robot_cleaner") && !connected.has(item)) { + if (SIGNAL_SINK_TYPES.has(item.type) && !connected.has(item)) { item._signalDriven = false; item._signalOn = false; } @@ -186,11 +224,11 @@ target.targetRef = null; target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.55); } else { - target.damage?.(2.4, connected ? "通電中の電線に接続された" : "通電中の電線に触れた"); + target.damage?.(2.4, connected ? "\u901a\u96fb\u4e2d\u306e\u96fb\u7dda\u306b\u63a5\u7d9a\u3055\u308c\u305f" : "\u901a\u96fb\u4e2d\u306e\u96fb\u7dda\u306b\u89e6\u308c\u305f"); target.panicTimer = Math.max(num(target.panicTimer), 0.8); target.fearTimer = Math.max(num(target.fearTimer), 0.8); target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.62); - target.setActionState?.("panic", { target: null, reason: "感電している", wake: true, sleeping: false }); + target.setActionState?.("panic", { target: null, reason: "\u611f\u96fb\u3057\u3066\u3044\u308b", wake: true, sleeping: false }); } emitEffect(worldRef, "electric_shock", target.x, target.y - Math.max(3, num(target.radius ?? target.r, 12) * 0.12), { size: Math.max(12, num(target.radius ?? target.r, 12) * 1.15), @@ -247,5 +285,7 @@ updateWorld, powerOverride, isWire, + isMinuteInRange, + currentMinuteOfDay, }); })(typeof window !== "undefined" ? window : globalThis); diff --git a/js/simulation_ambient_system.js b/js/simulation_ambient_system.js index 2b1dbe6..89930e7 100644 --- a/js/simulation_ambient_system.js +++ b/js/simulation_ambient_system.js @@ -38,7 +38,14 @@ // horizontal oval water items spawned by rain; it intentionally does not // share the rain overlay renderer. if (global.TarinaiWeatherSystem.shouldDropRainWater(worldRef, dt)) { - const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef); + const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef, { rainDrop: true }); + const sand = worldRef.toiletSandAt?.(drop.x, drop.y) || null; + if (sand) { + sand.toiletFlash = 1; + sand.awakeUntil = Math.max(sand.awakeUntil || 0, (worldRef.time || 0) + 0.8); + worldRef.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-rain-absorb"); + return; + } worldRef.addItem?.(drop, "rain-water"); } } diff --git a/js/simulation_maintenance_system.js b/js/simulation_maintenance_system.js index f8fb17d..8edbef3 100644 --- a/js/simulation_maintenance_system.js +++ b/js/simulation_maintenance_system.js @@ -5,6 +5,7 @@ // out-of-bounds cleanup, and draw-list invalidation. (function (global) { function runMaintenance(worldRef, dt, itemCountBefore) { + global.TarinaiStructureLifecycle?.maintainDependencies?.(worldRef); worldRef.compactTimer += dt; const compactInterval = 2.5; let itemsCompacted = false; diff --git a/js/snapshot_system.js b/js/snapshot_system.js index dcd5dea..53ddabf 100644 --- a/js/snapshot_system.js +++ b/js/snapshot_system.js @@ -241,7 +241,7 @@ const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger); const timers = [ q(t.reproductionTimer, 10), q(t.fightCooldown, 10), q(t.loveMochiTimer, 10), q(t.fightMochiTimer, 10), - q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100) + q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100), q(t.itemEffectTimers?.sedative, 10) ]; const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")]; const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1; @@ -301,7 +301,7 @@ const timers = Array.isArray(row[13]) ? row[13] : []; t.reproductionTimer = u(timers[0], 10); t.fightCooldown = u(timers[1], 10); t.loveMochiTimer = u(timers[2], 10); t.fightMochiTimer = u(timers[3], 10); t.zunchiDiseaseSeverity = u(timers[4], 10); t.zunchiStain = u(timers[5], 10); - t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10) }; + t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10), sedative: u(timers[9], 10) }; t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1); const modes = Array.isArray(row[14]) ? row[14] : []; t.powerItemMode = enumValue(POWER_MODE_IDS, modes[0], ""); @@ -346,7 +346,16 @@ q(item.fanSwingPhase || 0, 1000), q(item.fanBodyAngle ?? item.angle ?? 0, 1000), ]; - if (type === "pressure_switch") return [item.pressureTarget === "item" ? 1 : 0, Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0)), Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0)), Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0))]; + if (type === "pressure_switch") { + const targetCode = item.pressureTarget === "time" ? 2 : (item.pressureTarget === "item" ? 1 : 0); + const width = Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0)); + const height = Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0)); + const start = Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0)); + const end = Math.max(0, Math.min(1435, Number(item.pressureTimeEnd ?? 1080) | 0)); + const third = targetCode === 2 ? width * 1440 + start : width; + const fourth = targetCode === 2 ? height * 1440 + end : height; + return [targetCode, Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0)), third, fourth]; + } if (global.isRotatableItemType(type)) return [q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0]; if (global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)]; return []; @@ -435,10 +444,16 @@ item.fanSwingPhase = u(extra[5], 1000, item.fanSwingPhase || 0); item.fanBodyAngle = global.normalizedItemAngle(u(extra[6], 1000, item.angle), item.angle); } else if (type === "pressure_switch") { - item.pressureTarget = Number(extra[0]) === 1 ? "item" : "tarinai"; + const targetCode = Number(extra[0]) | 0; + const rawThird = Number(extra[2]); + const rawFourth = Number(extra[3]); + const packedTime = targetCode === 2 && rawThird >= 1440 && rawFourth >= 1440; + item.pressureTarget = targetCode === 2 ? "time" : (targetCode === 1 ? "item" : "tarinai"); item.pressureThreshold = Math.max(1, Math.min(999, Number(extra[1] || 1) | 0)); - item.pressureWidth = Math.max(60, Math.min(840, Number(extra[2] || 220) | 0)); - item.pressureHeight = Math.max(60, Math.min(840, Number(extra[3] || 160) | 0)); + item.pressureWidth = targetCode === 2 ? (packedTime ? Math.max(60, Math.min(840, Math.floor(rawThird / 1440))) : 220) : Math.max(60, Math.min(840, Number(extra[2] || 220) | 0)); + item.pressureHeight = targetCode === 2 ? (packedTime ? Math.max(60, Math.min(840, Math.floor(rawFourth / 1440))) : 160) : Math.max(60, Math.min(840, Number(extra[3] || 160) | 0)); + item.pressureTimeStart = targetCode === 2 ? Math.max(0, Math.min(1435, (packedTime ? rawThird % 1440 : rawThird) | 0)) : 360; + item.pressureTimeEnd = targetCode === 2 ? Math.max(0, Math.min(1435, (packedTime ? rawFourth % 1440 : rawFourth) | 0)) : 1080; item.signalActive = false; } else if (global.isRotatableItemType(type)) { const fallback = global.defaultItemAngle(type); diff --git a/js/structure_lifecycle.js b/js/structure_lifecycle.js index b9477e3..6223c0a 100644 --- a/js/structure_lifecycle.js +++ b/js/structure_lifecycle.js @@ -1,6 +1,12 @@ "use strict"; (function (global) { + const CONNECTION_ITEM_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]); + + function isLiveLooseItem(item) { + return Boolean(item && !item.dead && !(Number.isFinite(Number(item.amount)) && Number(item.amount) <= 0)); + } + function markWorldAfterStructureChange(worldRef, reason = "structure-change", item = null, radius = 40) { if (!worldRef) return; worldRef.drawListDirty = true; @@ -132,36 +138,143 @@ } - function removeDependentLinks(worldRef, target, reason = "support-removed") { - if (!worldRef || !target || !target.id) return 0; + function endpointTarget(worldRef, endpoint) { + if (!worldRef || !endpoint) return null; + const cached = endpoint._ref; + if (endpoint.kind === "tarinai") { + const token = endpoint.liveToken ?? null; + const found = worldRef.liveTarinaiById?.(endpoint.id, token) + || worldRef.tarinaiById?.get?.(endpoint.id) + || (worldRef.tarinai || []).find(t => t && !t.dead && t.id === endpoint.id) + || null; + if (!found || found.dead) return null; + if (token != null && Number(found.liveToken ?? token) !== Number(token)) return null; + return found; + } + if (endpoint.kind === "ant") { + const found = (worldRef.ants || []).find(ant => ant && !ant.dead && ant.id === endpoint.id) || null; + return found && !found.dead ? found : null; + } + let found = worldRef.itemById?.(endpoint.id) || null; + if (found) return isLiveLooseItem(found) ? found : null; + found = (worldRef.items || []).find(item => item && item.id === endpoint.id) || null; + if (found) return isLiveLooseItem(found) ? found : null; + if (cached && (!endpoint.id || cached.id === endpoint.id) && isLiveLooseItem(cached)) return cached; + return null; + } + + function connectionItems(worldRef) { + if (!worldRef) return []; + if (typeof worldRef.itemsOfType !== "function") return (worldRef.items || []).filter(item => isLiveLooseItem(item) && CONNECTION_ITEM_TYPES.has(item.type)); + const out = []; + for (const type of CONNECTION_ITEM_TYPES) { + for (const item of worldRef.itemsOfType(type) || []) if (isLiveLooseItem(item)) out.push(item); + } + return out; + } + + function removeOrphanedLinks(worldRef, reason = "link-endpoint-gone") { + if (!worldRef?.items) return 0; const pb = global.TarinaiPhysicsBodySystem || null; if (!pb?.endpoint) return 0; - let removed = 0; - const removedIds = new Set([target.id]); - let changed = true; - while (changed) { - changed = false; - for (const link of worldRef.items || []) { - if (!link || link.dead || !["rope", "rod", "spring", "wire", "insulated_wire"].includes(link.type)) continue; - const a = pb.endpoint(link, 0); - const b = pb.endpoint(link, 1); - const hit = (a && a.kind === "item" && removedIds.has(a.id)) || (b && b.kind === "item" && removedIds.has(b.id)); - if (!hit) continue; - link.amount = 0; - link.hp = 0; - removedIds.add(link.id); - removed += 1; - changed = true; + const links = connectionItems(worldRef); + const linksById = new Map(links.map(link => [link.id, link])); + const dependents = new Map(); + const queue = []; + const queued = new Set(); + const enqueue = link => { + if (!link || queued.has(link.id)) return; + queued.add(link.id); + queue.push(link); + }; + for (const link of links) { + let orphaned = false; + for (const endpoint of [pb.endpoint(link, 0), pb.endpoint(link, 1)]) { + const target = endpointTarget(worldRef, endpoint); + if (!target) { + orphaned = true; + continue; + } + if (endpoint?.kind === "item" && linksById.has(target.id)) { + if (!dependents.has(target.id)) dependents.set(target.id, []); + dependents.get(target.id).push(link); + } } + if (orphaned) enqueue(link); } - if (removed) { + let removed = 0; + let queueIndex = 0; + while (queueIndex < queue.length) { + const link = queue[queueIndex++]; + if (!isLiveLooseItem(link)) continue; + if (!markLooseItemGone(link, reason)) continue; + removed += 1; + for (const dependent of dependents.get(link.id) || []) enqueue(dependent); + } + let pendingCleared = false; + const pending = worldRef.pendingLinkEndpoint || null; + if (pending && !endpointTarget(worldRef, pending)) { + worldRef.pendingLinkEndpoint = null; + pendingCleared = true; + } + if (removed || pendingCleared) { worldRef.drawListDirty = true; - worldRef.markItemBucketsDirty?.("dependent-links-removed"); - worldRef.markSpatialDirty?.("dependent-links-removed"); + worldRef.markItemBucketsDirty?.(reason); + worldRef.markSpatialDirty?.(reason); } return removed; } + function reconcilePlushies(worldRef, reason = "plushie-attachment-reconcile") { + if (!worldRef?.items) return { restored: 0, removed: 0 }; + let restored = 0; + let removed = 0; + const source = worldRef.itemsOfType?.("plushie") || worldRef.items; + for (const plushie of source || []) { + if (!plushie || plushie.dead || !plushie.isStructure || plushie.type !== "plushie") continue; + if (plushie.playerHeld || plushie._heldByPlayer || Number(plushie.plushieFlingTimer || 0) > 0) continue; + const owner = worldRef.liveTarinaiById?.(plushie.ownerId) + || (worldRef.tarinai || []).find(t => t && !t.dead && t.id === plushie.ownerId) + || null; + if (!plushie.ownerId || !owner || owner.dead) { + if (markStructureGone(plushie, "plushie-owner-gone")) removed += 1; + plushie.carriedById = ""; + plushie.onHead = false; + continue; + } + if (plushie.carriedById === owner.id && plushie.onHead) continue; + const nextX = Number(owner.x || 0) || 0; + const nextY = (Number(owner.y || 0) || 0) - Math.max(22, (Number(owner.radius || 24) || 24) * 1.02); + plushie.ownerId = owner.id; + plushie.carriedById = owner.id; + plushie.onHead = true; + plushie.x = nextX; + plushie.y = nextY; + plushie.vx = 0; + plushie.vy = 0; + plushie.spinVelocity = 0; + restored += 1; + } + if (restored || removed) { + worldRef.drawListDirty = true; + worldRef.markItemBucketsDirty?.(reason); + worldRef.markSpatialDirty?.(reason); + } + return { restored, removed }; + } + + function maintainDependencies(worldRef) { + const linksRemoved = removeOrphanedLinks(worldRef, "link-endpoint-gone"); + const plushies = reconcilePlushies(worldRef, "plushie-attachment-reconcile"); + return { linksRemoved, plushiesRestored: plushies.restored, plushiesRemoved: plushies.removed }; + } + + + function removeDependentLinks(worldRef, target, reason = "support-removed") { + if (!worldRef || !target || !target.id) return 0; + return removeOrphanedLinks(worldRef, reason); + } + function deleteStructure(worldRef, structure, opts = {}) { if (!structure || structure.dead) return false; const reason = opts.reason || "structure-deleted"; @@ -200,6 +313,9 @@ const api = Object.freeze({ consumeGrassBedOnWake, deleteItem, + maintainDependencies, + reconcilePlushies, + removeOrphanedLinks, triggerOwnedStructureDestroyedPanic, }); global.TarinaiStructureLifecycle = api; diff --git a/js/structures.js b/js/structures.js index 2e6940d..86db6c9 100644 --- a/js/structures.js +++ b/js/structures.js @@ -71,7 +71,7 @@ this.dead = true; this.hp = 0; this.amount = 0; - global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, { type: "fling_out", name: "画面外" }); + global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, { type: "fling_out", name: "\u753b\u9762\u5916" }); global.TarinaiStructureLifecycle?.triggerOwnedStructureDestroyedPanic?.(worldRef, this, null, "plushie-fling-out"); worldRef?.markItemBucketsDirty?.("plushie-fling-out"); worldRef?.markSpatialDirty?.("plushie-fling-out"); diff --git a/js/tarinai.js b/js/tarinai.js index 4dbf71f..8f389e6 100644 --- a/js/tarinai.js +++ b/js/tarinai.js @@ -225,6 +225,7 @@ class Tarinai { constructor(world, opts = {}) { this.itemEffectTimers = { laxative: Math.max(0, Number(effectTimers.laxative || opts.laxativeTimer || 0) || 0), ammo: Math.max(0, Number(effectTimers.ammo || opts.ammoTimer || 0) || 0), + sedative: Math.max(0, Number(effectTimers.sedative || opts.sedativeTimer || 0) || 0), }; this.laxativePoopTimer = Number.isFinite(opts.laxativePoopTimer) ? opts.laxativePoopTimer : 3; this.ammoBumpCooldown = opts.ammoBumpCooldown || 0; diff --git a/js/tarinai_action_definitions.js b/js/tarinai_action_definitions.js index 6ec0664..114af64 100644 --- a/js/tarinai_action_definitions.js +++ b/js/tarinai_action_definitions.js @@ -253,7 +253,7 @@ function createTemperatureComfortActionSpec() { : ((target.type === "nest_box" || target.type === "pipe") ? target : null); if (shelter && (shelter.type === "nest_box" || shelter.type === "pipe")) { if (typeof t.enterNestBox === "function" && t.enterNestBox(shelter, dt, { purpose: "temperature" })) { - t.thought = shelter.type === "pipe" ? "土管の中で気温をしのいでいる" : "巣箱の中で気温をしのいでいる"; + t.thought = shelter.type === "pipe" ? "\u571f\u7ba1\u306e\u4e2d\u3067\u6c17\u6e29\u3092\u3057\u306e\u3044\u3067\u3044\u308b" : "\u5de3\u7bb1\u306e\u4e2d\u3067\u6c17\u6e29\u3092\u3057\u306e\u3044\u3067\u3044\u308b"; return true; } } diff --git a/js/tarinai_behavior_text.js b/js/tarinai_behavior_text.js index b5392bf..0dee3cc 100644 --- a/js/tarinai_behavior_text.js +++ b/js/tarinai_behavior_text.js @@ -102,7 +102,7 @@ function causePhraseForReason(chosenNeed, action, tarinai = null, options = {}) function buildReasonText(chosenNeed, tiedNeeds, action, tarinai = null, world = null, options = {}) { if (action?.id === "sunbath" && (options?.leisure || tarinai?.sunbathLeisure || currentTarinaiBehavior(tarinai)?.data?.leisure)) { - return "気分がいいので、日光浴している。"; + return "\u6c17\u5206\u304c\u3044\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002"; } if (action?.id === "seek_comfort_temperature") { const felt = tarinai?.world?.feltTemperatureFor?.(tarinai) ?? world?.temperatureAt?.(tarinai?.x, tarinai?.y, tarinai) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15); diff --git a/js/tarinai_consumable_behavior.js b/js/tarinai_consumable_behavior.js index ccfb432..79bd3dd 100644 --- a/js/tarinai_consumable_behavior.js +++ b/js/tarinai_consumable_behavior.js @@ -10,7 +10,7 @@ function finishBehaviorConsumption(t, item, foodType, eating, dt, label, color = t.eatTimer = Math.max(t.eatTimer || 0, 1.0); t.eatCooldown = rand(0.96, 1.12); t.foodReactTimer = Math.max(t.foodReactTimer || 0, 0.16); - if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { food: Math.max(18, Math.min(54, (Number(eating) || 0) * 1.1)) }, "meal"); + if (foodType !== "first_aid" && typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { food: Math.max(18, Math.min(54, (Number(eating) || 0) * 1.1)) }, "meal"); t.lastMealColor = color; if (Math.random() < Math.max(0.1, dt || 0.1) * 2.2) { const mouth = t.mouthPosition?.(item) || { x: t.x, y: t.y }; @@ -77,13 +77,16 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { const eating = t.consumeFoodServing?.(target); if (!(eating > 0)) return false; const isParam = isParamEffectItemType(foodType); + const hpOnlyMedicine = foodType === "first_aid"; const isSpecial = foodType === "love_mochi" || foodType === "fight_mochi" || foodType === "sleep_drug" || isParam; - const color = ({ sweet: "#78b957", love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff", protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b", mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f" })[foodType] || "#f1dfb7"; + const color = ({ sweet: "#78b957", love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff", first_aid: "#d24852", sedative: "#5c8eae", protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b", mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f" })[foodType] || "#f1dfb7"; finishBehaviorConsumption(t, target, foodType, eating, effectiveDt, `${labelFor(foodType)}\u3092\u98df\u3079\u3066\u3044\u308b`, color); - const hungerRelief = t.foodHungerReliefFor?.(foodType || target.type, eating, foodType === "sweet" ? 0.75 : (isSpecial ? 0.82 : 0.95)) ?? eating * 0.8; - t.applyFoodHungerRelief ? t.applyFoodHungerRelief(hungerRelief) : (t.hunger -= hungerRelief); - t.recoverHealth?.(eating * (foodType === "sweet" ? 0.28 : (isSpecial ? 0.24 : 0.46))); - t.affection += foodType === "sweet" ? eating * 0.03 : eating * 0.012; + if (!hpOnlyMedicine) { + const hungerRelief = t.foodHungerReliefFor?.(foodType || target.type, eating, foodType === "sweet" ? 0.75 : (isSpecial ? 0.82 : 0.95)) ?? eating * 0.8; + t.applyFoodHungerRelief ? t.applyFoodHungerRelief(hungerRelief) : (t.hunger -= hungerRelief); + t.recoverHealth?.(eating * (foodType === "sweet" ? 0.28 : (isSpecial ? 0.24 : 0.46))); + t.affection += foodType === "sweet" ? eating * 0.03 : eating * 0.012; + } if (isSpecial && t.applyParamItemEffect) t.applyParamItemEffect(foodType || target.type, target, { source: "eating", nutrition: eating, silentLog: foodType === "love_mochi" || foodType === "fight_mochi" || foodType === "sleep_drug" }); if (foodType === "sweet") { applyNeedRelief(t, { food: -eating * 0.4, fulfill: -eating * 0.2 }); @@ -91,7 +94,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { if (t.fightDisease) t.recoverFightDisease?.("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f"); } const fromDuplicator = isDuplicatorFoodSource(target); - t.makePoop?.(normalPoopUnitsFor(foodType || target.type, eating)); + if (!hpOnlyMedicine) t.makePoop?.(normalPoopUnitsFor(foodType || target.type, eating)); if (fromDuplicator) { t.mealCooldownUntil = Math.max(t.mealCooldownUntil || 0, (world.time || 0) + 1.2); t.behaviorLockTimer = Math.max(0, Math.min(t.behaviorLockTimer || 0, 0.18)); diff --git a/js/tarinai_direct_feeding_system.js b/js/tarinai_direct_feeding_system.js new file mode 100644 index 0000000..02f23af --- /dev/null +++ b/js/tarinai_direct_feeding_system.js @@ -0,0 +1,162 @@ +"use strict"; + +// Direct player-to-Tarinai feeding. Placement clicks and pinched-item drops +// share this path so item effects, feedback, and consumption stay consistent. +(function (global) { + const FOOD_TOOL_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice"]); + const MEDICINE_TOOL_TYPES = new Set(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]); + const HEART_FOOD_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunda_juice"]); + + function itemType(itemOrType = "") { + return String(typeof itemOrType === "object" ? itemOrType?.type || "" : itemOrType || ""); + } + + function isDirectFeedType(itemOrType = "") { + const type = itemType(itemOrType); + return FOOD_TOOL_TYPES.has(type) || MEDICINE_TOOL_TYPES.has(type); + } + + function isFoodGift(itemOrType = "") { + return HEART_FOOD_TYPES.has(itemType(itemOrType)); + } + + function hasConsumableAmount(item = null) { + if (!item || item.dead || !isDirectFeedType(item)) return false; + const type = itemType(item); + if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) { + if (Number.isFinite(Number(item.foodServingsRemaining))) return Number(item.foodServingsRemaining) > 0; + return Number(item.amount ?? 1) > 0; + } + return Number(item.amount ?? 1) > 0; + } + + function findTargetAt(worldRef, x, y) { + if (!worldRef || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) return null; + const px = Number(x); + const py = Number(y); + const list = worldRef.tarinai || []; + for (let i = list.length - 1; i >= 0; i -= 1) { + const t = list[i]; + if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue; + const hit = typeof t.contains === "function" + ? t.contains(px, py) + : (typeof global.distXY === "function" && global.distXY(px, py, t.x, t.y) <= Math.max(20, Number(t.radius || 22) * 1.35)); + if (hit) return t; + } + return null; + } + + function roleFor(item = null) { + const type = itemType(item); + if (type === "water") return "drink"; + if (MEDICINE_TOOL_TYPES.has(type) || type === "zunda_juice") return "medicine"; + return "food"; + } + + function spawnFoodGiftHearts(tarinai, nutrition = 0) { + const worldRef = tarinai?.world; + if (!worldRef?.effects || typeof global.TarinaiEffect !== "function") return false; + const count = 10 + Math.min(4, Math.max(0, Math.round((Number(nutrition) || 0) / 7))); + const radius = Math.max(18, Number(tarinai.radius || 22) || 22); + for (let i = 0; i < count; i += 1) { + const side = i % 2 ? 1 : -1; + worldRef.effects.push(new global.TarinaiEffect("heart", + tarinai.x + side * global.rand(radius * 0.06, radius * 0.82), + tarinai.y - global.rand(radius * 0.38, radius * 1.72), { + vx: side * global.rand(9, 30) + global.rand(-9, 9), + vy: global.rand(-76, -30), + size: global.rand(9, 17), + life: global.rand(0.82, 1.36), + color: "rgba(240,91,135,0.94)", + })); + } + worldRef.effects.push(new global.TarinaiEffect("ring", tarinai.x, tarinai.y, { + size: Math.max(24, radius * 1.35), life: 0.28, color: "rgba(240,91,135,0.42)", + })); + return true; + } + + function applyFoodGiftPersonality(tarinai, nutrition = 0) { + if (!tarinai || typeof tarinai.adjustPersonality !== "function") return false; + const amount = Math.max(0, Number(nutrition) || 0); + // A single gift intentionally outweighs ordinary pointer contact, which + // changes openness gradually per second while the cursor remains nearby. + const openness = 0.018 + Math.min(0.010, amount * 0.0004); + const sociability = 0.010 + Math.min(0.006, amount * 0.00024); + const changedOpen = tarinai.adjustPersonality("openness", openness, "\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f") || 0; + const changedSocial = tarinai.adjustPersonality("sociability", sociability, "\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f") || 0; + return Math.abs(changedOpen) + Math.abs(changedSocial) > 0; + } + + function removeGivenWorldItem(worldRef, item, label) { + if (!worldRef || !item || !(worldRef.items || []).includes(item)) return false; + if (global.TarinaiStructureLifecycle?.deleteItem) { + return Boolean(global.TarinaiStructureLifecycle.deleteItem(worldRef, item, { + reason: "direct-feed-consumed", + userReason: `${label}\u304c\u76f4\u63a5\u4e0e\u3048\u3089\u308c\u305f`, + wake: false, + panicOwner: false, + })); + } + item.amount = 0; + item.dead = true; + worldRef.markItemBucketsDirty?.("direct-feed-consumed"); + worldRef.markSpatialDirty?.("direct-feed-consumed"); + return true; + } + + function give(worldRef, tarinai, item, options = {}) { + if (!worldRef || !tarinai || tarinai.dead || !hasConsumableAmount(item)) return null; + if (typeof global.consumeBehaviorTarget !== "function") return null; + const type = itemType(item); + const label = typeof global.toolLabel === "function" ? global.toolLabel(type) : type; + const consumed = global.consumeBehaviorTarget(tarinai, worldRef, item, roleFor(item), 0.18); + if (!consumed) return null; + const nutrition = global.TarinaiItemRegistry?.food?.nutrition?.(type, 0) ?? 0; + + tarinai.target = null; + tarinai.targetKey = ""; + tarinai.sleeping = false; + tarinai.goodMode = "smile"; + tarinai.moodLiftTimer = Math.max(Number(tarinai.moodLiftTimer || 0) || 0, 1.2); + tarinai.foodReactTimer = Math.max(Number(tarinai.foodReactTimer || 0) || 0, 0.55); + tarinai.surpriseTimer = Math.max(Number(tarinai.surpriseTimer || 0) || 0, 0.28); + tarinai.thought = `${label}\u3092\u3082\u3089\u3063\u305f`; + + const foodGift = isFoodGift(type); + if (foodGift) { + tarinai.affection = Number(tarinai.affection || 0) + 1.8 + Math.max(0, Number(nutrition) || 0) * 0.035; + applyFoodGiftPersonality(tarinai, nutrition); + spawnFoodGiftHearts(tarinai, nutrition); + tarinai.bubble?.("\u2661", 2.2, "rgba(190,82,132,0.80)"); + } else { + tarinai.focusPulseTimer = Math.max(Number(tarinai.focusPulseTimer || 0) || 0, 0.72); + } + + const sourceLabel = options.source === "pinch" ? "\u624b\u6e21\u3057\u3067" : "\u76f4\u63a5"; + worldRef.log?.(`${tarinai.name}\u306b${label}\u3092${sourceLabel}\u4e0e\u3048\u305f\u3002`, "food", { + participants: [tarinai], eventType: "direct_feed", sound: false, + }); + + const removed = options.removeItem !== false ? removeGivenWorldItem(worldRef, item, label) : false; + if (!removed && (worldRef.items || []).includes(item)) { + item.vx = 0; + item.vy = 0; + item.prevX = item.x; + item.prevY = item.y; + } + worldRef.hoverGiveTarget = null; + worldRef.drawListDirty = true; + worldRef.recordFamily?.(tarinai); + if (global.uiCache) global.uiCache.selectedSnapshot = ""; + return { tarinai, item, type, label, foodGift, removed }; + } + + global.TarinaiDirectFeedingSystem = Object.freeze({ + isDirectFeedType, + isFoodGift, + hasConsumableAmount, + findTargetAt, + give, + }); +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/tarinai_disease_nest.js b/js/tarinai_disease_nest.js index 11a2713..aa7ed2c 100644 --- a/js/tarinai_disease_nest.js +++ b/js/tarinai_disease_nest.js @@ -83,7 +83,8 @@ updateZunchiDisease(dt) { this.zunchiStain = clamp(this.zunchiStain || 0, 0, 100); this.zunchiDiseaseCooldown = Math.max(0, (this.zunchiDiseaseCooldown || 0) - dt); - if (this.world?.weather === "light_rain") { + const shelteredFromRain = globalThis.TarinaiRainShelterSystem?.isSheltered?.(this.world, this.x, this.y) || false; + if (this.world?.weather === "light_rain" && !shelteredFromRain) { this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100); if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006); } @@ -283,7 +284,7 @@ if (!isNestContainerItem(box) || this.dead) return false; const alreadyInside = this.insideNestBoxId === box.id; if (!alreadyInside && box?.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(box)) { - return this.abortNestBoxEntry?.(box, "巣箱が燃えていて入れない") || false; + return this.abortNestBoxEntry?.(box, "\u5de3\u7bb1\u304c\u71c3\u3048\u3066\u3044\u3066\u5165\u308c\u306a\u3044") || false; } const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5; let occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : []; @@ -629,19 +630,19 @@ const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, currentNest.x, currentNest.y, Math.max(this.radius || 18, (currentNest.r || 42) * 1.15)); if (water) { globalThis.TarinaiItemDynamicToolSystem?.consumeWaterForFire?.(this.world, water, Math.max(10, (this.radius || 18) * 0.55)); - this.extinguishFire("水で消火された"); + this.extinguishFire("\u6c34\u3067\u6d88\u706b\u3055\u308c\u305f"); return; } const mul = globalThis.TarinaiItemDynamicToolSystem?.fireTempMultiplier?.(this.world, currentNest.x, currentNest.y, this) || 1; if (deterministicChance(this.world, "tarinai-nest-fire-extinguish", dt * 0.014 * mul, this, currentNest)) { - this.extinguishFire("自然に火が消えた"); + this.extinguishFire("\u81ea\u7136\u306b\u706b\u304c\u6d88\u3048\u305f"); return; } - if (this.burnTimer <= 0.01) this.extinguishFire("自然に火が消えた"); + if (this.burnTimer <= 0.01) this.extinguishFire("\u81ea\u7136\u306b\u706b\u304c\u6d88\u3048\u305f"); return; } if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 6.2, safety: dt * 3.0 }); - if (typeof this.damage === "function") this.damage(dt * 3.8, "炎上"); + if (typeof this.damage === "function") this.damage(dt * 3.8, "\u708e\u4e0a"); else this.energy = clamp((this.energy || 0) - dt * 0.12, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100)); this.hurtTimer = Math.max(this.hurtTimer || 0, 0.22); const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, this.x, this.y, Math.max(this.radius || 18, 16)); diff --git a/js/tarinai_food_interaction_system.js b/js/tarinai_food_interaction_system.js index c0cebef..d15a874 100644 --- a/js/tarinai_food_interaction_system.js +++ b/js/tarinai_food_interaction_system.js @@ -58,12 +58,16 @@ if (canSweetBite && (foodType === "love_mochi" || foodType === "fight_mochi" || tarinai.beginEating(it, `${eatLabel}\u3092\u98df\u3079\u3066\u3044\u308b`); tarinai.vx *= Math.pow(0.25, dt * 60); tarinai.vy *= Math.pow(0.25, dt * 60); - const hungerRelief = tarinai.foodHungerReliefFor(foodType || it.type, eating, 0.82); - tarinai.applyFoodHungerRelief ? tarinai.applyFoodHungerRelief(hungerRelief) : (tarinai.hunger -= hungerRelief); - tarinai.recoverHealth(eating * 0.24); - tarinai.affection += eating * 0.04; + const hpOnlyMedicine = foodType === "first_aid"; + if (!hpOnlyMedicine) { + const hungerRelief = tarinai.foodHungerReliefFor(foodType || it.type, eating, 0.82); + tarinai.applyFoodHungerRelief ? tarinai.applyFoodHungerRelief(hungerRelief) : (tarinai.hunger -= hungerRelief); + tarinai.recoverHealth(eating * 0.24); + tarinai.affection += eating * 0.04; + } const mealColors = { love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff", + first_aid: "#d24852", sedative: "#5c8eae", protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b", mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f", }; @@ -87,7 +91,7 @@ if (canSweetBite && (foodType === "love_mochi" || foodType === "fight_mochi" || tarinai.world.spawnEatEffect(mouth.x, mouth.y, foodType === "love_mochi" ? "#ff7bab" : (foodType === "fight_mochi" ? "#e07c43" : (foodType === "sleep_drug" ? "#b89cff" : (tarinai.lastMealColor || "#41aa66")))); } const fromDuplicator = isDuplicatorFoodSource(it); - tarinai.makePoop(normalPoopUnitsFor(foodType || it.type, eating)); + if (!hpOnlyMedicine) tarinai.makePoop(normalPoopUnitsFor(foodType || it.type, eating)); if (fromDuplicator) tarinai.mealCooldownUntil = Math.max(tarinai.mealCooldownUntil || 0, tarinai.world.time + 1.2); if (tarinai.world.time > tarinai.nextEatSound) { audio.eat(); tarinai.nextEatSound = tarinai.world.time + 0.42; } if (tarinai.world.time - tarinai.lastLog > 9 && Math.random() < 0.02) { diff --git a/js/tarinai_food_prototype_mixin.js b/js/tarinai_food_prototype_mixin.js index 24a508d..23deb7f 100644 --- a/js/tarinai_food_prototype_mixin.js +++ b/js/tarinai_food_prototype_mixin.js @@ -24,7 +24,7 @@ const max = foodServingsForSize(it.toolSize || "medium"); if (!Number.isFinite(it.foodServingsMax) || it.foodServingsMax <= 0) it.foodServingsMax = max; if (!Number.isFinite(it.foodServingsRemaining)) { - const original = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, laxative: 58, protein: 58, niteropu: 58, ammo: 48, mystery_drug: 52, mercury: 44, giant_drug: 52, dwarf_drug: 52, zunda_juice: 70 }[it.type] || 62; + const original = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, first_aid: 58, sedative: 58, laxative: 58, protein: 58, niteropu: 58, ammo: 48, mystery_drug: 52, mercury: 44, giant_drug: 52, dwarf_drug: 52, zunda_juice: 70 }[it.type] || 62; const ratio = clamp((it.amount ?? original) / original, 0.05, 1); it.foodServingsRemaining = Math.max(1, Math.ceil(it.foodServingsMax * ratio)); } diff --git a/js/tarinai_item_effects.js b/js/tarinai_item_effects.js index 7ef2dba..bbc4ee7 100644 --- a/js/tarinai_item_effects.js +++ b/js/tarinai_item_effects.js @@ -41,9 +41,10 @@ Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({ ensureItemEffectState() { - if (!this.itemEffectTimers || typeof this.itemEffectTimers !== "object") this.itemEffectTimers = { laxative: 0, ammo: 0 }; + if (!this.itemEffectTimers || typeof this.itemEffectTimers !== "object") this.itemEffectTimers = { laxative: 0, ammo: 0, sedative: 0 }; if (!Number.isFinite(this.itemEffectTimers.laxative)) this.itemEffectTimers.laxative = 0; if (!Number.isFinite(this.itemEffectTimers.ammo)) this.itemEffectTimers.ammo = 0; + if (!Number.isFinite(this.itemEffectTimers.sedative)) this.itemEffectTimers.sedative = 0; if (!Number.isFinite(this.laxativePoopTimer)) this.laxativePoopTimer = 3; if (!Number.isFinite(this.ammoBumpCooldown)) this.ammoBumpCooldown = 0; if (this.powerItemMode !== "protein" && this.powerItemMode !== "niteropu") this.powerItemMode = ""; @@ -223,6 +224,7 @@ let value = this.hasItemEffect("ammo") ? (EFFECT_REGISTRY?.modifier?.("ammo", "speedMultiplier", 10) ?? 10) : 1; if (this.sizeItemMode === "giant_drug") value *= EFFECT_REGISTRY?.modifier?.("giant_drug", "speedMultiplier", 1.5) ?? 1.5; if (this.sizeItemMode === "dwarf_drug") value *= EFFECT_REGISTRY?.modifier?.("dwarf_drug", "speedMultiplier", 0.5) ?? 0.5; + if (this.hasItemEffect("sedative")) value *= EFFECT_REGISTRY?.modifier?.("sedative", "speedMultiplier", 0.72) ?? 0.72; return value; }, @@ -278,6 +280,7 @@ if (Math.random() < step * 0.72) this.spawnPowerItemEffect(this.powerItemMode, this.powerItemMode === "protein" ? 0.32 : 0.28); } if (this.hasItemEffect("ammo") && Math.random() < step * 3.0) this.spawnPowerItemEffect("ammo", 0.32); + if (this.hasItemEffect("sedative") && Math.random() < step * 0.9) this.spawnPowerItemEffect("sedative", 0.22); if ((this.sizeItemMode === "giant_drug" || this.sizeItemMode === "dwarf_drug") && Math.random() < step * 1.4) this.spawnPowerItemEffect(this.sizeItemMode, 0.24); }, @@ -291,8 +294,8 @@ const side = this.facingDir ? this.facingDir() : (this.facing || 1); const x = this.x - side * this.radius * rand(0.72, 0.96); const y = this.y + this.radius * rand(0.25, 0.48); - this.world.spawnZunchi?.(x, y); this.poopCount = (this.poopCount || 0) + 1; + this.world.spawnZunchi?.(x, y); this.digest = Math.max(0, (this.digest || 0) - 3.0); const now = this.world?.time || 0; this.bubble("\u3076\u308a\u3085\u3063", 1.2, effectColor("laxative"), { force: true }); diff --git a/js/tarinai_item_targeting.js b/js/tarinai_item_targeting.js index ad3671e..2fa3f28 100644 --- a/js/tarinai_item_targeting.js +++ b/js/tarinai_item_targeting.js @@ -15,7 +15,7 @@ const NORMAL_POOP_MEAL_THRESHOLD = 4; function normalPoopUnitsFor(foodType = "", amount = 0, options = {}) { const type = String(foodType || ""); - if (type === "laxative" || options.laxative === true) return 0; + if (type === "laxative" || type === "first_aid" || options.laxative === true) return 0; if (isServingFoodType(type)) return 1; const value = Math.max(0, Number(amount) || 0); return value / 18; @@ -171,9 +171,9 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) { }; if (Array.isArray(primary)) for (const it of primary) addCandidate(it); - // 食料・水・薬は、生成直後/移動直後の spatial cache 漏れにも耐える。 - // item type buckets が使える場合は role ごとの型だけを見ることで、 - // crowded world で毎回 world.items 全体を足す経路を避ける。 + // \u98df\u6599\u30fb\u6c34\u30fb\u85ac\u306f\u3001\u751f\u6210\u76f4\u5f8c/\u79fb\u52d5\u76f4\u5f8c\u306e spatial cache \u6f0f\u308c\u306b\u3082\u8010\u3048\u308b\u3002 + // item type buckets \u304c\u4f7f\u3048\u308b\u5834\u5408\u306f role \u3054\u3068\u306e\u578b\u3060\u3051\u3092\u898b\u308b\u3053\u3068\u3067\u3001 + // crowded world \u3067\u6bce\u56de world.items \u5168\u4f53\u3092\u8db3\u3059\u7d4c\u8def\u3092\u907f\u3051\u308b\u3002 if (foodLikeRole) { if (world?.itemsOfType) { for (const type of foodRoleSearchTypes(role)) for (const it of world.itemsOfType(type) || []) addCandidate(it); diff --git a/js/tarinai_local_environment_system.js b/js/tarinai_local_environment_system.js index 801b57c..2cdc3cd 100644 --- a/js/tarinai_local_environment_system.js +++ b/js/tarinai_local_environment_system.js @@ -3,6 +3,53 @@ // Layer: entity-runtime/tarinai/update-system // Owns local family/friend/grass/nest comfort environment effects. (function (global) { + function parasolFootprint(item = null) { + if (!item || item.dead || item.type !== "rain_shelter") return null; + const r = Math.max(30, Number(item.r || 50) || 50); + // The pole foot is the visual and physical center of the shade. + return { + centerX: Number(item.x || 0), + centerY: Number(item.y || 0) + r * 0.84, + radiusX: r * 1.58, + radiusY: r * 0.68, + }; + } + + function parasolRange(item = null) { + return parasolFootprint(item)?.radiusX || 0; + } + + function parasolContains(item = null, x = 0, y = 0) { + const footprint = parasolFootprint(item); + if (!footprint) return false; + const nx = (Number(x || 0) - footprint.centerX) / Math.max(1, footprint.radiusX); + const ny = (Number(y || 0) - footprint.centerY) / Math.max(1, footprint.radiusY); + return nx * nx + ny * ny <= 1; + } + + function parasolAt(worldRef, x, y) { + if (!worldRef) return null; + const candidates = typeof worldRef.itemsOfType === "function" + ? worldRef.itemsOfType("rain_shelter") + : (worldRef.nearbyItems?.(x, y, 240) || []); + let best = null; + let bestRatio = Infinity; + for (const item of candidates || []) { + const footprint = parasolFootprint(item); + if (!footprint) continue; + const nx = (Number(x || 0) - footprint.centerX) / Math.max(1, footprint.radiusX); + const ny = (Number(y || 0) - footprint.centerY) / Math.max(1, footprint.radiusY); + const ratio = Math.sqrt(nx * nx + ny * ny); + if (ratio <= 1 && ratio < bestRatio) { best = item; bestRatio = ratio; } + } + return best; + } + + function shelterRange(item = null) { return parasolRange(item); } + function shelterAt(worldRef, x, y) { return parasolAt(worldRef, x, y); } + function isSheltered(worldRef, x, y) { return Boolean(parasolAt(worldRef, x, y)); } + function blocksWeatherAt(worldRef, x, y) { return Boolean(parasolAt(worldRef, x, y)); } + function updateBody(dt) { const parent = this.parentToFollow ? this.parentToFollow() : null; if (parent && dist(this, parent) < 96) { @@ -30,7 +77,17 @@ if (!it || it.dead || it.type !== "nest_box") continue; nestComfort += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 120, 0, 1); } + let rainShelterComfort = 0; + const rainShelter = parasolAt(this.world, this.x, this.y); + if (rainShelter && (this.world?.weather || "") === "light_rain") { + const range = parasolRange(rainShelter); + rainShelterComfort = clamp(1 - distXY(this.x, this.y, rainShelter.x, rainShelter.y) / range, 0.25, 1); + } if (nestComfort > 0) applyNeedRelief(this, { sleep: -dt * Math.min(4, nestComfort * 3), safety: -dt * Math.min(5, nestComfort * 4) }); + if (rainShelterComfort > 0) { + applyNeedRelief(this, { safety: -dt * Math.min(6, rainShelterComfort * 5), health: -dt * Math.min(3, rainShelterComfort * 2), fulfill: -dt * Math.min(2, rainShelterComfort * 1.2) }); + if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.35, rainShelterComfort * 0.08)); + } if (grassComfort > 0) { 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)); @@ -45,6 +102,9 @@ return true; } + const parasolSystem = Object.freeze({ parasolFootprint, parasolRange, parasolContains, parasolAt, shelterRange, shelterAt, isSheltered, blocksWeatherAt }); + global.TarinaiParasolSystem = parasolSystem; + global.TarinaiRainShelterSystem = parasolSystem; global.TarinaiLocalEnvironmentSystem = Object.freeze({ updateOne, }); diff --git a/js/tarinai_needs_core.js b/js/tarinai_needs_core.js index ac6ea77..1f58f35 100644 --- a/js/tarinai_needs_core.js +++ b/js/tarinai_needs_core.js @@ -3,7 +3,7 @@ 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", + food: "\u98df\u6b32", sleep: "\u7761\u7720", health: "\u5065\u5eb7", safety: "\u5b89\u5168", diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index 52eca7b..91a8457 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -579,11 +579,11 @@ function shouldWakeFromSleep(tarinai, world, needs) { const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now)); const minSleepDuration = Math.max(2, Number(session.minDuration) || 8); - // 明確な妨害だけは最低睡眠時間より優先して起床させる。 + // \u660e\u78ba\u306a\u59a8\u5bb3\u3060\u3051\u306f\u6700\u4f4e\u7761\u7720\u6642\u9593\u3088\u308a\u512a\u5148\u3057\u3066\u8d77\u5e8a\u3055\u305b\u308b\u3002 if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true; if (findNearbyDanger(world, tarinai, 160)) return true; - // 空腹・睡眠欲の回復・体力回復などの通常要因では、最低2時間は寝続ける。 + // \u7a7a\u8179\u30fb\u7761\u7720\u6b32\u306e\u56de\u5fa9\u30fb\u4f53\u529b\u56de\u5fa9\u306a\u3069\u306e\u901a\u5e38\u8981\u56e0\u3067\u306f\u3001\u6700\u4f4e2\u6642\u9593\u306f\u5bdd\u7d9a\u3051\u308b\u3002 if (elapsed < minSleepDuration) return false; const foodNeed = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0; @@ -642,6 +642,16 @@ function resolveNeedsCore(dt) { const beforeNeeds = this.needs ? { ...this.needs } : createDefaultNeeds(); const needs = updateNeedsCached(this, this.world, dt); synchronizeActionTextFromState(this); + if (this.state === "seek_toilet" || this.toiletUrgeTargetId) { + this.toiletUrgeTargetId = ""; + this.toiletUrgeStartedAt = 0; + this.toiletUrgeDeadline = 0; + if (this.state === "seek_toilet") { + this.target = null; + this.behaviorLockTimer = 0; + this.goIdle?.("\u5f85\u3063\u3066\u3044\u308b"); + } + } if (processForcedBehaviorQueue(this, this.world, needs)) return; if (protectSleepSession(this, this.world, needs)) return; if (continueBuildPlan(this, this.world, dt)) return; @@ -1007,6 +1017,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 parasolSystem = globalThis.TarinaiParasolSystem || globalThis.TarinaiRainShelterSystem; + if (parasolSystem?.blocksWeatherAt?.(this.world, this.x, this.y)) 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; @@ -1159,28 +1171,34 @@ function resolveNeedsCore(dt) { return global.TarinaiNeedPlannerSystem.updateOne(this, dt); }, + dropPoopNow(source = "normal") { + const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3; + if ((Number(this.digest) || 0) + 1e-6 < threshold) return false; + this.digest = Math.max(0, (Number(this.digest) || 0) - threshold); + this.poopCount = (this.poopCount || 0) + 1; + const side = this.facingDir(); + const backX = this.x - side * this.radius * rand(0.72, 0.96); + const backY = this.y + this.radius * rand(0.25, 0.48); + this.world.spawnZunchi(backX, backY, this); + this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true }); + if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] }); + return true; + }, + makePoop(force = 1) { const add = Math.max(0, Number(force) || 0); if (add <= 0) return false; const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3; this.digest = Math.max(0, Number(this.digest) || 0) + add; if (this.digest + 1e-6 < threshold) return false; - this.digest = Math.max(0, this.digest - threshold); if (this.hasLodgedPinEffect?.("blocksZunchi")) { + this.digest = Math.max(0, this.digest - threshold); this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1); 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 false; } - this.poopCount = (this.poopCount || 0) + 1; - const side = this.facingDir(); - const backX = this.x - side * this.radius * rand(0.72, 0.96); - const backY = this.y + this.radius * rand(0.25, 0.48); - this.world.spawnZunchi(backX, backY, this); - const now = this.world?.time || 0; - this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true }); - if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] }); - return true; + return this.dropPoopNow?.("normal") || false; }, interactWithItems(dt) { diff --git a/js/tarinai_social_action_runtime.js b/js/tarinai_social_action_runtime.js index a806a6e..d86776b 100644 --- a/js/tarinai_social_action_runtime.js +++ b/js/tarinai_social_action_runtime.js @@ -78,6 +78,7 @@ function socialFightMateWeights(t, other, world, options = {}) { if (t.type === "angry" || other.type === "angry") fightWeight *= 1.42; if (t.isZunchiSlave || other.isZunchiSlave) fightWeight *= 1.22; if (fightMochi) fightWeight *= 3.4; + if (t.hasItemEffect?.("sedative") || other.hasItemEffect?.("sedative")) fightWeight *= 0.28; 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); diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index a063972..1166598 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -350,7 +350,7 @@ const step = Math.min(maxRestore, dt * restore); const sx = dx / d * step; const sy = dy / d * step; - if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, sx, sy, { dt, reason: externalActive ? "tarinai-sleep-restore-weak" : "tarinai-sleep-move" }); + if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, sx, sy, { dt }); else { this.x += sx; this.y += sy; } } else if (!externalActive && Math.hypot((this.vx || 0) + (this.impulseVx || 0), (this.vy || 0) + (this.impulseVy || 0)) < 0.35) { this.x = spot.x; @@ -363,13 +363,13 @@ const moveDx = ((Number.isFinite(this.vx) ? this.vx : 0) + impulseVx) * dt; const moveDy = ((Number.isFinite(this.vy) ? this.vy : 0) + impulseVy) * dt; if (Math.hypot(moveDx, moveDy) > 0.001) { - if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, moveDx, moveDy, { dt, reason: "tarinai-sleep-physical-full", maxChecks: 18, maxStep: 10 }); + if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, moveDx, moveDy, { dt, maxChecks: 18, maxStep: 10 }); else { this.x += moveDx; this.y += moveDy; } } if (this.world?.resolveSolidObstacleCollision) { const beforeContactX = Number(this.x || 0) || 0; const beforeContactY = Number(this.y || 0) || 0; - const pushed = this.world.resolveSolidObstacleCollision(this, { maxPasses: 2, searchRadius: Math.max(72, (Number(this.radius) || 22) + 120), maxChecks: 18, slop: 0.28, reason: "tarinai-sleep-full-contact" }); + const pushed = this.world.resolveSolidObstacleCollision(this, { maxPasses: 2, searchRadius: Math.max(72, (Number(this.radius) || 22) + 120), maxChecks: 18, slop: 0.28 }); if (pushed) { this.lastSolidObstacleCollisionAt = this.world.time || 0; marker?.markSleepExternalMotion?.(this, dt, "sleep-full-contact"); @@ -615,8 +615,8 @@ if (this.world?.moveTarinaiWithCollision) { const motionOnlyPass = Boolean(this._motionOnlyPhysicsPass); this.world.moveTarinaiWithCollision(this, moveDx, moveDy, motionOnlyPass - ? { dt, reason: "tarinai-move-smooth", light: true, maxChecks: this.state === "panic" ? 18 : 14, maxStep: this.state === "panic" ? 12 : 14 } - : { dt, reason: "tarinai-move" }); + ? { dt, light: true, maxChecks: this.state === "panic" ? 18 : 14, maxStep: this.state === "panic" ? 12 : 14 } + : { dt }); } else { this.x += moveDx; @@ -675,19 +675,19 @@ checkLife(dt = 0.016) { let reason = ""; const activeDiseaseDeathReason = () => { - if (this.zunchiDisease) return "ずんち病"; - if (this.sleepDisease) return "ねむり病"; - if (this.explosionDisease) return "爆発病"; - if (this.fightDisease) return "きずつき病"; + if (this.zunchiDisease) return "\u305a\u3093\u3061\u75c5"; + if (this.sleepDisease) return "\u306d\u3080\u308a\u75c5"; + if (this.explosionDisease) return "\u7206\u767a\u75c5"; + if (this.fightDisease) return "\u304d\u305a\u3064\u304d\u75c5"; return ""; }; if (this.energy <= 0) { - reason = this.temperatureDeathReason?.() || activeDiseaseDeathReason() || this.dominantDeathCause("", { lowHealth: true }) || "事故"; + reason = this.temperatureDeathReason?.() || activeDiseaseDeathReason() || this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545"; } - else if (this.hunger >= 108) reason = "飢餓"; - else if (this.age >= this.lifeSpan) reason = "天寿を全うした"; + else if (this.hunger >= 108) reason = "\u98e2\u9913"; + else if (this.age >= this.lifeSpan) reason = "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f"; else if (this.stress >= 124) { - reason = activeDiseaseDeathReason() || this.dominantDeathCause("ストレス過多", { stressDeath: true }) || "ストレス過多"; + reason = activeDiseaseDeathReason() || this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a"; } if (reason) this.die(reason); }, diff --git a/js/tarinai_sunbath_system.js b/js/tarinai_sunbath_system.js index dcd1244..a517a8d 100644 --- a/js/tarinai_sunbath_system.js +++ b/js/tarinai_sunbath_system.js @@ -24,7 +24,9 @@ } this.energy = clamp((this.energy || 0) + dt * 0.03, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100)); 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.fightDisease || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0; + const parasolSystem = globalThis.TarinaiParasolSystem || globalThis.TarinaiRainShelterSystem; + const sunlightBlocked = parasolSystem?.blocksWeatherAt?.(this.world, this.x, this.y) || false; + if (!this.isSunbathTime() || sunlightBlocked || Number(felt) >= 22 || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.fightDisease || 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 a26d926..7ac891c 100644 --- a/js/tarinai_update_step_frame.js +++ b/js/tarinai_update_step_frame.js @@ -45,6 +45,12 @@ t.updateSpecialDiseases(dt); if (t.updateBurning) t.updateBurning(dt); if (t.updateItemEffects) t.updateItemEffects(dt); + if (t.hasItemEffect?.("sedative")) { + t.fearTimer = Math.max(0, (t.fearTimer || 0) - dt * 1.8); + t.intimidatedTimer = Math.max(0, (t.intimidatedTimer || 0) - dt * 1.4); + t.fightMochiTimer = Math.max(0, (t.fightMochiTimer || 0) - dt * 2.6); + if (typeof applyNeedRelief === "function") applyNeedRelief(t, { safety: -dt * 2.6, social: -dt * 0.5 }); + } if (t.dead) return { done: true }; t.loveMochiTimer = Math.max(0, (t.loveMochiTimer || 0) - dt); t.fightMochiTimer = Math.max(0, (t.fightMochiTimer || 0) - dt); @@ -56,7 +62,7 @@ t.intimidateTargetId = null; if (t.state === "intimidate" || t.state === "ant_intimidate") { t.spriteLockUntil = 0; - t.setActionState?.("idle", { target: null, reason: "威嚇を終えた", clearTarget: true, sleeping: false }); + t.setActionState?.("idle", { target: null, reason: "\u5a01\u5687\u3092\u7d42\u3048\u305f", clearTarget: true, sleeping: false }); } } if (t.birthRitualTimer > 0) { @@ -194,8 +200,11 @@ t.vx *= Math.pow(0.996, dt * 60); t.vy *= Math.pow(0.996, dt * 60); } else if (t.world.weather === "light_rain") { - t.energy += dt * 0.018; - applyNeedRelief(t, { health: -dt * 1, safety: -dt * 1 }); + const shelteredFromRain = global.TarinaiRainShelterSystem?.isSheltered?.(t.world, t.x, t.y) || false; + if (!shelteredFromRain) { + t.energy += dt * 0.018; + applyNeedRelief(t, { health: -dt * 1, safety: -dt * 1 }); + } } else if (t.world.weather === "sunny") { t.vx *= Math.pow(1.001, dt * 60); t.vy *= Math.pow(1.001, dt * 60); diff --git a/js/tarinai_update_step_movement.js b/js/tarinai_update_step_movement.js index b6b2469..579e649 100644 --- a/js/tarinai_update_step_movement.js +++ b/js/tarinai_update_step_movement.js @@ -158,7 +158,6 @@ if (world?.moveTarinaiWithCollision) { world.moveTarinaiWithCollision(t, moveX, moveY, { dt, - reason: state === "sleep" ? "tarinai-sleep-physical" : "tarinai-passive-physical", maxChecks: context.cadence?.lane === "sleep" ? 18 : 22, maxStep: 10, }); @@ -192,7 +191,6 @@ searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120), maxChecks: context.cadence?.lane === "sleep" ? 18 : 22, slop: 0.28, - reason: state === "eat" ? "tarinai-eat-passive-contact" : (state === "sleep" ? "tarinai-sleep-passive-contact" : "tarinai-locked-passive-contact"), }); if (pushed) { t.lastSolidObstacleCollisionAt = world.time || 0; diff --git a/js/ui_bind.js b/js/ui_bind.js index b69982c..881589f 100644 --- a/js/ui_bind.js +++ b/js/ui_bind.js @@ -357,9 +357,9 @@ function bindUI() { renderStats(); }); const toggleChartSeries = (target) => { - const icon = target?.closest?.("i[data-series-key]"); - if (!icon) return false; - const key = String(icon.dataset.seriesKey || ""); + const control = target?.closest?.("[data-series-key]"); + if (!control) return false; + const key = String(control.dataset.seriesKey || ""); if (!key) return false; if (!(uiCache.chartHiddenSeries instanceof Set)) uiCache.chartHiddenSeries = new Set(); if (uiCache.chartHiddenSeries.has(key)) uiCache.chartHiddenSeries.delete(key); @@ -370,10 +370,6 @@ function bindUI() { return true; }; ui.colonyChartLegend?.addEventListener("click", (e) => toggleChartSeries(e.target)); - ui.colonyChartLegend?.addEventListener("keydown", (e) => { - if (e.key !== "Enter" && e.key !== " ") return; - if (toggleChartSeries(e.target)) e.preventDefault(); - }); ui.logPushEnabled?.addEventListener("change", () => { uiCache.logPushEnabled = Boolean(ui.logPushEnabled.checked); audio.notify?.(); diff --git a/js/ui_charts.js b/js/ui_charts.js index 238bb98..b980aff 100644 --- a/js/ui_charts.js +++ b/js/ui_charts.js @@ -2,7 +2,7 @@ function colonyMoodTip() { const mood = world?.evaluateColonyMood?.(true) || world?.colonyMood || null; - return String(mood?.description || "大きな問題が少なく、落ち着いた状態です。"); + return String(mood?.description || "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002"); } function bindColonyMoodTooltip() { @@ -47,9 +47,9 @@ function renderStats() { setTextIfChanged(ui.statJuvenile, "juvenile", values.juvenile); setTextIfChanged(ui.statElder, "elder", values.elder); setTextIfChanged(ui.statDead, "dead", values.dead); - const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "安定" }; - setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "安定"); - const tempLabel = `${Math.round(temp)}℃`; + const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "\u5b89\u5b9a" }; + setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "\u5b89\u5b9a"); + const tempLabel = `${Math.round(temp)}\u2103`; setTextIfChanged(ui.temperatureValue, "temperature", tempLabel); if (ui.temperatureSlider) { ui.temperatureSlider.disabled = world.temperatureAuto !== false; @@ -67,7 +67,7 @@ function renderStats() { if (ui.tarinaiPopulationLimitInput && document.activeElement !== ui.tarinaiPopulationLimitInput) ui.tarinaiPopulationLimitInput.value = tarinaiLimit > 0 ? String(tarinaiLimit) : ""; if (ui.objectLimitInput && document.activeElement !== ui.objectLimitInput) ui.objectLimitInput.value = objectLimit > 0 ? String(objectLimit) : ""; } - setTextIfChanged(ui.colonyLimitStatus, "colonyLimitStatus", `たりない ${values.pop} / ${tarinaiLimit || "∞"} オブジェクト ${objectCount} / ${objectLimit || "∞"}`); + setTextIfChanged(ui.colonyLimitStatus, "colonyLimitStatus", `\u305f\u308a\u306a\u3044 ${values.pop} / ${tarinaiLimit || "\u221e"}\u3000\u30aa\u30d6\u30b8\u30a7\u30af\u30c8 ${objectCount} / ${objectLimit || "\u221e"}`); bindColonyMoodTooltip(); window.TarinaiTooltips.refresh(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood); window.TarinaiGroundUI.update(world); @@ -188,9 +188,9 @@ function chartSeasonDayLabelForTime(t = 0) { const dayLength = Math.max(1, Number(CONFIG.dayLength || 120) || 120); const dayNumber = Math.floor(Math.max(0, Number(t || 0) || 0) / dayLength) + 1; const seasonIndex = Math.floor(((dayNumber - 1) % 20) / 5); - const season = ["春", "夏", "秋", "冬"][seasonIndex] || "春"; + const season = ["\u6625", "\u590f", "\u79cb", "\u51ac"][seasonIndex] || "\u6625"; const seasonDay = ((dayNumber - 1) % 5) + 1; - return `${season}${seasonDay}日`; + return `${season}${seasonDay}\u65e5`; } function niceChartMax(raw = 0) { @@ -284,43 +284,43 @@ function drawColonyChart(values) { const cy = yFor(last, s).toFixed(1); return ``; }).join(""); - chart.innerHTML = ``; + chart.innerHTML = ``; if (ui.colonyChartLegend) ui.colonyChartLegend.innerHTML = allSeries.map(s => { const hidden = hiddenSeries.has(s.key); - return `${s.label} ${formatChartValue(s.key, rows[rows.length - 1] || values)}`; + return ``; }).join(""); } function chartSeries(mode = "population") { if (mode === "life") return [ - { key: "stress", label: "ストレス", color: "#d96262", percent: true }, - { key: "need_food", label: "摂餌", color: "#e4a33f", percent: true }, - { key: "need_sleep", label: "睡眠", color: "#6f8edc", percent: true }, - { key: "need_health", label: "健康", color: "#2e9d68", percent: true }, - { key: "need_safety", label: "安全", color: "#d65e46", percent: true }, - { key: "need_social", label: "関係", color: "#9b78d4", percent: true }, - { key: "need_fulfill", label: "充足", color: "#0072b2", percent: true }, + { key: "stress", label: "\u30b9\u30c8\u30ec\u30b9", color: "#d96262", percent: true }, + { key: "need_food", label: "\u98df\u6b32", color: "#e4a33f", percent: true }, + { key: "need_sleep", label: "\u7761\u7720", color: "#6f8edc", percent: true }, + { key: "need_health", label: "\u5065\u5eb7", color: "#2e9d68", percent: true }, + { key: "need_safety", label: "\u5b89\u5168", color: "#d65e46", percent: true }, + { key: "need_social", label: "\u95a2\u4fc2", color: "#9b78d4", percent: true }, + { key: "need_fulfill", label: "\u5145\u8db3", color: "#0072b2", percent: true }, ]; if (mode === "objects") return [ - { key: "obj_grass", label: "草", color: "#5b9d61" }, - { key: "obj_zunchi", label: "ずんち", color: "#6f5a2f" }, - { key: "obj_trace", label: "死骸", color: "#8a7054" }, - { key: "obj_grass_bed", label: "かんたんベッド", color: "#78ff45" }, - { key: "obj_water", label: "雨滴", color: "#5aa8d6" }, - { key: "temperature", label: "気温", color: "#c46f3c" }, + { key: "obj_grass", label: "\u8349", color: "#5b9d61" }, + { key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f5a2f" }, + { key: "obj_trace", label: "\u6b7b\u9ab8", color: "#8a7054" }, + { key: "obj_grass_bed", label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", color: "#78ff45" }, + { key: "obj_water", label: "\u96e8\u6ef4", color: "#5aa8d6" }, + { key: "temperature", label: "\u6c17\u6e29", color: "#c46f3c" }, ]; return [ - { key: "pop", label: "総数", color: "#5d8ee6" }, - { key: "sick", label: "病気", color: "#d96262" }, - { key: "juvenile", label: "幼体", color: "#78a642" }, - { key: "elder", label: "老体", color: "#8a7054" }, - { key: "births", label: "誕生数", color: "#d48ac2" }, - { key: "deaths", label: "死亡数", color: "#4d4d56" }, + { key: "pop", label: "\u7dcf\u6570", color: "#5d8ee6" }, + { key: "sick", label: "\u75c5\u6c17", color: "#d96262" }, + { key: "juvenile", label: "\u5e7c\u4f53", color: "#78a642" }, + { key: "elder", label: "\u8001\u4f53", color: "#8a7054" }, + { key: "births", label: "\u8a95\u751f\u6570", color: "#d48ac2" }, + { key: "deaths", label: "\u6b7b\u4ea1\u6570", color: "#4d4d56" }, ]; } function formatChartValue(key, values) { const v = values[key] ?? 0; - if (key === "temperature") return `${Math.round(Number(v) || 0)}℃`; + if (key === "temperature") return `${Math.round(Number(v) || 0)}\u2103`; return String(Math.round(v)); } diff --git a/js/ui_input_mouse.js b/js/ui_input_mouse.js index eb34f41..dbbd52c 100644 --- a/js/ui_input_mouse.js +++ b/js/ui_input_mouse.js @@ -45,6 +45,7 @@ world.pointer.motion = 0; world.hoverGrabTarget = null; world.hoverDeleteTarget = null; + world.hoverGiveTarget = null; }); return true; } diff --git a/js/ui_input_shared.js b/js/ui_input_shared.js index 62fdc72..e914cfc 100644 --- a/js/ui_input_shared.js +++ b/js/ui_input_shared.js @@ -121,6 +121,24 @@ return true; } + function directFeedTargetAt(p, item = null) { + const feeding = global.TarinaiDirectFeedingSystem; + if (!p?.inside || !feeding) return null; + const type = item?.type || (typeof global.toolItemType === "function" ? global.toolItemType(world.tool || "") : ""); + if (!feeding.isDirectFeedType?.(type)) return null; + if (item && !feeding.hasConsumableAmount?.(item)) return null; + return feeding.findTargetAt?.(world, p.x, p.y) || null; + } + + function setGiveHover(target = null) { + const next = target && !target.dead ? target : null; + if (world.hoverGiveTarget !== next) { + world.hoverGiveTarget = next; + world.drawListDirty = true; + } + return next; + } + function rememberGrabOrigin(target) { if (!target || uiCache.grabKind !== "tarinai") return; uiCache.grabOrigin = { x: target.x, y: target.y, vx: target.vx || 0, vy: target.vy || 0 }; @@ -208,6 +226,9 @@ } function moveGrab(p, clientX, clientY, options = {}) { + world.pointer.x = p?.x ?? world.pointer.x; + world.pointer.y = p?.y ?? world.pointer.y; + world.pointer.inside = Boolean(p?.inside); const target = uiCache.grabTarget; if (!target || target.dead) { uiCache.grabbing = false; @@ -266,6 +287,7 @@ target.prevY = target.y - dy; target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38); } + setGiveHover(uiCache.grabKind === "item" ? directFeedTargetAt(p, target) : null); world.drawListDirty = true; const now = performance.now(); if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) { @@ -280,6 +302,7 @@ function endGrab(clientX, clientY) { if (!uiCache.grabbing) return false; const target = uiCache.grabTarget; + let directlyGiven = false; if (target) { if (uiCache.grabKind === "tarinai") { target.target = null; @@ -287,28 +310,36 @@ target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15); finishTarinaiGrab(target, clientX, clientY); } else if (uiCache.grabKind === "item") { - const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900); - const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900); - const releaseSpeed = Math.hypot(releaseVx, releaseVy); - if (target.type === "plushie" && typeof target.fling === "function") { - const sourceX = Number(uiCache.grabLastWorldX ?? target.x) || target.x; - const sourceY = Number(uiCache.grabLastWorldY ?? target.y) || target.y; - const vx = releaseSpeed > 80 ? releaseVx : 0; - const vy = releaseSpeed > 80 ? releaseVy : 0; - target.fling(world, sourceX, sourceY, { source: "pinch", vx, vy, speed: Math.max(620, releaseSpeed * 1.05), life: 2.8 }); - } else if (releaseSpeed > 18) { - target.vx = releaseVx; - target.vy = releaseVy; - const frameDt = Math.max(1 / 90, Math.min(1 / 30, Number(world.dt || 1 / 60) || 1 / 60)); - target.prevX = target.x - releaseVx * frameDt; - target.prevY = target.y - releaseVy * frameDt; - if ((target.type === "ball" || target.type === "balloon")) target.spinVelocity = clamp((target.spinVelocity || 0) + releaseVx * 0.018, -42, 42); - } else if ((target.type === "ball" || target.type === "balloon")) { - target.prevX = target.x; - target.prevY = target.y; + const releasePoint = screenToWorldClient(clientX, clientY); + const giveTarget = directFeedTargetAt(releasePoint, target); + const result = giveTarget + ? global.TarinaiDirectFeedingSystem?.give?.(world, giveTarget, target, { source: "pinch" }) + : null; + directlyGiven = Boolean(result); + if (!directlyGiven) { + const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900); + const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900); + const releaseSpeed = Math.hypot(releaseVx, releaseVy); + if (target.type === "plushie" && typeof target.fling === "function") { + const sourceX = Number(uiCache.grabLastWorldX ?? target.x) || target.x; + const sourceY = Number(uiCache.grabLastWorldY ?? target.y) || target.y; + const vx = releaseSpeed > 80 ? releaseVx : 0; + const vy = releaseSpeed > 80 ? releaseVy : 0; + target.fling(world, sourceX, sourceY, { source: "pinch", vx, vy, speed: Math.max(620, releaseSpeed * 1.05), life: 2.8 }); + } else if (releaseSpeed > 18) { + target.vx = releaseVx; + target.vy = releaseVy; + const frameDt = Math.max(1 / 90, Math.min(1 / 30, Number(world.dt || 1 / 60) || 1 / 60)); + target.prevX = target.x - releaseVx * frameDt; + target.prevY = target.y - releaseVy * frameDt; + if ((target.type === "ball" || target.type === "balloon")) target.spinVelocity = clamp((target.spinVelocity || 0) + releaseVx * 0.018, -42, 42); + } else if ((target.type === "ball" || target.type === "balloon")) { + target.prevX = target.x; + target.prevY = target.y; + } } } - if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release"); + if (uiCache.grabKind === "item" && !directlyGiven && !target.dead) syncGrabbedPhysicsItem(target, "pinch-release"); target.playerHeld = false; target._heldByPlayer = false; if (uiCache.grabKind === "tarinai") { @@ -317,22 +348,23 @@ target.impulseVx = 0; target.impulseVy = 0; target.pinchThoughtTimer = 0; - if (target.thought === "つままれている") target.thought = ""; + if (target.thought === "\u3064\u307e\u307e\u308c\u3066\u3044\u308b") target.thought = ""; const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(target) : null; const reason = String(behavior?.reason || behavior?.text || behavior?.label || ""); - if (reason === "つままれている" && typeof clearBehavior === "function") clearBehavior(target); + if (reason === "\u3064\u307e\u307e\u308c\u3066\u3044\u308b" && typeof clearBehavior === "function") clearBehavior(target); } if (target.type === "duplicator") { target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.2); } } - audio.drop?.(); + if (!directlyGiven) audio.drop?.(); uiCache.grabbing = false; uiCache.grabTarget = null; uiCache.grabKind = ""; uiCache.grabVelocityX = 0; uiCache.grabVelocityY = 0; uiCache.grabLastMoveAt = 0; + setGiveHover(null); world.rebuildSpatial?.(); uiCache.grabLastSpatialAt = 0; canvas.style.cursor = cursorForTool(world.tool); @@ -537,7 +569,10 @@ world.pointer.inside = !!p.inside; const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0)); world.pointer.motion = clamp(delta / 18, 0, 2); + const inactivePointerAction = !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting; + const giveTarget = inactivePointerAction ? directFeedTargetAt(p) : null; if (world.tool === "shoot") { + setGiveHover(null); if (p.inside) { if (world.shootCursorActive) { const lastRawX = Number.isFinite(Number(world.shootPointerRawX)) ? Number(world.shootPointerRawX) : prevX; @@ -549,8 +584,14 @@ world.shootPointerRawX = p.x; world.shootPointerRawY = p.y; } - canvas.style.cursor = "none"; - } else if (world.tool === "pinch" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) { + canvas.style.cursor = "none"; + } else if (giveTarget) { + setGiveHover(giveTarget); + if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; } + if (world.hoverDeleteTarget) { world.hoverDeleteTarget = null; world.drawListDirty = true; } + canvas.style.cursor = "pointer"; + } else if (world.tool === "pinch" && p.inside && inactivePointerAction) { + setGiveHover(null); const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null; if (world.hoverGrabTarget !== target) { world.hoverGrabTarget = target || null; @@ -558,7 +599,8 @@ } if (world.hoverDeleteTarget) { world.hoverDeleteTarget = null; world.drawListDirty = true; } canvas.style.cursor = target ? "grab" : "default"; - } else if (world.tool === "delete" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) { + } else if (world.tool === "delete" && p.inside && inactivePointerAction) { + setGiveHover(null); const target = world.findDeleteToolTargetAt ? world.findDeleteToolTargetAt(p.x, p.y) : null; if (world.hoverDeleteTarget !== target) { world.hoverDeleteTarget = target || null; @@ -567,6 +609,7 @@ if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; } canvas.style.cursor = target ? "pointer" : "default"; } else { + setGiveHover(null); if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; @@ -596,6 +639,7 @@ if (world) { world.hoverGrabTarget = null; world.hoverDeleteTarget = null; + world.hoverGiveTarget = null; } } diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index 1bdc20c..fef2eeb 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -34,7 +34,7 @@ function applyFieldLayout(fieldType = world.fieldType || "garden") { function normalizeResetPresetId(presetId = "default") { if (window.TarinaiResetPresets?.normalize) return window.TarinaiResetPresets.normalize(presetId); - return new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy"]).has(String(presetId || "")) ? String(presetId) : "default"; + return new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy", "hair_trigger"]).has(String(presetId || "")) ? String(presetId) : "default"; } function syncFieldDialogChoices() { diff --git a/js/ui_log.js b/js/ui_log.js index 43b46bb..4c90e6e 100644 --- a/js/ui_log.js +++ b/js/ui_log.js @@ -88,7 +88,7 @@ function shouldShowInEventLog(entry = {}) { const type = String(entry.eventType || ""); const text = String(entry.text || ""); if (type === "player_item_placement" || type.includes("player") || type.includes("tool") || type.includes("robot")) return false; - if (/ロボ掃除機|掃除機/.test(text)) return false; + if (/\u30ed\u30dc\u6383\u9664\u6a5f|\u6383\u9664\u6a5f/.test(text)) return false; return true; } diff --git a/js/ui_selected.js b/js/ui_selected.js index 5537b98..b5616d0 100644 --- a/js/ui_selected.js +++ b/js/ui_selected.js @@ -333,7 +333,7 @@ function isIdleDisplayText(value = "") { function reasonLabel(t) { if (!t) return "\u306a\u3057"; - if (Number(t.electricShockUntil || 0) > Number(t.world?.time || world?.time || 0)) return "感電している。"; + if (Number(t.electricShockUntil || 0) > Number(t.world?.time || world?.time || 0)) return "\u611f\u96fb\u3057\u3066\u3044\u308b\u3002"; const behavior = behaviorText(t); if (behavior) { const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(behavior, t.state || "") || behavior; diff --git a/js/ui_tools.js b/js/ui_tools.js index 5f403ac..81b5d0e 100644 --- a/js/ui_tools.js +++ b/js/ui_tools.js @@ -1,7 +1,86 @@ "use strict"; const SCALABLE_TOOLS = new Set(scalableToolIds()); -const TEXT_ICON_TOOL_IDS = new Set(["poke", "pinch", "water_hose", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", "reciprocator", "copy", "undo", "redo", "area_delete", "clear_tool"]); +const PHYSICS_ICON_TOOL_IDS = new Set(window.TarinaiItemToolMetadata?.PHYSICS_TOOL_IDS || []); +const TEXT_ICON_TOOL_IDS = new Set(["poke", "pinch", "water_hose", "copy", "undo", "redo", "area_delete", "clear_tool"]); + + +function drawPhysicsToolIcon(canvas, toolId) { + const ctx = canvas?.getContext?.("2d"); + if (!ctx || !PHYSICS_ICON_TOOL_IDS.has(toolId)) return false; + const w = canvas.width || 160; + const h = canvas.height || 160; + const sx = w / 160; + const sy = h / 160; + ctx.clearRect(0, 0, w, h); + ctx.save(); + ctx.scale(sx, sy); + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + const line = (x1, y1, x2, y2, width = 10, stroke = "#4f4a42") => { + ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineWidth = width; ctx.strokeStyle = stroke; ctx.stroke(); + }; + const circle = (x, y, r, fill, stroke = "#4f4a42", width = 6) => { + ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = fill; ctx.fill(); ctx.lineWidth = width; ctx.strokeStyle = stroke; ctx.stroke(); + }; + const rounded = (x, y, width, height, radius, fill, stroke = "#4f4a42", lineWidth = 6) => { + const r = Math.max(0, Math.min(radius, width / 2, height / 2)); + ctx.beginPath(); + ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.quadraticCurveTo(x + width, y, x + width, y + r); + ctx.lineTo(x + width, y + height - r); ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); + ctx.lineTo(x + r, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - r); + ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); + ctx.fillStyle = fill; ctx.fill(); ctx.lineWidth = lineWidth; ctx.strokeStyle = stroke; ctx.stroke(); + }; + if (toolId === "rope") { + ctx.beginPath(); ctx.moveTo(22, 112); ctx.bezierCurveTo(48, 22, 92, 144, 138, 48); ctx.lineWidth = 13; ctx.strokeStyle = "#9a6b3f"; ctx.stroke(); + circle(22, 112, 10, "#f3d3a6", "#6c4b2e", 6); circle(138, 48, 10, "#f3d3a6", "#6c4b2e", 6); + } else if (toolId === "rod") { + line(28, 126, 132, 34, 18, "#8a633d"); circle(28, 126, 12, "#d7e0e8"); circle(132, 34, 12, "#d7e0e8"); + } else if (toolId === "spring") { + const pts = [[18,80],[36,80],[48,48],[64,112],[80,48],[96,112],[112,48],[124,80],[142,80]]; + ctx.beginPath(); ctx.moveTo(...pts[0]); for (const p of pts.slice(1)) ctx.lineTo(...p); ctx.lineWidth = 11; ctx.strokeStyle = "#647487"; ctx.stroke(); + circle(18,80,8,"#e8eef4","#465564",5); circle(142,80,8,"#e8eef4","#465564",5); + } else if (toolId === "wire" || toolId === "insulated_wire") { + ctx.beginPath(); ctx.moveTo(18,112); ctx.bezierCurveTo(52,22,104,138,142,52); ctx.lineWidth = toolId === "insulated_wire" ? 16 : 10; ctx.strokeStyle = toolId === "insulated_wire" ? "#3474b8" : "#3e3b39"; ctx.stroke(); + if (toolId === "insulated_wire") { ctx.lineWidth = 5; ctx.strokeStyle = "#b9ddff"; ctx.stroke(); } + else { ctx.beginPath(); ctx.moveTo(83,47); ctx.lineTo(70,78); ctx.lineTo(87,76); ctx.lineTo(75,108); ctx.lineWidth = 8; ctx.strokeStyle = "#f0b62b"; ctx.stroke(); } + circle(18,112,8,"#e9eef1"); circle(142,52,8,"#e9eef1"); + } else if (toolId === "pressure_switch") { + rounded(27,91,106,36,12,"#6f7881"); rounded(46,50,68,48,15,"#e7b842"); line(58,112,102,112,5,"#c9d2d8"); + } else if (toolId === "glass_wall") { + rounded(28,35,104,90,8,"rgba(111,205,225,0.34)","#4d8795",8); line(42,105,78,50,7,"#d9f7ff"); line(83,105,116,55,5,"#d9f7ff"); + } else if (toolId === "bounce_fence") { + line(22,104,138,104,15,"#e06788"); line(34,72,126,72,10,"#f3a9bb"); + for (const x of [48,80,112]) { line(x,126,x,42,7,"#6c5960"); ctx.beginPath(); ctx.moveTo(x-12,57); ctx.lineTo(x,40); ctx.lineTo(x+12,57); ctx.lineWidth=7; ctx.strokeStyle="#e06788"; ctx.stroke(); } + } else if (toolId === "gate_fence") { + line(28,32,28,132,13,"#665c50"); line(132,32,132,132,13,"#665c50"); line(34,92,74,72,12,"#c18c55"); line(126,92,86,72,12,"#c18c55"); circle(80,75,8,"#f2d197","#6d5842",5); + } else if (toolId === "rotator") { + circle(80,80,20,"#e4aa48","#5f574d",8); + for (const a of [0, Math.PI*2/3, Math.PI*4/3]) { const x=80+Math.cos(a)*52, y=80+Math.sin(a)*52; line(80,80,x,y,15,"#6d7c8c"); circle(x,y,9,"#cfd8df","#53606b",5); } + } else if (toolId === "poison_block") { + rounded(31,31,98,98,18,"#8f6cad","#4f3d60",8); circle(62,68,10,"#f1e6f7","#4f3d60",4); circle(98,68,10,"#f1e6f7","#4f3d60",4); line(60,105,100,105,8,"#f1e6f7"); line(56,118,104,90,6,"#eadff0"); line(56,90,104,118,6,"#eadff0"); + } else if (toolId === "reciprocator") { + line(22,80,138,80,11,"#657381"); rounded(57,51,46,58,10,"#d68c4f","#5d5046",7); + ctx.beginPath(); ctx.moveTo(42,48); ctx.lineTo(24,62); ctx.lineTo(42,76); ctx.moveTo(118,48); ctx.lineTo(136,62); ctx.lineTo(118,76); ctx.lineWidth=8; ctx.strokeStyle="#4f7fa8"; ctx.stroke(); + } else if (toolId === "one_way_fence") { + line(24,112,136,112,14,"#756653"); for (const x of [34,64,94,124]) line(x,45,x,124,7,"#9c8a72"); + ctx.beginPath(); ctx.moveTo(48,67); ctx.lineTo(104,67); ctx.lineTo(88,49); ctx.moveTo(104,67); ctx.lineTo(88,85); ctx.lineWidth=12; ctx.strokeStyle="#4b9d62"; ctx.stroke(); + } else if (toolId === "fence_h") { + line(24,55,24,130,12,"#75512f"); line(136,55,136,130,12,"#75512f"); line(24,72,136,72,14,"#b9854d"); line(24,108,136,108,14,"#b9854d"); + } else { ctx.restore(); return false; } + ctx.restore(); + return true; +} + +function attachPhysicsToolIcon(btn, toolId) { + if (!btn || !PHYSICS_ICON_TOOL_IDS.has(toolId)) return false; + const canvas = createToolPreviewCanvas("tool-sprite-preview tool-physics-preview", 160); + if (!drawPhysicsToolIcon(canvas, toolId)) return false; + btn.classList.add("tool-has-canvas-icon", "tool-has-physics-icon"); + btn.prepend(canvas); + return true; +} function applyToolTextIcon(btn, def, toolId) { if (!btn || !def?.iconText || !TEXT_ICON_TOOL_IDS.has(toolId)) return false; @@ -56,8 +135,9 @@ function renderToolPalette() { if (!def || def.simulationOnly || toolId === "observe") continue; const btn = document.createElement("button"); btn.className = "tool"; - const hasTextIcon = applyToolTextIcon(btn, def, toolId); - if (!hasTextIcon) applyToolImageIcon(btn, def); + const hasPhysicsIcon = category.id === "physics" && attachPhysicsToolIcon(btn, toolId); + const hasTextIcon = !hasPhysicsIcon && applyToolTextIcon(btn, def, toolId); + if (!hasPhysicsIcon && !hasTextIcon) applyToolImageIcon(btn, def); btn.dataset.tool = toolId; btn.dataset.toolCategory = category.id || ""; const label = document.createElement("span"); @@ -74,7 +154,7 @@ function renderToolPalette() { btn.setAttribute("aria-keyshortcuts", shortcut); btn.appendChild(badge); } - attachToolSpritePreview(btn, def, toolId); + if (!hasPhysicsIcon) attachToolSpritePreview(btn, def, toolId); if (toolId === world?.tool) btn.classList.add("selected"); body.appendChild(btn); } diff --git a/js/version.js b/js/version.js index 4db4a08..f7705c6 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.16.18"; - const APP_BUILD = "tarinai-fixes-v39-16-18"; + const APP_VERSION = "39.16.31"; + const APP_BUILD = "tarinai-fixes-v39-16-30"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/weather_system.js b/js/weather_system.js index 0dae735..3b008f5 100644 --- a/js/weather_system.js +++ b/js/weather_system.js @@ -52,6 +52,7 @@ const x = hasX ? px : rand(58, worldRef.w - 58); const y = hasY ? py : rand(58, worldRef.h - 58); const drop = new Item("water", x, y); + drop.rainDrop = opts?.rainDrop === true || (!hasX && !hasY); const amount = Number(opts?.amount); drop.amount = Number.isFinite(amount) ? amount : rand(24, 58); // Rain drops, placed water drops, and water-balloon splashes all use the diff --git a/js/world.js b/js/world.js index 48cbbc2..3de34fe 100644 --- a/js/world.js +++ b/js/world.js @@ -19,6 +19,7 @@ class World { constructor() { this.pointer = { x: 0, y: 0, inside: false, motion: 0 }; this.hoverGrabTarget = null; this.hoverDeleteTarget = null; + this.hoverGiveTarget = null; this.shootCursorActive = false; this.shootPointerRawX = 0; this.shootPointerRawY = 0; @@ -82,6 +83,7 @@ class World { constructor() { this.drawListDirty = true; this.antUpdatePhase = 0; this.selected = null; + this.hoverGiveTarget = null; this.deadCount = 0; this.birthEventCount = 0; this.liveIdNext = 1; diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index d709c61..db48ca4 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -374,7 +374,16 @@ let spawned = 0; for (let i = 0; i < n; i++) { const a = -Math.PI * 0.15 + deterministicRange(this, "zunchi-burst-angle", -0.92, 0.92, source, pin, i) + (i / Math.max(1, n - 1) - 0.5) * 1.2; - const it = new Item("zunchi", clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40), clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40)); + const spawnX = clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40); + const spawnY = clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40); + const sand = this.toiletSandAt?.(spawnX, spawnY) || null; + if (sand) { + sand.toiletFlash = 1; + sand.awakeUntil = Math.max(sand.awakeUntil || 0, (this.time || 0) + 0.8); + this.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-absorb"); + continue; + } + const it = new Item("zunchi", spawnX, spawnY); it.amount = 240; it.vx = Math.cos(a) * deterministicRange(this, "zunchi-burst-speed-x", 260, 560, source, pin, i) + deterministicRange(this, "zunchi-burst-jitter-x", -40, 40, source, pin, i); it.vy = Math.sin(a) * deterministicRange(this, "zunchi-burst-speed-y", 200, 480, source, pin, i) - deterministicRange(this, "zunchi-burst-up", 30, 160, source, pin, i); @@ -389,8 +398,28 @@ return spawned; }, + toiletSandAt(x, y) { + const candidates = typeof this.itemsOfType === "function" + ? this.itemsOfType("toilet") + : (this.nearbyItems?.(x, y, 96) || []); + for (const item of candidates || []) { + if (!item || item.dead || item.type !== "toilet") continue; + if (globalThis.TarinaiToiletSandSystem?.contains?.(item, x, y)) return item; + } + return null; + }, + spawnZunchi(x, y, source = null) { - const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40)); + const spawnX = clamp(x, 40, this.w - 40); + const spawnY = clamp(y, 40, this.h - 40); + const sand = this.toiletSandAt?.(spawnX, spawnY) || null; + if (sand) { + sand.toiletFlash = 1; + sand.awakeUntil = Math.max(sand.awakeUntil || 0, (this.time || 0) + 0.8); + this.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-absorb"); + return null; + } + const it = new Item("zunchi", spawnX, spawnY); it.amount = 260; it.stage = "fresh"; it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0); @@ -661,7 +690,7 @@ return dist(a, ball) + ap - (dist(b, ball) + bp); }); for (const t of arr.slice(5)) { - t.goIdle((ball.type === "balloon" ? "水風船" : "ボール") + "が混み合っているので眺めている"); + t.goIdle((ball.type === "balloon" ? "\u6c34\u98a8\u8239" : "\u30dc\u30fc\u30eb") + "\u304c\u6df7\u307f\u5408\u3063\u3066\u3044\u308b\u306e\u3067\u773a\u3081\u3066\u3044\u308b"); t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + deterministicRange(this, "ball-chaser-release-angle", -0.45, 0.45, t, ball); } } @@ -694,7 +723,7 @@ ball.amount = Math.max(ball.amount || 999, 999); audio.ballHit?.(); this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: isBalloon ? "rgba(94,188,236,0.56)" : "rgba(255,255,255,0.58)" })); - if (!isBalloon) this.log("ボールをつついて転がした。", "observe", { eventType: "ball_poke" }); + if (!isBalloon) this.log("\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066\u8ee2\u304c\u3057\u305f\u3002", "observe", { eventType: "ball_poke" }); return true; }, diff --git a/js/world_environment.js b/js/world_environment.js index 7bed650..4423bbc 100644 --- a/js/world_environment.js +++ b/js/world_environment.js @@ -6,6 +6,7 @@ const Geometry = global.TarinaiGeometry; const BOUNCE_FENCE_RESTITUTION = 4.20; + const ORDINARY_FENCE_RESTITUTION = 0.38; const BOUNCE_FENCE_MIN_SPEED = 820; const BOUNCE_FENCE_MAX_SPEED = 1680; @@ -330,7 +331,8 @@ cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true, vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)), bounce, gate, glassWall, oneWay, oneWayNx: Math.sin(angle), oneWayNy: -Math.cos(angle), gateOpen: Boolean(it.gateOpen), - restitution: bounce ? BOUNCE_FENCE_RESTITUTION : undefined, minBounceSpeed: bounce ? BOUNCE_FENCE_MIN_SPEED : undefined + restitution: bounce ? BOUNCE_FENCE_RESTITUTION : ORDINARY_FENCE_RESTITUTION, + minBounceSpeed: bounce ? BOUNCE_FENCE_MIN_SPEED : undefined }; }, @@ -755,7 +757,7 @@ t.lastBounceFenceImpulseAt = now; t._sleepExternalMotionUntil = Math.max(Number(t._sleepExternalMotionUntil || 0) || 0, now + 1.15); global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(t, 0.016, "bounce-fence-launch"); - if (t.sleeping || t.state === "sleep") global.TarinaiMovementUpdateStep?.wakeSleepingFromExternalMotion?.(t, "バウンス柵で跳ね飛ばされた"); + if (t.sleeping || t.state === "sleep") global.TarinaiMovementUpdateStep?.wakeSleepingFromExternalMotion?.(t, "\u30d0\u30a6\u30f3\u30b9\u67f5\u3067\u8df3\u306d\u98db\u3070\u3055\u308c\u305f"); t.fallTimer = Math.max(Number(t.fallTimer || 0) || 0, 0.62); t.fallMax = Math.max(Number(t.fallMax || 0) || 0, Number(t.fallTimer || 0) || 0); // Clear the body beyond the contact skin so the multi-pass collision @@ -800,7 +802,7 @@ const moveX = Number(dx || 0) || 0; const moveY = Number(dy || 0) || 0; const dist = Math.hypot(moveX, moveY); - if (dist <= 0.0001) return this.resolveSolidObstacleCollision(t, { maxPasses: 2, reason: opts.reason || "tarinai-stationary" }); + if (dist <= 0.0001) return this.resolveSolidObstacleCollision(t, { maxPasses: 2 }); if (this.isTarinaiHiddenInNestBox?.(t)) { t.x += moveX; t.y += moveY; @@ -842,7 +844,6 @@ slop: 0.30, oneWayDx: stepX, oneWayDy: stepY, - reason: opts.reason || "tarinai-swept-hit", }) || true; continue; } @@ -856,7 +857,6 @@ slop: 0.35, oneWayDx: stepX, oneWayDy: stepY, - reason: opts.reason || "tarinai-swept-move", }); collided = passHit || collided; } @@ -901,7 +901,7 @@ }, resolveFenceCollision(t) { - return this.resolveSolidObstacleCollision(t, { maxPasses: 2, reason: "resolve-fence" }); + return this.resolveSolidObstacleCollision(t, { maxPasses: 2 }); }, segmentIntersectsRect(x1, y1, x2, y2, r) { diff --git a/js/world_family_social.js b/js/world_family_social.js index 577dc5c..1f3b15d 100644 --- a/js/world_family_social.js +++ b/js/world_family_social.js @@ -112,14 +112,14 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { if (d <= limit && d < bestD) { best = t; bestD = d; } } if (!best) return null; - const nameLine = String(best.name || "たりない"); + const nameLine = String(best.name || "\u305f\u308a\u306a\u3044"); const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "") || window.TEXT_CATALOG?.stateLabel?.(best.state, best) || best.thought - || "様子を見ている"; + || "\u69d8\u5b50\u3092\u898b\u3066\u3044\u308b"; const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(best) : (Number(best.maxEnergy) || 100); - const statusLine = `体力: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)} 空腹: ${Math.round(best.hunger || 0)} ストレス: ${Math.round(best.stress || 0)}`; - const tempLine = worldRef.temperatureTooltipLineFor(best, "体感気温") || "体感気温: --℃"; + const statusLine = `\u4f53\u529b: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)}\u3000\u7a7a\u8179: ${Math.round(best.hunger || 0)}\u3000\u30b9\u30c8\u30ec\u30b9: ${Math.round(best.stress || 0)}`; + const tempLine = worldRef.temperatureTooltipLineFor(best, "\u4f53\u611f\u6c17\u6e29") || "\u4f53\u611f\u6c17\u6e29: --\u2103"; return { target: best, lines: [nameLine, String(behavior), statusLine, tempLine], kind: "tarinai" }; } @@ -140,7 +140,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { function ownedBedTooltipLinesLocal(worldRef, bed) { if (!bed || bed.dead || bed.type !== "grass_bed") return []; const owner = worldRef.liveTarinaiById?.(bed.ownerId) || null; - return ["かんたんベッド", `持ち主: ${owner?.name || "不明"}`, worldRef.temperatureTooltipLineFor?.(bed, "周辺気温")].filter(Boolean); + return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, worldRef.temperatureTooltipLineFor?.(bed, "\u5468\u8fba\u6c17\u6e29")].filter(Boolean); } Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ @@ -620,14 +620,14 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { occupants = this.nestBoxOccupants(box, Infinity); } const names = occupants.map(t => t?.name).filter(Boolean).slice(0, isPipe ? 8 : 5); - const more = occupants.length > names.length ? `、ほか${occupants.length - names.length}匹` : ""; - const label = isPipe ? "土管" : "巣箱"; - const insideLabel = isPipe ? "土管に入ってる子" : "入ってる子"; - return [label, names.length ? `${insideLabel}: ${names.join("、")}${more}` : `${insideLabel}: なし`, this.temperatureTooltipLineFor?.(box, "内部気温")].filter(Boolean); + const more = occupants.length > names.length ? `\u3001\u307b\u304b${occupants.length - names.length}\u5339` : ""; + const label = isPipe ? "\u571f\u7ba1" : "\u5de3\u7bb1"; + const insideLabel = isPipe ? "\u571f\u7ba1\u306b\u5165\u3063\u3066\u308b\u5b50" : "\u5165\u3063\u3066\u308b\u5b50"; + return [label, names.length ? `${insideLabel}: ${names.join("\u3001")}${more}` : `${insideLabel}: \u306a\u3057`, this.temperatureTooltipLineFor?.(box, "\u5185\u90e8\u6c17\u6e29")].filter(Boolean); }, - temperatureTooltipLineFor(target, label = "体感気温") { + temperatureTooltipLineFor(target, label = "\u4f53\u611f\u6c17\u6e29") { if (!target || !this.temperatureAt) return ""; const isActor = Array.isArray(this.tarinai) && this.tarinai.includes(target); const temp = isActor @@ -637,8 +637,8 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) { : this.temperatureAt(target.x, target.y)); if (!Number.isFinite(Number(temp))) return ""; const status = this.temperatureStatusFor?.(temp, isActor ? target : null) || null; - const suffix = status?.label ? `(${status.label})` : ""; - return `${label}: ${Number(temp).toFixed(1)}℃${suffix}`; + const suffix = status?.label ? `\uff08${status.label}\uff09` : ""; + return `${label}: ${Number(temp).toFixed(1)}\u2103${suffix}`; }, pointerItemTooltipInfo() { diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 3ee2515..1d53c48 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -7,6 +7,7 @@ const SIGNBOARD_CHARS_PER_LINE = 6; const SIGNBOARD_LINES = 3; const SIGNBOARD_TEXT_LIMIT = SIGNBOARD_CHARS_PER_LINE * SIGNBOARD_LINES; + const CONNECTION_ITEM_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]); function ensureEditorDialog(id, className, html) { let dialog = document.getElementById(id); @@ -222,21 +223,21 @@ return ensureEditorDialog("fanEditor", "rotator-editor fan-editor hidden", ` `); } @@ -301,8 +302,8 @@ item.angle = item.fanSwingCenter; worldRef?.markSpatialDirty?.("fan-swing-settings"); if (worldRef) worldRef.drawListDirty = true; - worldRef?.log?.(item.fanSwingOn ? "扇風機の首振り設定を変更した。" : "扇風機を固定向きにした。", "observe"); - if (typeof showToast === "function") showToast(item.fanSwingOn ? "扇風機: 首振りON" : "扇風機: 固定向き"); + worldRef?.log?.(item.fanSwingOn ? "\u6247\u98a8\u6a5f\u306e\u9996\u632f\u308a\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u305f\u3002" : "\u6247\u98a8\u6a5f\u3092\u56fa\u5b9a\u5411\u304d\u306b\u3057\u305f\u3002", "observe"); + if (typeof showToast === "function") showToast(item.fanSwingOn ? "\u6247\u98a8\u6a5f: \u9996\u632f\u308aON" : "\u6247\u98a8\u6a5f: \u56fa\u5b9a\u5411\u304d"); global.render?.(); close(); }; @@ -412,7 +413,7 @@ if (speedNum) speedNum.value = String(deg); if (thick) thick.value = String(initialThickness); if (thickNum) thickNum.value = String(initialThickness); - if (poweredInput) { poweredInput.checked = phyScalar(item, "motorOn", true) !== false; poweredInput.disabled = Boolean(item._signalDriven); poweredInput.title = item._signalDriven ? "電線信号で制御中" : ""; } + if (poweredInput) { poweredInput.checked = phyScalar(item, "motorOn", true) !== false; poweredInput.disabled = Boolean(item._signalDriven); poweredInput.title = item._signalDriven ? "\u96fb\u7dda\u4fe1\u53f7\u3067\u5236\u5fa1\u4e2d" : ""; } editor.syncInputPair(speed, speedNum, controller.draw); editor.syncInputPair(thick, thickNum, controller.draw); const close = () => { @@ -550,7 +551,7 @@ if (travelNum) travelNum.value = String(travelVal); if (thick) thick.value = String(initialThickness); if (thickNum) thickNum.value = String(initialThickness); - if (powered) { powered.checked = phyScalar(item, "railOn", true) !== false; powered.disabled = Boolean(item._signalDriven); powered.title = item._signalDriven ? "電線信号で制御中" : ""; } + if (powered) { powered.checked = phyScalar(item, "railOn", true) !== false; powered.disabled = Boolean(item._signalDriven); powered.title = item._signalDriven ? "\u96fb\u7dda\u4fe1\u53f7\u3067\u5236\u5fa1\u4e2d" : ""; } if (solidInput) solidInput.checked = phyScalar(item, "solid", true) !== false; editor.syncInputPair(angle, angleNum); editor.syncInputPair(speed, speedNum); @@ -660,6 +661,14 @@ if (item.type === "gate_fence") data.gateOpen = Boolean(item.gateOpen); if (item.type === "duplicator") { data.storedFoodType = item.storedFoodType || ""; data.storedFoodLabel = item.storedFoodLabel || ""; } if (item.type === "robot_cleaner") { data.robotCleanerMask = Number(item.robotCleanerMask ?? 3) | 0; data.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed); } + if (item.type === "pressure_switch") { + data.pressureTarget = item.pressureTarget === "time" ? "time" : (item.pressureTarget === "item" ? "item" : "tarinai"); + data.pressureThreshold = Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0)); + data.pressureWidth = Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0)); + data.pressureHeight = Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0)); + data.pressureTimeStart = Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0)); + data.pressureTimeEnd = Math.max(0, Math.min(1435, Number(item.pressureTimeEnd ?? 1080) | 0)); + } if (item.type === "fan") { data.fanSwingOn = Boolean(item.fanSwingOn); data.fanSwingCenter = Number(item.fanSwingCenter ?? item.angle ?? 0) || 0; @@ -720,6 +729,15 @@ item.storedFoodLabel = String(data.storedFoodLabel || (item.storedFoodType ? toolLabel(item.storedFoodType) : "")); if (item.roles) item.roles.food = Boolean(item.storedFoodType); } + if (item.type === "pressure_switch") { + item.pressureTarget = data.pressureTarget === "time" ? "time" : (data.pressureTarget === "item" ? "item" : "tarinai"); + item.pressureThreshold = Math.max(1, Math.min(999, Number(data.pressureThreshold || 1) | 0)); + item.pressureWidth = Math.max(60, Math.min(840, Number(data.pressureWidth || 220) | 0)); + item.pressureHeight = Math.max(60, Math.min(840, Number(data.pressureHeight || 160) | 0)); + item.pressureTimeStart = Math.max(0, Math.min(1435, Number(data.pressureTimeStart ?? 360) | 0)); + item.pressureTimeEnd = Math.max(0, Math.min(1435, Number(data.pressureTimeEnd ?? 1080) | 0)); + item.signalActive = false; + } if (item.type === "robot_cleaner") { const mask = Number(data.robotCleanerMask); item.robotCleanerMask = Number.isFinite(mask) ? Math.max(0, Math.min(0x3f, mask | 0)) : (global.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3); @@ -794,14 +812,22 @@ function ensureSignalItemEditor() { return ensureEditorDialog("signalItemEditor", "rotator-editor hidden", ` `); } @@ -814,21 +840,74 @@ const widthNum = dialog.querySelector("#pressureSwitchWidthNum"); const height = dialog.querySelector("#pressureSwitchHeight"); const heightNum = dialog.querySelector("#pressureSwitchHeightNum"); - target.value = item.pressureTarget === "item" ? "item" : "tarinai"; + const countRow = dialog.querySelector("#pressureSwitchCountRow"); + const widthRow = dialog.querySelector("#pressureSwitchWidthRow"); + const heightRow = dialog.querySelector("#pressureSwitchHeightRow"); + const timeRow = dialog.querySelector("#pressureSwitchTimeRow"); + const timeRange = dialog.querySelector("#pressureSwitchTimeRange"); + const timeTrack = dialog.querySelector("#pressureSwitchTimeTrack"); + const timeStart = dialog.querySelector("#pressureSwitchTimeStart"); + const timeEnd = dialog.querySelector("#pressureSwitchTimeEnd"); + const timeStartLabel = dialog.querySelector("#pressureSwitchTimeStartLabel"); + const timeEndLabel = dialog.querySelector("#pressureSwitchTimeEndLabel"); + const normalizeMinute = (value, fallback) => { + const n = Number(value); + const minute = Number.isFinite(n) ? Math.round(n / 5) * 5 : fallback; + return Math.max(0, Math.min(1435, minute)); + }; + const formatMinute = value => { + const minute = normalizeMinute(value, 0); + return `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`; + }; + target.value = item.pressureTarget === "time" ? "time" : (item.pressureTarget === "item" ? "item" : "tarinai"); count.value = String(item.pressureThreshold || 1); width.value = widthNum.value = String(item.pressureWidth || 220); height.value = heightNum.value = String(item.pressureHeight || 160); + timeStart.value = String(normalizeMinute(item.pressureTimeStart, 360)); + timeEnd.value = String(normalizeMinute(item.pressureTimeEnd, 1080)); + const updateTimeRange = () => { + const start = normalizeMinute(timeStart.value, 360); + const end = normalizeMinute(timeEnd.value, 1080); + timeStart.value = String(start); + timeEnd.value = String(end); + timeStartLabel.textContent = formatMinute(start); + timeEndLabel.textContent = formatMinute(end); + const startPct = start / 1435 * 100; + const endPct = end / 1435 * 100; + const idle = "rgba(94,112,128,0.22)"; + const active = "rgba(67,158,218,0.78)"; + timeTrack.style.background = start <= end + ? `linear-gradient(to right, ${idle} 0%, ${idle} ${startPct}%, ${active} ${startPct}%, ${active} ${endPct}%, ${idle} ${endPct}%, ${idle} 100%)` + : `linear-gradient(to right, ${active} 0%, ${active} ${endPct}%, ${idle} ${endPct}%, ${idle} ${startPct}%, ${active} ${startPct}%, ${active} 100%)`; + if (timeRange) timeRange.dataset.wraps = start > end ? "1" : "0"; + }; const updateOverlay = () => { - worldRef._pressureSwitchOverlay = { - item, - width: Math.max(60, Math.min(840, Number(widthNum.value || width.value || 220) | 0)), - height: Math.max(60, Math.min(840, Number(heightNum.value || height.value || 160) | 0)), - }; + if (target.value === "time") { + if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null; + } else { + worldRef._pressureSwitchOverlay = { + item, + width: Math.max(60, Math.min(840, Number(widthNum.value || width.value || 220) | 0)), + height: Math.max(60, Math.min(840, Number(heightNum.value || height.value || 160) | 0)), + }; + } global.render?.(); }; + const updateMode = () => { + const timeMode = target.value === "time"; + countRow.hidden = timeMode; + widthRow.hidden = timeMode; + heightRow.hidden = timeMode; + timeRow.hidden = !timeMode; + updateTimeRange(); + updateOverlay(); + }; syncEditorInputPair(width, widthNum, updateOverlay); syncEditorInputPair(height, heightNum, updateOverlay); - updateOverlay(); + timeStart.oninput = updateTimeRange; + timeEnd.oninput = updateTimeRange; + target.onchange = updateMode; + updateMode(); const close = () => { dialog.classList.add("hidden"); if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null; @@ -836,10 +915,12 @@ }; dialog.querySelector("#signalItemEditorApply").onclick = () => { global.TarinaiHistory.capture(worldRef, "detector-edit"); - item.pressureTarget = target.value === "item" ? "item" : "tarinai"; + item.pressureTarget = target.value === "time" ? "time" : (target.value === "item" ? "item" : "tarinai"); item.pressureThreshold = Math.max(1, Math.min(999, Number(count.value || 1) | 0)); item.pressureWidth = Math.max(60, Math.min(840, Number(widthNum.value || width.value || 220) | 0)); item.pressureHeight = Math.max(60, Math.min(840, Number(heightNum.value || height.value || 160) | 0)); + item.pressureTimeStart = normalizeMinute(timeStart.value, 360); + item.pressureTimeEnd = normalizeMinute(timeEnd.value, 1080); worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("detector-edit"); global.render?.(); @@ -1030,19 +1111,6 @@ }); - function ballPlacementBlocked(worldRef, item) { - if (!worldRef || !item || item.type !== "ball") return false; - const radiusFor = typeof itemRadiusFor === "function" ? itemRadiusFor : ((type, fallback = 12) => fallback); - const r = Math.max(8, Number(item.r || item.radius || radiusFor(item.type, 18) || 18) || 18); - const search = Math.max(72, r * 3.2); - for (const other of worldRef.nearbyItems?.(item.x, item.y, search, true) || worldRef.items || []) { - if (!other || other === item || other.dead || other.type !== "ball") continue; - const or = Math.max(8, Number(other.r || other.radius || radiusFor(other.type, 18) || 18) || 18); - if (distXY(item.x, item.y, other.x, other.y) < r + or - 1.0) return true; - } - return false; - } - function clearGrassBedsForPlacement(worldRef, item) { return clearSoftItemsUnderPlacementItem(worldRef, item, "placement-soft-overlap-clear"); } @@ -1135,7 +1203,6 @@ } } if (item.type === "grass" && (this.grassBlockedAt?.(item.x, item.y, item, { ignoreSoftPlacement: true }) || this.grassOnTarinaiAt?.(item.x, item.y))) return true; - if (ballPlacementBlocked(this, item)) return true; if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true; if (this.importantPlacementOverlapBlocked(item)) return true; if (item.type === "nest_box") { @@ -1229,7 +1296,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" || it.type === "plushie") continue; + if (!it || it.dead || it.type === "trace" || it.type === "splat" || it.type === "plushie" || CONNECTION_ITEM_TYPES.has(it.type)) continue; let hit = false; let d = distXY(x, y, it.x, it.y); if (global.TarinaiMechanicalSystem.isMechanicalType(it.type)) { @@ -1261,8 +1328,18 @@ placeItem(item, dropped = true) { if (!item) return null; + const directFeeding = global.TarinaiDirectFeedingSystem; + if (directFeeding?.isDirectFeedType?.(item)) { + const directTarget = directFeeding.findTargetAt?.(this, item.x, item.y) || null; + const given = directTarget ? directFeeding.give?.(this, directTarget, item, { source: "placement" }) : null; + if (given) { + this.drawListDirty = true; + if (typeof global.render === "function") global.render(); + return item; + } + } if (!(this.canAddObjects?.(1) ?? true)) { - showToast(`オブジェクト総数は${this.objectLimit}個までです。`); + showToast(`\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u7dcf\u6570\u306f${this.objectLimit}\u500b\u307e\u3067\u3067\u3059\u3002`); return null; } if (item.type === "grass" && !this.canAddGrass?.(1)) { @@ -1286,7 +1363,7 @@ if (removedSoft) this.updateItemCounts?.("placement-soft-overlap-clear"); const placed = this.addItem?.(item, `place:${item.type}`); if (!placed) { - if (!(this.canAddObjects?.(1) ?? true)) showToast(`オブジェクト総数は${this.objectLimit}個までです。`); + if (!(this.canAddObjects?.(1) ?? true)) showToast(`\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u7dcf\u6570\u306f${this.objectLimit}\u500b\u307e\u3067\u3067\u3059\u3002`); else if (item.type === "grass") showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`); return null; } @@ -1424,7 +1501,7 @@ if (tool === "new") { const spot = this.edgeSpawnPoint(x, y, 42, true); const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, generation: 1, entryTimer: 2.0 }); - if (!t) { showToast(`たりない個体数は${this.tarinaiPopulationLimit}匹までです。`); return; } + if (!t) { showToast(`\u305f\u308a\u306a\u3044\u500b\u4f53\u6570\u306f${this.tarinaiPopulationLimit}\u5339\u307e\u3067\u3067\u3059\u3002`); return; } t.wanderAngle = spot.angle; this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`); return; @@ -1436,7 +1513,6 @@ const itemType = toolItemType(tool); // In placement mode, clicks always mean placement. // Editing rotators / reciprocators is reserved for observe or non-placement tools. - if (itemType && this.directSetDuplicatorAt?.(x, y, itemType)) return; if (itemType) { let px = x, py = y; const rawItem = this.applyToolSize(new Item(itemType, px, py)); @@ -1444,6 +1520,17 @@ const requestedAngle = this.toolAngleFor ? this.toolAngleFor(itemType) : defaultItemAngle(itemType); global.TarinaiPlacementPreviewSystem.applyToolOrientation(this, rawItem, requestedAngle); } + const directFeeding = global.TarinaiDirectFeedingSystem; + const directTarget = directFeeding?.isDirectFeedType?.(rawItem) + ? directFeeding.findTargetAt?.(this, x, y) + : null; + // A Tarinai under the pointer takes precedence over a duplicator load slot. + if (directTarget) { + const given = this.placeItem(rawItem, true); + if (given) this._forceImmediateToolVisualRefresh?.("tool-change"); + return; + } + if (this.directSetDuplicatorAt?.(x, y, itemType)) return; // Placement uses the same footprint that the preview validates. const placed = this.placeItem(rawItem, true); if (!placed) return; @@ -1474,8 +1561,8 @@ if (type === "ball_poke" || type === "avoid_relationship" || type === "mechanism_poke") return true; if (/^(player_|tool_|tool:)/.test(type) || type.includes("tool") || type.includes("player")) return true; if (type.includes("robot")) return true; - if (/ロボ掃除機|掃除機/.test(text)) return true; - if (/範囲削除|コピーした|貼り付けた|つないだ|設定を変更|看板を書き換え|看板の文字を消した|を消した。|つついて動かした/.test(text)) return true; + if (/\u30ed\u30dc\u6383\u9664\u6a5f|\u6383\u9664\u6a5f/.test(text)) return true; + if (/\u7bc4\u56f2\u524a\u9664|\u30b3\u30d4\u30fc\u3057\u305f|\u8cbc\u308a\u4ed8\u3051\u305f|\u3064\u306a\u3044\u3060|\u8a2d\u5b9a\u3092\u5909\u66f4|\u770b\u677f\u3092\u66f8\u304d\u63db\u3048|\u770b\u677f\u306e\u6587\u5b57\u3092\u6d88\u3057\u305f|\u3092\u6d88\u3057\u305f\u3002|\u3064\u3064\u3044\u3066\u52d5\u304b\u3057\u305f/.test(text)) return true; return false; }, diff --git a/js/world_reset_presets.js b/js/world_reset_presets.js index 5c751c1..efe9eb7 100644 --- a/js/world_reset_presets.js +++ b/js/world_reset_presets.js @@ -11,6 +11,7 @@ const RESET_PRESETS = Object.freeze({ family: { label: "\u5BB6\u65CF" }, war: { label: "\u6226\u4E89" }, happy: { label: "\u5E78\u798F" }, + hair_trigger: { label: "\u4E00\u89E6\u5373\u767A" }, }); function normalizeResetPresetId(presetId = "default") { @@ -152,6 +153,10 @@ const RESET_PRESETS = Object.freeze({ const otherRects = worldRef.solidObstacleRects?.(other) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, other) || []; const otherRadius = presetItemFootprintRadius(worldRef, other); const margin = (itemIsFence || otherIsFence) ? 4 : Math.max(8, Math.min(itemRadius, otherRadius) * 0.28); + if (opts.rotationEnvelope && item.type === "rotator" && otherIsFence && otherRects.length) { + const envelope = Math.max(itemRadius, globalThis.TarinaiMechanicalSystem?.reach?.(item) || 0) + Math.max(8, Number(opts.rotationMargin || 12) || 12); + if (otherRects.some(rect => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(rect, item.x, item.y, envelope, 0))) return true; + } if (itemRects.length && otherRects.length) { if (itemRects.some(a => otherRects.some(b => globalThis.TarinaiCollisionFootprints?.rectsOverlap?.(a, b, margin)))) return true; continue; @@ -200,8 +205,19 @@ const RESET_PRESETS = Object.freeze({ function addPresetItem(worldRef, type, x, y, opts = {}) { const item = new Item(type, x, y); if (Number.isFinite(Number(opts.r))) item.r = Number(opts.r); - if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle); + if (opts.randomAngle) item.angle = randomPresetAngle(Number(opts.angleStep) || Math.PI / 4); + else if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle); if (opts.toolSize) item.toolSize = opts.toolSize; + if (type === "fan") { + const center = Number.isFinite(Number(opts.fanSwingCenter)) ? Number(opts.fanSwingCenter) : (Number(item.angle) || 0); + item.angle = center; + item.fanBodyAngle = Number.isFinite(Number(opts.fanBodyAngle)) ? Number(opts.fanBodyAngle) : center; + item.fanSwingCenter = center; + item.fanSwingRange = Number.isFinite(Number(opts.fanSwingRange)) ? Number(opts.fanSwingRange) : (35 * Math.PI / 180); + item.fanSwingSpeed = Number.isFinite(Number(opts.fanSwingSpeed)) ? Number(opts.fanSwingSpeed) : (48 * Math.PI / 180); + item.fanSwingPhase = Number.isFinite(Number(opts.fanSwingPhase)) ? Number(opts.fanSwingPhase) : rand(0, Math.PI * 2); + item.fanSwingOn = opts.fanSwingOn === true && item.fanSwingRange > 0 && item.fanSwingSpeed > 0; + } if (type === "duplicator") { item.storedFoodType = opts.storedFoodType || "sweet"; item.storedFoodLabel = typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType; @@ -235,13 +251,15 @@ const RESET_PRESETS = Object.freeze({ function addPresetItemsScattered(worldRef, type, count, zone = null, opts = {}) { let placed = 0; + const placedItems = []; const n = Math.max(0, Number(count) | 0); const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n; for (let i = 0; i < attempts && placed < n; i++) { const p = randPoint(worldRef, opts.pad || 82, zone); const item = addPresetItem(worldRef, type, p.x, p.y, opts); - if (item) placed++; + if (item) { placed++; placedItems.push(item); } } + return placedItems; } function addPresetFenceLine(worldRef, count, yRatio, angle = 0, type = "fence_h", opts = {}) { @@ -264,6 +282,48 @@ const RESET_PRESETS = Object.freeze({ } } + + function addPresetBounceFenceBox(worldRef, cx, cy, halfW, halfH) { + const fenceLength = 142; + const horizontalCount = Math.max(2, Math.ceil((halfW * 2) / fenceLength)); + const verticalCount = Math.max(2, Math.ceil((halfH * 2) / fenceLength)); + const horizontalStep = (halfW * 2) / horizontalCount; + const verticalStep = (halfH * 2) / verticalCount; + for (let i = 0; i < horizontalCount; i++) { + const x = cx - halfW + horizontalStep * (i + 0.5); + addPresetItem(worldRef, "bounce_fence", x, cy - halfH, { angle: 0, reason: "preset-hair-trigger-boundary" }); + addPresetItem(worldRef, "bounce_fence", x, cy + halfH, { angle: 0, reason: "preset-hair-trigger-boundary" }); + } + for (let i = 0; i < verticalCount; i++) { + const y = cy - halfH + verticalStep * (i + 0.5); + addPresetItem(worldRef, "bounce_fence", cx - halfW, y, { angle: Math.PI / 2, reason: "preset-hair-trigger-boundary" }); + addPresetItem(worldRef, "bounce_fence", cx + halfW, y, { angle: Math.PI / 2, reason: "preset-hair-trigger-boundary" }); + } + return { cx, cy, halfW, halfH }; + } + + function pointOutsidePresetBox(point, box, margin = 0) { + return Math.abs((point?.x || 0) - box.cx) > box.halfW + margin || Math.abs((point?.y || 0) - box.cy) > box.halfH + margin; + } + + function addPresetTarinaiOutsideBox(worldRef, count, box) { + const n = Math.max(0, Number(count) | 0); + const zones = [ + { x0: 82, x1: Math.max(82, box.cx - box.halfW - 70), y0: 82, y1: worldRef.h - 82 }, + { x0: Math.min(worldRef.w - 82, box.cx + box.halfW + 70), x1: worldRef.w - 82, y0: 82, y1: worldRef.h - 82 }, + { x0: box.cx - box.halfW, x1: box.cx + box.halfW, y0: 82, y1: Math.max(82, box.cy - box.halfH - 70) }, + { x0: box.cx - box.halfW, x1: box.cx + box.halfW, y0: Math.min(worldRef.h - 82, box.cy + box.halfH + 70), y1: worldRef.h - 82 }, + ].filter(zone => zone.x1 >= zone.x0 && zone.y1 >= zone.y0); + for (let i = 0; i < n; i++) { + let p = randPoint(worldRef, 82, zones[i % Math.max(1, zones.length)] || null); + if (!pointOutsidePresetBox(p, box, 28)) { + const side = i % 2 === 0 ? -1 : 1; + p = { x: clamp(box.cx + side * (box.halfW + 96), 82, worldRef.w - 82), y: clamp(box.cy + rand(-box.halfH, box.halfH), 82, worldRef.h - 82) }; + } + worldRef.addTarinai({ x: p.x, y: p.y, generation: 1 }); + } + } + function generateDefaultPreset(worldRef, field, seedPopulation) { const population = seedPopulation ?? field.population ?? CONFIG.initialPopulation; const grassCount = field.grass ?? 18; @@ -282,10 +342,11 @@ const RESET_PRESETS = Object.freeze({ addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 }); addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 }); addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 }); + addPresetItemsScattered(worldRef, "balloon", presetCount(field, compact ? 5 : 7, 3, 12), center, { pad: 92, reason: "preset-athletic-water-balloon", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: compact ? 240 : 200 }); addPresetFenceLine(worldRef, Math.max(2, Math.round((compact ? 3 : 4) * factor)), 0.36, 0, "fence_h", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" }); addPresetFenceLine(worldRef, Math.max(1, Math.round((compact ? 2 : 3) * factor)), 0.62, Math.PI / 2, "fence_v", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" }); addPresetFencesScattered(worldRef, Math.max(1, Math.round((compact ? 2 : 4) * factor)), null, { pad: 108, reason: "preset-athletic-fence", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210 }); - addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-slow-custom", motorSpeed: Math.PI * 0.18, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 }); + addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-simple", segments: [[-66, 0, 66, 0]], motorSpeed: Math.PI * 0.18, thickness: 13, rotationEnvelope: true, rotationMargin: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 180, maxRadius: compact ? 320 : 280 }); addPresetItemsScattered(worldRef, "reciprocator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-reciprocator-slow-custom", railMotorSpeed: 36, railTravel: 120, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 }); } @@ -317,6 +378,8 @@ const RESET_PRESETS = Object.freeze({ addPresetItemsScattered(worldRef, "ant_nest", presetCount(field, 1.25, 1, 2), null, { pad: 116, reason: "preset-war-ant-nest", avoidOverlap: true, attempts: 80, maxRadius: 190 }); addPresetItemsScattered(worldRef, "pushpin", presetCount(field, 5, 2, 9), null, { pad: 88, reason: "preset-war-pushpin-reduced" }); addPresetItemsScattered(worldRef, "oshibyo", presetCount(field, 8, 4, 16), null, { pad: 88, reason: "preset-war-oshibyo" }); + addPresetItemsScattered(worldRef, "fan", presetCount(field, 2, 1, 4), null, { pad: 118, reason: "preset-war-rotating-fan", randomAngle: true, fanSwingOn: true, fanSwingRange: 110 * Math.PI / 180, fanSwingSpeed: 72 * Math.PI / 180, avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 230 }); + addPresetItemsScattered(worldRef, "bounce_fence", presetCount(field, 3, 2, 5), null, { pad: 104, reason: "preset-war-bounce-fence", randomAngle: true, avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 230 }); addPresetFencesScattered(worldRef, presetCount(field, 8, 4, 14), null, { pad: 94, reason: "preset-war-fence" }); } @@ -325,10 +388,38 @@ const RESET_PRESETS = Object.freeze({ addPresetTarinai(worldRef, presetCount(field, 16, 8, 28), center); addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 8, 4, 14), center, { pad: 96, reason: "preset-happy-nest-box" }); addPresetItemsScattered(worldRef, "robot_cleaner", presetCount(field, 2, 1, 3), center, { pad: 110, reason: "preset-happy-robot-cleaner" }); - addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" }); + const happyDuplicators = addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" }); + for (const duplicator of happyDuplicators) addPresetItem(worldRef, "toilet", duplicator.x, duplicator.y, { reason: "preset-happy-toilet-sand" }); + addPresetItemsScattered(worldRef, "rain_shelter", presetCount(field, 3, 2, 5), center, { pad: 132, reason: "preset-happy-parasol", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 240 }); + addPresetItemsScattered(worldRef, "balloon", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-water-balloon", avoidOverlap: true, attempts: 90, maxRadius: 190 }); addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" }); } + + function generateHairTriggerPreset(worldRef, field) { + const cx = worldRef.w * 0.5; + const cy = worldRef.h * 0.5; + const halfW = clamp(worldRef.w * 0.18, 132, 238); + const halfH = clamp(worldRef.h * 0.18, 104, 182); + const box = addPresetBounceFenceBox(worldRef, cx, cy, halfW, halfH); + const inside = { + x0: cx - halfW + 52, + x1: cx + halfW - 52, + y0: cy - halfH + 52, + y1: cy + halfH - 52, + }; + addPresetItemsScattered(worldRef, "ball", presetCount(field, 7, 4, 12), inside, { pad: 0, reason: "preset-hair-trigger-ball" }); + addPresetTarinaiOutsideBox(worldRef, presetCount(field, 14, 7, 24), box); + const duplicatorCount = presetCount(field, 2, 1, 4); + for (let i = 0; i < duplicatorCount; i++) { + const side = i % 2 === 0 ? -1 : 1; + const x = clamp(cx + side * (halfW + 112), 84, worldRef.w - 84); + const spread = duplicatorCount <= 2 ? 0 : ((i / Math.max(1, duplicatorCount - 1)) - 0.5) * Math.min(worldRef.h * 0.42, 260); + const y = clamp(cy + spread, 84, worldRef.h - 84); + addPresetItem(worldRef, "duplicator", x, y, { storedFoodType: "food", reason: "preset-hair-trigger-pet-food-duplicator" }); + } + } + function addDefaultPresetRobotCleaner(worldRef, presetId = "default") { if (!worldRef || normalizeResetPresetId(presetId) === "empty") return; if ((worldRef.items || []).some(item => item && !item.dead && item.type === "robot_cleaner")) return; @@ -355,6 +446,7 @@ const RESET_PRESETS = Object.freeze({ case "family": generateFamilyPreset(worldRef, field); break; case "war": generateWarPreset(worldRef, field); break; case "happy": generateHappyPreset(worldRef, field); break; + case "hair_trigger": generateHairTriggerPreset(worldRef, field); break; case "default": default: generateDefaultPreset(worldRef, field, seedPopulation); diff --git a/js/world_temperature_system.js b/js/world_temperature_system.js index 074173f..723b7ba 100644 --- a/js/world_temperature_system.js +++ b/js/world_temperature_system.js @@ -113,25 +113,25 @@ if (this.temperatureAuto === false) this.currentTemperature = this.manualTemperature; return this.manualTemperature; }, + weatherFeltTemperatureOffsetAt(x, y, actor = null) { + if (!actor) return 0; + const weather = String(this.weather || "sunny"); + if (weather === "sunny") return 3.5; + if (weather === "light_rain") return -3.5; + return 0; + }, 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; const range = Math.max(24, CONFIG.temperatureItemRange ?? 190); - for (const it of this.nearbyItems(x, y, range + 8)) { + const fanRange = 320; + for (const it of this.nearbyItems(x, y, Math.max(range + 8, fanRange + 40))) { if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove" && it.type !== "fan")) continue; if (it.type === "fan") { - const angle = itemAngleFor(it); - const dx = x - it.x; - const dy = y - it.y; - const along = dx * Math.cos(angle) + dy * Math.sin(angle); - const fanRange = range * 1.35; - if (along <= 8 || along > fanRange) continue; - const lateral = Math.abs(-dx * Math.sin(angle) + dy * Math.cos(angle)); - const halfWidth = 28 + along * 0.72; - if (lateral > halfWidth) continue; - const edge = Math.pow(clamp(1 - along / fanRange, 0, 1), 0.62) * Math.pow(clamp(1 - lateral / Math.max(1, halfWidth), 0, 1), 0.30); - temp -= 5 * edge; + const wind = global.TarinaiItemDynamicBallSystem?.fanWindAt?.(it, x, y, { range: fanRange }) || null; + if (!wind) continue; + temp -= 5 * wind.falloff; continue; } const d = distXY(x, y, it.x, it.y); @@ -151,7 +151,10 @@ temp += clamp(toward, -nestWarmthLimit, nestWarmthLimit); break; } - if (actor && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3; + const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem; + const weatherBlocked = Boolean(actor && parasolSystem?.blocksWeatherAt?.(this, x, y)); + temp += weatherBlocked ? 0 : (this.weatherFeltTemperatureOffsetAt?.(x, y, actor) || 0); + if (actor && !weatherBlocked && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3; return this.temperatureNumber(temp); }, pipeInteriorTemperatureFor(pipe, actor = null) { @@ -204,6 +207,8 @@ if (direction === "cold") types.push("stove"); else if (direction === "hot") types.push("dry_ice", "fan"); else types.push("stove", "dry_ice", "fan"); + const weather = String(this.weather || "sunny"); + if ((direction === "hot" && weather === "sunny") || (direction === "cold" && weather === "light_rain")) types.push("rain_shelter"); const seen = new Set(); const out = []; for (const type of types) { @@ -242,6 +247,19 @@ const itemRange = Math.max(24, CONFIG.temperatureItemRange ?? 190); for (const it of this.temperatureComfortCandidateItems?.(currentStatus.direction) || this.items || []) { if (!it || it.dead) continue; + if (it.type === "rain_shelter") { + const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem; + const footprint = parasolSystem?.parasolFootprint?.(it) || { + centerX: it.x, + centerY: it.y + (Number(it.r || 50) || 50) * 0.84, + radiusX: (Number(it.r || 50) || 50) * 1.58, + radiusY: (Number(it.r || 50) || 50) * 0.68, + }; + for (const [nx, ny] of [[0.82, 0], [-0.82, 0], [0.72, 0.36], [-0.72, 0.36], [0.72, -0.36], [-0.72, -0.36]]) { + pushCandidate(footprint.centerX + footprint.radiusX * nx, footprint.centerY + footprint.radiusY * ny, -72); + } + continue; + } if (it.type === "nest_box" || it.type === "pipe") { const shelterTemp = it.type === "pipe" && this.pipeInteriorTemperatureFor ? this.pipeInteriorTemperatureFor(it, actor) @@ -273,7 +291,7 @@ 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 = Number.isFinite(Number(c.tempOverride)) ? Number(c.tempOverride) : this.temperatureAt(c.x, c.y, actor); + const temp = c.tempOverride != null && Number.isFinite(Number(c.tempOverride)) ? Number(c.tempOverride) : 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; @@ -282,7 +300,7 @@ const score = status.discomfort * 110 + Math.abs(temp - targetTemp) * 2.8 + d * 0.22 + blockedPenalty + (c.extra || 0); if (score < bestScore) { bestScore = score; - const shelterLabel = c.targetItem?.type === "pipe" ? "土管" : (c.targetItem?.type === "nest_box" ? "巣箱" : "快適な温度帯"); + const shelterLabel = c.targetItem?.type === "pipe" ? "\u571f\u7ba1" : (c.targetItem?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5feb\u9069\u306a\u6e29\u5ea6\u5e2f"); best = { x: c.x, y: c.y, dead: false, detour: !c.targetItem, temperatureTarget: true, temperature: temp, label: shelterLabel }; if (c.targetItem) best.shelterItem = c.targetItem; } diff --git a/js/world_tool_actions.js b/js/world_tool_actions.js index f5ec786..3b08702 100644 --- a/js/world_tool_actions.js +++ b/js/world_tool_actions.js @@ -21,19 +21,6 @@ findDeleteToolTargetAt(x, y) { let best = null; let bestD = Infinity; - if (wireTool) { - const antCandidates = this.nearbyAnts?.(x, y, 44, true) || this.ants || []; - for (let i = antCandidates.length - 1; i >= 0; i -= 1) { - const ant = antCandidates[i]; - if (!ant || ant.dead) continue; - const hit = Math.max(10, (Number(ant.r) || 6) * 1.9); - const d = distXY(x, y, ant.x, ant.y); - if (d <= hit && d < bestD) { - best = { kind: "ant", id: ant.id || "", _ref: ant, type: ant.kind || "ant", label: ant.kind === "queen" ? "女王アリ" : "アリ", x: ant.x, y: ant.y, localX: 0, localY: 0, center: true }; - bestD = d; - } - } - } const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || []; for (let i = itemCandidates.length - 1; i >= 0; i -= 1) { const it = itemCandidates[i]; @@ -156,7 +143,7 @@ const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || []; for (let i = itemCandidates.length - 1; i >= 0; i -= 1) { const it = itemCandidates[i]; - if (!it || it.dead) continue; + if (!it || it.dead || it.type === "splat") continue; if ((this.tool === "rope" || this.tool === "rod" || this.tool === "spring") && global.TarinaiPhysicsSoftClear.isSoftPlacementType(it.type)) continue; if (wireTool && ["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) continue; const isMechanism = Boolean(global.TarinaiMechanicalSystem.isMechanicalType(it.type)); @@ -215,8 +202,8 @@ useLinkToolAt(x, y) { const endpoint = this.findLinkEndpointAt?.(x, y); - const linkNames = { rope: "ヒモ", rod: "棒", spring: "バネ", wire: "電線", insulated_wire: "絶縁電線" }; - const toolName = linkNames[this.tool] || "接続"; + const linkNames = { rope: "\u30d2\u30e2", rod: "\u68d2", spring: "\u30d0\u30cd", wire: "\u96fb\u7dda", insulated_wire: "\u7d76\u7e01\u96fb\u7dda" }; + const toolName = linkNames[this.tool] || "\u63a5\u7d9a"; if (!endpoint) { showToast(`${toolName}\u3092\u3064\u306a\u3050\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002`); return true; } if (!this.pendingLinkEndpoint) { this.pendingLinkEndpoint = endpoint; diff --git a/scripts/regression_check.py b/scripts/regression_check.py index ec791e6..60d372d 100644 --- a/scripts/regression_check.py +++ b/scripts/regression_check.py @@ -269,6 +269,140 @@ def check_redundant_correction_paths_removed() -> None: ok("redundant correction paths remain removed") + +def direct_feeding_smoke() -> None: + source = read_text(ROOT / "js/tarinai_direct_feeding_system.js") + placement = read_text(ROOT / "js/world_placement_log.js") + ui = read_text(ROOT / "js/ui_input_shared.js") + render = read_text(ROOT / "js/render.js") + world_source = read_text(ROOT / "js/world.js") + js = f""" +const vm = require('vm'); +class TestEffect {{ + constructor(type, x, y, options = {{}}) {{ Object.assign(this, {{ type, x, y }}, options); }} +}} +const context = {{ + console, globalThis: null, window: null, + rand(a, b) {{ return (Number(a) + Number(b)) * 0.5; }}, + distXY(ax, ay, bx, by) {{ return Math.hypot(ax - bx, ay - by); }}, + isServingFoodType(type) {{ return !['grass', 'water'].includes(type); }}, + toolLabel(type) {{ return ({{ sweet: '\u305a\u3093\u3060\u9905', protein: '\u30d7\u30ed\u30c6\u30a4\u30f3' }})[type] || type; }}, + TarinaiEffect: TestEffect, + TarinaiItemRegistry: {{ food: {{ nutrition(type) {{ return type === 'sweet' ? 16.5 : 15; }} }} }}, + uiCache: {{ selectedSnapshot: 'stale' }}, +}}; +context.globalThis = context; context.window = context; +context.consumeBehaviorTarget = (t, world, item, role) => {{ + item.consumedRole = role; + item.foodServingsRemaining = Math.max(0, Number(item.foodServingsRemaining || 1) - 1); + return true; +}}; +context.TarinaiStructureLifecycle = {{ + deleteItem(world, item) {{ item.dead = true; world.items = world.items.filter(entry => entry !== item); return true; }} +}}; +vm.createContext(context); +vm.runInContext({source!r}, context, {{ filename: 'tarinai_direct_feeding_system.js' }}); +const api = context.TarinaiDirectFeedingSystem; +if (!api || !api.isDirectFeedType('sweet') || !api.isDirectFeedType('protein') || api.isDirectFeedType('ball')) throw new Error('direct-feed eligibility mismatch'); +const world = {{ + tarinai: [], items: [], effects: [], logs: [], time: 12, hoverGiveTarget: null, + isTarinaiHiddenInNestBox() {{ return false; }}, + log(text, kind, meta) {{ this.logs.push({{ text, kind, meta }}); }}, + recordFamily(t) {{ this.recorded = t; }}, +}}; +function makeTarinai(name, x, y) {{ + return {{ + name, x, y, radius: 22, world, affection: 0, currentPersonality: {{ openness: 0, sociability: 0 }}, + contains(px, py) {{ return Math.hypot(px - this.x, py - this.y) <= 24; }}, + adjustPersonality(key, delta, reason) {{ + this.currentPersonality[key] += delta; + this.personalityReasons = this.personalityReasons || []; + this.personalityReasons.push(reason); + return delta; + }}, + bubble(text) {{ this.lastBubble = text; }}, + }}; +}} +const first = makeTarinai('first', 20, 20); +const top = makeTarinai('top', 20, 20); +world.tarinai = [first, top]; +if (api.findTargetAt(world, 20, 20) !== top) throw new Error('top-most direct-feed target was not selected'); +const sweet = {{ type: 'sweet', amount: 5, foodServingsRemaining: 5, x: 20, y: 20 }}; +world.items.push(sweet); +const result = api.give(world, top, sweet, {{ source: 'pinch' }}); +if (!result?.foodGift || !result.removed || !sweet.dead || world.items.includes(sweet)) throw new Error('pinched food was not consumed and removed'); +const hearts = world.effects.filter(effect => effect.type === 'heart'); +if (hearts.length < 10 || hearts.length > 14) throw new Error(`food gift heart count out of range: ${{hearts.length}}`); +if (!(top.currentPersonality.openness > 0.018) || !(top.currentPersonality.sociability >= 0.010)) throw new Error('food gift personality effect is too weak'); +if (top.currentPersonality.openness <= 0.006) throw new Error('food gift does not exceed one second of cursor openness effect'); +const expectedPersonalityReason = '\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f'; +if (!top.personalityReasons?.length || top.personalityReasons.some(reason => reason !== expectedPersonalityReason)) throw new Error('direct-feed personality reason mismatch'); +if (top.lastBubble !== '\u2661' || context.uiCache.selectedSnapshot !== '') throw new Error('food gift feedback/cache refresh missing'); +if (!world.logs.some(entry => entry.meta?.eventType === 'direct_feed')) throw new Error('direct-feed observation log missing'); +const priorEffects = world.effects.length; +const medicine = {{ type: 'protein', amount: 5, foodServingsRemaining: 5, x: 20, y: 20 }}; +world.items.push(medicine); +const medicineResult = api.give(world, top, medicine, {{ source: 'pinch' }}); +if (!medicineResult || medicineResult.foodGift || medicine.consumedRole !== 'medicine') throw new Error('medicine direct-feed path mismatch'); +if (world.effects.length !== priorEffects) throw new Error('medicine incorrectly emitted food hearts'); +if (!(top.focusPulseTimer >= 0.72)) throw new Error('medicine feedback pulse missing'); +const placementItem = {{ type: 'sweet', amount: 5, foodServingsRemaining: 5, x: 20, y: 20 }}; +const placementResult = api.give(world, top, placementItem, {{ source: 'placement' }}); +if (!placementResult || placementResult.removed || placementItem.dead) throw new Error('ephemeral placement item should be consumed without lifecycle deletion'); +""" + with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f: + f.write(js) + tmp = Path(f.name) + try: + result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True) + finally: + tmp.unlink(missing_ok=True) + if result.returncode != 0: + sys.stderr.write(result.stdout) + sys.stderr.write(result.stderr) + fail("direct feeding runtime smoke test failed") + placement_start = placement.find("placeItem(item, dropped = true)") + direct_call = placement.find("directFeeding.give?.", placement_start) + object_limit = placement.find("this.canAddObjects?.(1)", placement_start) + blocked = placement.find("this.placementBlocked(item)", placement_start) + if min(placement_start, direct_call, object_limit, blocked) < 0 or not (direct_call < object_limit < blocked): + fail("direct placement feeding must run before object-limit and overlap rejection") + click_section = placement.find("const itemType = toolItemType(tool)") + click_direct = placement.find("const directTarget =", click_section) + duplicator_set = placement.find("this.directSetDuplicatorAt?.", click_section) + if min(click_section, click_direct, duplicator_set) < 0 or click_direct > duplicator_set: + fail("Tarinai direct feeding must take precedence over duplicator loading at the same point") + required_ui = ["directFeedTargetAt", 'source: "pinch"', "setGiveHover", "hoverGiveTarget"] + if any(token not in ui for token in required_ui): + fail("pinch direct-feeding input bridge is incomplete") + if 'label: "\\u4e0e\\u3048\\u308b"' not in render or "world.hoverGiveTarget" not in render: + fail("direct-feeding hover overlay is missing") + if world_source.count("this.hoverGiveTarget = null;") < 2: + fail("direct-feeding hover state is not initialized and reset") + ok("direct food/medicine feeding, placement interception, personality, hearts, and overlay verified") + + +def check_collision_reason_and_projection_dirty_removed() -> None: + collision_paths = [ + ROOT / "js/world_environment.js", + ROOT / "js/tarinai_update_step_movement.js", + ROOT / "js/tarinai_social_move_life.js", + ] + collision_source = "\n".join(read_text(path) for path in collision_paths) + if re.search(r"resolveSolidObstacleCollision\s*\([^)]*\{[^}]*\breason\s*:", collision_source, re.S): + fail("unused resolveSolidObstacleCollision reason option returned") + environment = read_text(ROOT / "js/world_environment.js") + if "opts.reason" in environment: + fail("resolveSolidObstacleCollision still reads unused reason option") + constraint = read_text(ROOT / "js/constraint_system.js") + match = re.search(r"function finishLinkSpatialUpdate\([^)]*\)\s*\{(.*?)\n \}", constraint, re.S) + if not match: + fail("finishLinkSpatialUpdate helper not found") + body = match.group(1) + if "Math.hypot" in body or "before" in body or body.count("markConstraintSpatialDirty") != 1: + fail("constraint Projection dirty determination is duplicated") + ok("unused collision reason and duplicate constraint Projection dirty determination removed") + def check_basic_action_spec_runtime() -> None: source = "\n".join(read_text(ROOT / rel) for rel in ["js/tarinai_action_definitions.js", "js/tarinai_needs_items.js"]) required = { @@ -679,6 +813,7 @@ def check_processing_split() -> None: if not (js_files.index("js/tarinai_forced_behavior.js") < js_files.index("js/tarinai_item_targeting.js") < js_files.index("js/tarinai_consumable_behavior.js") < js_files.index("js/tarinai_social_action_runtime.js") < js_files.index("js/tarinai_building_behavior.js") < js_files.index("js/tarinai_action_definitions.js") < js_files.index("js/tarinai_needs_items.js")): fail("tarinai behavior split load order is invalid") items = read_text(ROOT / "js/items.js") + temperature_system = read_text(ROOT / "js/world_temperature_system.js") init = read_text(ROOT / "js/item_type_initializers.js") item_render = read_text(ROOT / "js/item_render_runtime.js") if "class Item" not in items or "function initializeItemTypeState" not in init or "Object.assign(Item.prototype" not in item_render: @@ -1855,6 +1990,884 @@ def check_save_stream_deadlock_guard() -> None: fail("save export UI must recover from pending/error states") ok("save compression stream backpressure guard present") + +def check_electrocution_gate_chart_and_ascii() -> None: + health = read_text(ROOT / "js/health.js") + combat_effects = read_text(ROOT / "js/world_combat_effects.js") + action_state = read_text(ROOT / "js/tarinai_action_state.js") + signal = read_text(ROOT / "js/signal_system.js") + charts = read_text(ROOT / "js/ui_charts.js") + bind = read_text(ROOT / "js/ui_bind.js") + panel = read_text(ROOT / "css/panel.css") + + if '\\u611f\\u96fb' not in health or '\\u901a\\u96fb\\u4e2d\\u306e\\u96fb\\u7dda' not in health: + fail("electrocution cause normalization is missing") + if '"gate_fence"' not in signal or 'node.gateOpen = nextOpen' not in signal or 'signal-gate-toggle' not in signal: + fail("gate fence signal output handling is missing") + if ' + + - -
- - @@ -98,23 +121,34 @@
-
総数0
-
死亡0
状態安定
-
地面
-
-
気温10℃
- -
-
-
上限設定0は上限なし
-
- - +
+ コロニー設定 +
+
+
地面
+ +
+
+
気温10℃
+
+ + +
+
+
+
上限設定0は上限なし
+
+
+ + +
+
たりない 0 / ∞ オブジェクト 0 / ∞
+
+
-
たりない 0 / ∞ オブジェクト 0 / ∞
-
+
@@ -162,6 +196,36 @@
+ + + + +