From 5ed977788c3bc800485bf8afc04b1ca7c68e3c0d Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Sat, 27 Jun 2026 21:50:05 +0900 Subject: [PATCH] hm --- app_manifest.json | 2 +- assets/sounds/shoot_bolt_action.mp3 | Bin 0 -> 33853 bytes assets/sounds/shoot_pistol.mp3 | Bin 0 -> 17812 bytes assets/ui/tool_shoot.png | Bin 0 -> 1256 bytes css/base.css | 1 + css/components.css | 4 +- css/layout.css | 10 +- index.html | 288 ++++++++++++------------- js/audio.js | 2 + js/collision_footprint_system.js | 4 +- js/command_dispatcher.js | 6 + js/constraint_system.js | 37 ++-- js/debug_tools.js | 5 +- js/health.js | 71 +++++- js/item_dynamic_duplicator_system.js | 85 +------- js/item_lifecycle_support.js | 5 +- js/item_registry.js | 23 +- js/item_render_runtime.js | 29 +-- js/item_type_initializers.js | 4 +- js/item_update_policy.js | 2 +- js/items.js | 2 +- js/mechanical_system.js | 54 +++-- js/render.js | 227 ++++++++++++++++++- js/sim_core.js | 24 ++- js/simulation_environment_system.js | 10 + js/sound_pack.js | 11 +- js/tarinai_consumable_behavior.js | 4 +- js/tarinai_contact_item_system.js | 4 +- js/tarinai_disease_nest.js | 14 +- js/tarinai_item_interaction_context.js | 2 +- js/tarinai_item_targeting.js | 8 +- js/tarinai_needs_items.js | 24 ++- js/tarinai_social_move_life.js | 2 + js/tarinai_update_step_frame.js | 6 + js/tarinai_update_step_movement.js | 38 +++- js/text_catalog.js | 2 +- js/ui_input_mouse.js | 20 ++ js/ui_input_shared.js | 139 ++++++++++++ js/ui_input_touch.js | 9 + js/ui_layout_dialogs.js | 2 +- js/ui_selected.js | 10 +- js/version.js | 4 +- js/world.js | 11 + js/world_combat_effects.js | 23 +- js/world_environment.js | 25 ++- js/world_placement_log.js | 22 +- js/world_tool_actions.js | 184 ++++++++++++++++ scripts/generate_item_icons.py | 15 -- service-worker.js | 2 +- 49 files changed, 1084 insertions(+), 392 deletions(-) create mode 100644 assets/sounds/shoot_bolt_action.mp3 create mode 100644 assets/sounds/shoot_pistol.mp3 create mode 100644 assets/ui/tool_shoot.png diff --git a/app_manifest.json b/app_manifest.json index caa7202..d6bf065 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.15.18", + "version": "39.15.27", "css": [ "css/base.css", "css/layout.css", diff --git a/assets/sounds/shoot_bolt_action.mp3 b/assets/sounds/shoot_bolt_action.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d611e74b1fc80f377c8f9fad7454226f854ef879 GIT binary patch literal 33853 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 literal 0 HcmV?d00001 diff --git a/assets/sounds/shoot_pistol.mp3 b/assets/sounds/shoot_pistol.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..77db29188cb0b2bc948eac697b85074dad22afa0 GIT binary patch literal 17812 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 literal 0 HcmV?d00001 diff --git a/assets/ui/tool_shoot.png b/assets/ui/tool_shoot.png new file mode 100644 index 0000000000000000000000000000000000000000..50b5f7fe7ca6588f651d2714fbbeb4b01fb8926d GIT binary patch literal 1256 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU|H|!;uumf=k46Rd5_&hTGyLg z@euAP+P^+=Ip2nJ(nmCzSTe6J_1X}+Dugj+Y4CoBt~K`+UMuXKd;CV7tKydRmPuUE zLb^f!|7o^*hUH!uFN^OJLl&_!tgs-kx_tAuR3IkI%d;WgO zdp4a5Cb*dH+9x$vb=705^iM z_feL_x$Xv21`81ei-3NHz5VISYonJbFTf4%=s1 zvt|Pu$m{H{1ReAh_evGx5381b%Dn{ZUagE1DCli>Ndq4sFiVdJjU%2qU@s2pv9b`nIXe?RijD3i9@jk zNT@7*$9$&j*0cB<8y1I^*2M04R(ZburI@Vg^-C{z{wuft_sjoV*VfW~zb4toSJlLZ z*!GKloNQnB|LN-X-4#U#E?jmo<#O5fdc{u8WS`St{SW27dpLXJ$@%ss`}9`LJ6^Qs z<&A^a#Sho-{r+-Y^4y6l-0I_d_sywhY%jYK^R=CS4e#>c_#a<|9!uXmR{QJBsX52} zKHmH;^=)a8@rjZ*5B?-oL@xRTG zdWYA_*G9+IeZ0&Z^iVP96hr8_-8vOY9KaBnb4s4EGb`jvj(2Znr-xqcJK_6N`sc53 zdn@#OqjybYh!(^D4;5#;-hY}cw(a>L`MQ034AVC5xvBo|mtkGZb`GJdAHQCGapl{c z1wEB-gyPkT8D)Mk=KbJ58~XfhSz+Pk^qOMH1-V@2=U>hEzVJ7Ll0Vaom62LEIAgvW zpT4he4GI8v$3IWj^Dho6V|vLth2hm6PH>njw(PO`k@tY(1}kPBiSx-YXGghz+J%yNpuV3 z7^~#{ebS5lS@zn$__sHl6__%WKDkzWdiLk`!?(MRNp!yY@p%5?%l{XhpZ`iGI`}|p zLn?oQv*XYuRu0pow{q4^gd Vom}xh4OoIPc)I$ztaD0e0sujQ8L9vP literal 0 HcmV?d00001 diff --git a/css/base.css b/css/base.css index 5a72a27..66a0007 100644 --- a/css/base.css +++ b/css/base.css @@ -441,6 +441,7 @@ h1 { .tool[data-tool="stone"] { --tool-icon: url("../assets/ui/tool_stone.webp"); } .tool[data-tool="genkotsu"] { --tool-icon: url("../assets/ui/tool_genkotsu.webp"); } .tool[data-tool="firecracker"] { --tool-icon: url("../assets/ui/tool_firecracker.webp"); } +.tool[data-tool="shoot"] { --tool-icon: url("../assets/ui/tool_shoot.png"); --tool-icon-zoom: 1.48; } .tool[data-tool="fence_v"] { --tool-icon: url("../assets/ui/tool_fence_v.webp"); } .tool[data-tool="fence_h"] { --tool-icon: url("../assets/ui/tool_fence_h.webp"); } .tool[data-tool="bed"] { --tool-icon: url("../assets/ui/tool_bed.webp"); } diff --git a/css/components.css b/css/components.css index d5fdc33..554b5c2 100644 --- a/css/components.css +++ b/css/components.css @@ -745,8 +745,8 @@ z-index: 11; overflow: hidden; background: linear-gradient(180deg, rgba(238, 247, 255, 0.98), rgba(221, 237, 255, 0.98)); - border-color: rgba(82,128,190,0.36); - box-shadow: 0 8px 18px rgba(58, 82, 112, 0.13), inset 0 1px 0 rgba(255,255,255,0.82); + border: 3px solid rgba(52,112,188,0.72); + box-shadow: 0 9px 22px rgba(58, 82, 112, 0.18), inset 0 0 0 1px rgba(255,255,255,0.86); } .tool-category[data-tool-category="operate"] .tool-category-toggle { background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(231, 243, 255, 0.98)); diff --git a/css/layout.css b/css/layout.css index c7b4fec..d69ca60 100644 --- a/css/layout.css +++ b/css/layout.css @@ -6,12 +6,13 @@ .tool[data-tool="delete"] { --tool-icon: url("../assets/ui/tool_delete.webp"); } .tool[data-tool="area_delete"] { --tool-icon: url("../assets/ui/tool_delete.webp"); } .tool[data-tool="new"] { --tool-icon: url("../assets/ui/tool_new.webp"); } +.tool[data-tool="shoot"] { --tool-icon: url("../assets/ui/tool_shoot.png"); --tool-icon-zoom: 1.48; } .tool[data-tool="poke"] { --tool-emoji: "\1F448"; } .tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; } .tool[data-tool="water_hose"] { --tool-emoji: "\1F6BF"; } .tool[data-tool="copy"] { --tool-emoji: "⧉"; } -.tool[data-tool="area_delete"] { --tool-emoji: "✕"; } +.tool[data-tool="area_delete"] { --tool-emoji: "×"; } .tool[data-tool="rope"] { --tool-emoji: "╰"; } .tool[data-tool="rod"] { --tool-emoji: "━"; } .tool[data-tool="reciprocator"] { --tool-emoji: "⇄"; } @@ -41,10 +42,15 @@ .tool[data-tool="area_delete"] { --tool-icon-zoom: 1.42; } .tool[data-tool="pinch"].tool-emoji-icon::before { font-size: 22px; } .tool[data-tool="area_delete"].tool-emoji-icon::before { - font-size: 30px; + font-size: 31px; + line-height: 1; color: #f3a7aa; text-shadow: 0 1px 0 rgba(255,255,255,0.65), 0 2px 5px rgba(146,54,62,0.16); filter: none; + transform: translateY(-54%) scale(var(--tool-icon-zoom, 1)); +} +.tool-category[data-tool-category="operate"] .tool[data-tool="area_delete"].tool-emoji-icon::before { + transform: translate(-50%, -54%) scale(var(--tool-icon-zoom, 1)); } .tool[data-tool="ball"] { --tool-icon-zoom: 1.36; } diff --git a/index.html b/index.html index 11c5b1f..9685d88 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -66,17 +66,17 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/audio.js b/js/audio.js index 666f224..0fd084b 100644 --- a/js/audio.js +++ b/js/audio.js @@ -303,6 +303,8 @@ const audio = { grab() { this.play("sfx_grab", { category: "ops", minGap: 0.08 }); }, drop() { this.play("sfx_drop", { category: "ops", minGap: 0.08 }); }, waterHose() { this.play("sfx_water_hose", { category: "ops", minGap: 0.20 }); }, + shoot() { this.play("sfx_shoot", { category: "ops", minGap: 0.045 }); }, + boltAction() { this.play("sfx_bolt_action", { category: "ops", minGap: 0.10 }); }, ballHit() { this.play("sfx_ball_hit", { category: "ops", minGap: 0.08 }); }, stoneImpact() { this.play("sfx_stone_impact", { category: "ops", minGap: 0.18 }); }, genkotsuImpact() { this.play("sfx_genkotsu_impact", { category: "ops", minGap: 0.20 }); setTimeout(() => this.play("sfx_ground_rumble", { category: "ops", minGap: 0.20 }), 80); }, diff --git a/js/collision_footprint_system.js b/js/collision_footprint_system.js index bc7881a..7991cdc 100644 --- a/js/collision_footprint_system.js +++ b/js/collision_footprint_system.js @@ -208,7 +208,9 @@ const otherRects = worldRef.solidObstacleRects?.(other) || []; const otherRadius = itemFootprintRadius(worldRef, other, otherRects); const hasMechanism = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || global.TarinaiMechanicalSystem?.isMechanicalType?.(other.type); - const margin = hasMechanism ? 1.25 : Math.max(4, Math.min(itemRadius, otherRadius) * 0.18); + const itemIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(item.type) : /fence/.test(String(item.type || "")); + const otherIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(other.type) : /fence/.test(String(other.type || "")); + const margin = (itemIsFence || otherIsFence) ? (item.type === "gate_fence" || other.type === "gate_fence" ? 1.5 : 2.5) : (hasMechanism ? 1.25 : Math.max(4, Math.min(itemRadius, otherRadius) * 0.18)); if (itemRects.length && otherRects.length) { if (itemRects.some(a => otherRects.some(b => rectsOverlap(a, b, margin)))) return true; continue; diff --git a/js/command_dispatcher.js b/js/command_dispatcher.js index 0c1e509..5d8561a 100644 --- a/js/command_dispatcher.js +++ b/js/command_dispatcher.js @@ -25,6 +25,12 @@ const id = String(toolId || "observe"); if (worldRef.tool === id) return ok({ toolId: id, changed: false }); worldRef.tool = id; + if (id !== "shoot") { + worldRef.hoverShootTarget = null; + worldRef.shootCursorActive = false; + worldRef.shootCursorOffsetX = 0; + worldRef.shootCursorOffsetY = 0; + } emit(worldRef, "tool:selected", { toolId: id }); return ok({ toolId: id, changed: true }); } diff --git a/js/constraint_system.js b/js/constraint_system.js index 8e0b7cd..8554365 100644 --- a/js/constraint_system.js +++ b/js/constraint_system.js @@ -363,8 +363,9 @@ if (obj.radius && obj.name) { obj.x += moveX; obj.y += moveY; - const kickScale = mode === "rod" ? 0.038 : 0.052; - const kickLimit = mode === "rod" ? 88 : 105; + const kickScale = mode === "rod" ? 0.020 : 0.052; + const kickLimit = mode === "rod" ? 54 : 105; + if (mode === "rod") { obj.vx = (obj.vx || 0) * 0.90; obj.vy = (obj.vy || 0) * 0.90; } obj.vx = clamp((obj.vx || 0) + constraintKick(moveX, dt, kickScale, kickLimit), -150, 150); obj.vy = clamp((obj.vy || 0) + constraintKick(moveY, dt, kickScale, kickLimit), -150, 150); if (obj.state === "sleep" || obj.sleeping) { @@ -384,8 +385,8 @@ // Project the rail body along its single legal axis. Powered motors are // intentionally stiff, so a rope/rod can slow them but not yank them far // off their drive path in one frame. - const railScale = mode === "rod" ? (powered ? 0.58 : 0.94) : (powered ? 0.22 : 0.78); - const railCap = mode === "rod" ? (powered ? 34 : 42) : 18; + const railScale = mode === "rod" ? (powered ? 0.46 : 0.76) : (powered ? 0.22 : 0.78); + const railCap = mode === "rod" ? (powered ? 22 : 30) : 18; const along = clamp(alongRaw * railScale, -railCap, railCap); if (Math.abs(along) < 0.001) return false; const beforePhase = ps(obj, "railPhase", 0); @@ -399,16 +400,16 @@ } const changed = Math.abs(ps(obj, "railPhase", 0) - beforePhase) > 0.0001; const current = ps(obj, "slideSpeed", 0); - const kick = constraintKick(along, dt, mode === "rod" ? (powered ? 0.0035 : 0.009) : (powered ? 0.0045 : 0.012), mode === "rod" ? (powered ? 18 : 44) : (powered ? 22 : 58)); - pset(obj, "slideSpeed", clamp(current * (powered ? 0.74 : 0.88) + kick, -120, 120), "constraint-slide-project"); + const kick = constraintKick(along, dt, mode === "rod" ? (powered ? 0.0025 : 0.0055) : (powered ? 0.0045 : 0.012), mode === "rod" ? (powered ? 12 : 26) : (powered ? 22 : 58)); + pset(obj, "slideSpeed", clamp(current * (powered ? 0.70 : (mode === "rod" ? 0.72 : 0.88)) + kick, -120, 120), "constraint-slide-project"); if (changed) commitConstraintMovedMechanical(obj, "constraint-reciprocator-project"); return changed; } if (obj.type === "poison_block") { const beforeX = Number(obj.x || 0) || 0; const beforeY = Number(obj.y || 0) || 0; - const blockScale = mode === "rod" ? 0.92 : 0.72; - const blockCap = mode === "rod" ? 34 : 18; + const blockScale = mode === "rod" ? 0.74 : 0.72; + const blockCap = mode === "rod" ? 24 : 18; const px = clamp(moveX * blockScale, -blockCap, blockCap); const py = clamp(moveY * blockScale, -blockCap, blockCap); if (Math.hypot(px, py) < 0.001) return false; @@ -428,8 +429,9 @@ nextVx -= nx * intoCorrection * 0.50; nextVy -= ny * intoCorrection * 0.50; } - nextVx += constraintKick(px, dt, 0.012, 36); - nextVy += constraintKick(py, dt, 0.012, 36); + if (mode === "rod") { nextVx *= 0.86; nextVy *= 0.86; } + nextVx += constraintKick(px, dt, mode === "rod" ? 0.006 : 0.012, mode === "rod" ? 20 : 36); + nextVy += constraintKick(py, dt, mode === "rod" ? 0.006 : 0.012, mode === "rod" ? 20 : 36); pset(obj, "xv", clamp(nextVx, -165, 165), "constraint-poison-project"); pset(obj, "yv", clamp(nextVy, -165, 165), "constraint-poison-project"); obj.vx = ps(obj, "xv", obj.vx || 0); @@ -456,8 +458,9 @@ obj.prevY = obj.y; obj.x += moveX; obj.y += moveY; - obj.vx = clamp((obj.vx || 0) + constraintKick(moveX, dt, mode === "rod" ? 0.032 : 0.042, mode === "rod" ? 62 : 78), -125, 125); - obj.vy = clamp((obj.vy || 0) + constraintKick(moveY, dt, mode === "rod" ? 0.032 : 0.042, mode === "rod" ? 62 : 78), -125, 125); + if (mode === "rod") { obj.vx = (obj.vx || 0) * 0.90; obj.vy = (obj.vy || 0) * 0.90; } + obj.vx = clamp((obj.vx || 0) + constraintKick(moveX, dt, mode === "rod" ? 0.016 : 0.042, mode === "rod" ? 42 : 78), -125, 125); + obj.vy = clamp((obj.vy || 0) + constraintKick(moveY, dt, mode === "rod" ? 0.016 : 0.042, mode === "rod" ? 42 : 78), -125, 125); return true; } @@ -514,7 +517,7 @@ const d = Math.max(0.001, Math.hypot(dx, dy)); const delta = d - length; if (mode === "rope" && delta <= 0) return false; - if (mode === "rod" && Math.abs(delta) < 0.35) return false; + if (mode === "rod" && Math.abs(delta) < 0.85) return false; const nx = dx / d; const ny = dy / d; const ma = endpointMass(pa.obj); @@ -523,7 +526,7 @@ const ib = Number.isFinite(mb) ? 1 / Math.max(0.1, mb) : 0; const sum = ia + ib || 1; const step = Math.max(0.012, Math.min(0.05, Number(dt || 0.016) || 0.016)); - const compliance = mode === "rod" ? 0.035 / (step * 60) : 0.46 / (step * 60); + const compliance = mode === "rod" ? 0.085 / (step * 60) : 0.46 / (step * 60); const target = delta / (1 + compliance); const cap = mode === "rod" ? Math.max(6.0, Math.min(46, length * maxFraction)) @@ -538,11 +541,11 @@ if (mode === "rope") { changed = solveOnce(a, b, 0.58, 0.044) || changed; } else { - const iterations = 7; + const iterations = 5; for (let i = 0; i < iterations; i += 1) { const aa = endpointWorld(ep(item, 0), item.world || null) || a; const bb = endpointWorld(ep(item, 1), item.world || null) || b; - changed = solveOnce(aa, bb, 0.96, 0.18) || changed; + changed = solveOnce(aa, bb, 0.82, 0.10) || changed; } } return changed; @@ -906,7 +909,7 @@ const ca = endpointWorld(ep(item, 0), worldRef) || aa; const cb = endpointWorld(ep(item, 1), worldRef) || bb; const stretch = ca && cb ? Math.abs(distXY(ca.x, ca.y, cb.x, cb.y) - length) : 0; - if (stretch > Math.max(0.75, length * 0.006)) { + if (stretch > Math.max(1.6, length * 0.012)) { item._rodStretchCorrectionCount = (item._rodStretchCorrectionCount || 0) + 1; item._rodLastStretch = stretch; changed = applyDistanceConstraint(item, ca, cb, Math.min(0.033, dt || 0.016), length, "rod") || changed; diff --git a/js/debug_tools.js b/js/debug_tools.js index 5bd1f95..ea60846 100644 --- a/js/debug_tools.js +++ b/js/debug_tools.js @@ -131,6 +131,7 @@ return hints.join(" ") || "-"; } + const DEBUG_REFRESH_MS = 750; window.setInterval(() => { const w = window.world; const input = window.TarinaiInputMode?.snapshot?.() || {}; @@ -154,7 +155,7 @@ `spatial dirtyReasons ${topMap(diag.spatial?.dirtyReasons, 4)} rebuildReasons ${topMap(diag.spatial?.rebuildReasons, 4)}`, `nearby obstacleRects hit:${diag.nearby?.obstacleRectHits ?? 0} miss:${diag.nearby?.obstacleRectMisses ?? 0} built:${diag.nearby?.obstacleRectBuilt ?? 0} bypass:${diag.nearby?.obstacleRectBypass ?? 0} constraintDirty mark:${diag.constraintDirty?.marked ?? 0} coal:${diag.constraintDirty?.coalesced ?? 0}`, `scheduler heap:${scheduler.heap ?? 0} realtime:${scheduler.activeRealtime ?? scheduler.realtime ?? 0} ran:${scheduler.lastRan ?? 0} itemRt:${scheduler.realtimeRan ?? 0} itemDue:${scheduler.dueRan ?? 0} constraints:${scheduler.constraints ?? 0} postPairs:${scheduler.postConstraintPairs ?? 0} rodStretch:${fmtMs(scheduler.constraintStats?.rodStretchMax || 0)} corr:${scheduler.constraintStats?.rodCorrections ?? 0}`, - `items active:${scheduler.activeRealtime ?? 0} asleep:${scheduler.sleeping ?? scheduler.heap ?? 0} budgetPairs:${scheduler.physicsBudget?.maxPairs ?? "?"} substeps:${scheduler.physics?.mechanical?.substeps ?? scheduler.mechanicalStats?.substeps ?? "?"}`, + `items active:${scheduler.activeRealtime ?? 0} asleep:${scheduler.sleeping ?? scheduler.heap ?? 0} budgetPairs:${scheduler.physicsBudget?.maxPairs ?? "?"} substeps:${scheduler.physics?.mechanical?.substeps ?? scheduler.mechanicalStats?.substeps ?? "?"} fenceCCD:${Boolean((scheduler.physics?.mechanical || scheduler.mechanicalStats)?.fenceSensitive)}`, `terrain dirty raw:${terrain.raw ?? 0} global:${terrain.global ?? 0} chunk:${terrain.chunk ?? 0} coalesced:${terrain.coalesced ?? 0} reasons:${topMap(terrain.reasons, 4)}`, `work ai ${diag.work?.stats?.aiRuns ?? 0}/${diag.work?.stats?.aiSkips ?? 0} env ${diag.work?.stats?.envRuns ?? 0}/${diag.work?.stats?.envSkips ?? 0} coll ${diag.work?.stats?.collisionRuns ?? 0}/${diag.work?.stats?.collisionSkips ?? 0}`, `hints ${diagnosticHints(diag, perf)}`, @@ -173,5 +174,5 @@ : `tarinai debug fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms`; if (Date.now() >= copiedMessageUntil && copyBtn.textContent !== "Copy") copyBtn.textContent = "Copy"; if (!minimized) body.textContent = lastText; - }, 500); + }, DEBUG_REFRESH_MS); })(); diff --git a/js/health.js b/js/health.js index 24bb899..ba03579 100644 --- a/js/health.js +++ b/js/health.js @@ -11,6 +11,9 @@ const HEALTH = (() => { poke: "\u3064\u3064\u304b\u308c\u3059\u304e", firecracker: "\u7206\u7af9", genkotsu: "\u3052\u3093\u3053\u3064", + shooting: "射撃", + stoneCrush: "石に潰された", + weakness: "衰弱", poisonBlock: "毒ブロック", accident: "\u4e8b\u6545", collision: "\u885d\u7a81", @@ -30,6 +33,10 @@ const HEALTH = (() => { const text = String(reason || ""); if (!text) return ""; if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, ""); + if (/射撃|shoot/.test(text)) return CAUSES.shooting; + if (/石に潰された|石.*潰|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush; + const collisionWith = text.match(/([^、。\s]+)に衝突/); + if (collisionWith) return `${collisionWith[1]}に衝突`; if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight; if (/\u3052\u3093\u3053\u3064|genkotsu/.test(text)) return CAUSES.genkotsu; if (/毒ブロック|poison.?block/.test(text)) return CAUSES.poisonBlock; @@ -37,7 +44,7 @@ const HEALTH = (() => { if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug; if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker; if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds; - if (/\u885d\u7a81|collision/.test(text)) return CAUSES.collision; + if (/\u885d\u7a81|collision|急激な速度変化/.test(text)) return CAUSES.collision; if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return CAUSES.poke; if (/\u306d\u3080\u308a\u75c5|sleep.*disease/.test(text)) return CAUSES.sleepDisease; if (/\u7206\u767a\u75c5|explosion.*disease/.test(text)) return CAUSES.explosionDisease; @@ -50,7 +57,8 @@ const HEALTH = (() => { } if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return CAUSES.hunger; if (/\u30b9\u30c8\u30ec\u30b9|stress/.test(text)) return CAUSES.stress; - if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u77f3|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|stone|accident/.test(text)) return CAUSES.accident; + if (/\u77f3|stone/.test(text)) return CAUSES.stoneCrush; + if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|accident/.test(text)) return CAUSES.accident; if (/\u5bff\u547d|\u5929\u5bff/.test(text)) return CAUSES.lifespan; return ""; } @@ -96,24 +104,33 @@ const HEALTH = (() => { const recentMajor = hasRecentMajorDamage(t); const recent = recentDamageCause(t, 20); const fighting = isFightingContext(t, recent); - if (recentMajor && t.lastMajorDamageCause === CAUSES.collision) return CAUSES.collision; - if (recentMajor && (!direct || direct === t.lastMajorDamageCause || opts.fromDamage || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) { + + // Direct damage death: use the immediate/direct source, not a weakened suffix. + if (opts.fromDamage) { + if (direct) return direct; + if (recentMajor && t.lastMajorDamageCause) return t.lastMajorDamageCause; + if (recent) return recent; + return CAUSES.accident; + } + + if (recentMajor && t.lastMajorDamageCause === CAUSES.collision && !(t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) return CAUSES.collision; + if (recentMajor && (!direct || direct === t.lastMajorDamageCause || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) { return weakenedDeathReasonFor(t.lastMajorDamageCause); } const weakenedTrace = recentDamageCause(t, WEAKENED_TRACE_WINDOW); const traceCanWeaken = weakenedTrace && weakenedTrace !== CAUSES.stress && weakenedTrace !== CAUSES.hunger && weakenedTrace !== CAUSES.lifespan && weakenedTrace !== CAUSES.outOfBounds; - if (traceCanWeaken && (!direct || direct === CAUSES.stress || direct === CAUSES.hunger || direct === weakenedTrace || opts.fromDamage) && (t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 114)) { + if (traceCanWeaken && (!direct || direct === CAUSES.stress || direct === CAUSES.hunger || direct === weakenedTrace) && (t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 114)) { return weakenedDeathReasonFor(weakenedTrace); } if (direct && direct !== CAUSES.stress) return direct; if (t.zunchiDisease && ((t.zunchiDiseaseSeverity || 0) >= 0.30 || /\u75c5|zunchi/.test(text))) return CAUSES.zunchiDisease; if (t.hunger >= 108 || (t.hunger >= 94 && t.energy <= 26) || /\u98e2|\u7a7a\u8179|hunger/.test(text)) return CAUSES.hunger; - if (fighting && (t.energy <= 44 || t.stress >= 112 || t.mood <= 22 || opts.fromDamage)) return CAUSES.fight; - if (recent && recent !== CAUSES.stress && (t.energy <= 22 || t.mood <= 14 || t.stress >= 126 || opts.fromDamage)) return weakenedDeathReasonFor(recent); + if (fighting && (t.energy <= 44 || t.stress >= 112 || t.mood <= 22)) return CAUSES.fight; + if (recent && recent !== CAUSES.stress && (t.energy <= 22 || t.mood <= 14 || t.stress >= 126)) return weakenedDeathReasonFor(recent); if (text.includes(CAUSES.lifespan) || text.includes("\u5bff\u547d")) return CAUSES.lifespan; if (direct) return direct; if (t.stress >= 118 || /\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return CAUSES.stress; - if (t.energy <= 0.5) return CAUSES.accident; + if (opts.lowHealth || t.energy <= 0.5) return CAUSES.weakness; return ""; } @@ -122,11 +139,42 @@ const HEALTH = (() => { if (text.includes(WEAKENED_SUFFIX)) return text; const cause = dominantDeathCause(t, text, opts) || CAUSES.accident; if (cause.includes(WEAKENED_SUFFIX)) return cause; - const canWeaken = cause && !cause.endsWith("\u75c5") && cause !== CAUSES.hunger && cause !== CAUSES.stress && cause !== CAUSES.lifespan && cause !== CAUSES.outOfBounds && cause !== CAUSES.collision && cause !== CAUSES.genkotsu; + if (opts.fromDamage) return cause; + const canWeaken = cause && !cause.endsWith("\u75c5") && cause !== CAUSES.hunger && cause !== CAUSES.stress && cause !== CAUSES.lifespan && cause !== CAUSES.outOfBounds && cause !== CAUSES.collision && cause !== CAUSES.genkotsu && cause !== CAUSES.weakness; if (canWeaken && (opts.weakened || (hasRecentMajorDamage(t) && cause === t.lastMajorDamageCause))) return weakenedDeathReasonFor(cause); return cause; } + + function maybeReleaseZunchiFromDamage(t, actualLoss = 0, cause = "") { + if (!t || !t.world || t.dead) return false; + const loss = Math.max(0, Number(actualLoss) || 0); + if (loss <= 0.1) return false; + const now = t.world.time || 0; + if (now < Number(t.nextDamageZunchiAt || -999)) return false; + const probability = Math.max(0.06, Math.min(0.82, 0.06 + loss / 48)); + if (Math.random() >= probability) return false; + t.nextDamageZunchiAt = now + Math.max(0.55, 1.55 - Math.min(0.9, loss / 38)); + if (t.hasLodgedPinEffect?.("blocksZunchi")) { + t.oshiriByoZunchiStock = Math.min(24, Math.max(0, t.oshiriByoZunchiStock || 0) + 1); + t.thought = "ダメージでずんちを我慢している"; + return false; + } + const side = t.facingDir ? t.facingDir() : (t.facing || 1); + 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; + t.world.spawnZunchi?.(x, y, t); + t.poopCount = (t.poopCount || 0) + 1; + t.digest = Math.max(0, Number(t.digest || 0) - 1.0); + t.nextPoopBubbleAt = now + 2.0; + t.bubble?.("ぶりゅっ", 1.1, "rgba(74,91,50,0.78)", { force: true }); + if (now > Number(t.nextDamageZunchiLogAt || -999)) { + t.nextDamageZunchiLogAt = now + 10; + t.world.log?.(`${t.name}はダメージでずんちを放出した。`, "accident", { participants: [t] }); + } + return true; + } function applyDamage(t, amount, reason = "") { const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100); t.maxEnergy = maxEnergy; @@ -142,14 +190,14 @@ const HEALTH = (() => { if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null); if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure(); audio.damage?.(actualLoss); + maybeReleaseZunchiFromDamage(t, actualLoss, cause || reason); if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f"); if (t.wakeFromDamage) t.wakeFromDamage(cause || reason); if (t.maybeBecomeTimidAfterDamage) t.maybeBecomeTimidAfterDamage(cause || reason); t.showHpBar(); } if (t.energy <= 0.5) { - const majorNow = actualLoss >= Math.max(10, before * 0.42); - t.die(normalizeDeathReason(t, reason || cause, { fromDamage: true, weakened: majorNow })); + t.die(normalizeDeathReason(t, reason || cause, { fromDamage: true })); } } @@ -172,6 +220,7 @@ const HEALTH = (() => { normalizeDeathReason, applyDamage, briefDeathReason, + maybeReleaseZunchiFromDamage, }); })(); diff --git a/js/item_dynamic_duplicator_system.js b/js/item_dynamic_duplicator_system.js index 000c0cd..8f54a68 100644 --- a/js/item_dynamic_duplicator_system.js +++ b/js/item_dynamic_duplicator_system.js @@ -14,89 +14,18 @@ const s = support(); const syncRoles = s.syncDuplicatorRoles || global.TarinaiDuplicatorRuntime?.syncRoles; const applyStoredPin = s.duplicatorApplyStoredPin; - const canLoadNow = s.duplicatorCanLoadNow; - const loadPointFor = s.duplicatorLoadPoint || global.TarinaiDuplicatorRuntime?.loadPoint; - const loadTypeForItem = s.duplicatorLoadTypeForItem || global.TarinaiDuplicatorRuntime?.loadTypeForItem; - const candidateReach = s.duplicatorCandidateReach; - const setStoredType = s.duplicatorSetStoredType || global.TarinaiDuplicatorRuntime?.setStoredType; - if (typeof syncRoles !== "function" || typeof loadPointFor !== "function" || typeof loadTypeForItem !== "function" || typeof setStoredType !== "function") return false; + if (typeof syncRoles !== "function") return false; item.amount = 999; syncRoles(item); if (typeof applyStoredPin === "function") applyStoredPin(item, worldRef); item.duplicatorPinFlash = Math.max(0, (item.duplicatorPinFlash || 0) - dt); - if (!worldRef?.nearbyItems) return true; - if (typeof canLoadNow === "function" && !canLoadNow(item, worldRef)) { - item._duplicatorCandidateKey = ""; - item._duplicatorCandidateSince = 0; - return true; - } - - const loadPoint = loadPointFor(item); - const pickupRadius = Math.max(68, (item.r || 34) + 44); - const candidates = Array.from(worldRef.nearbyItems(item.x, item.y, pickupRadius, true) || []); - // SpatialGrid の分類漏れを補う。ただし、複製機からかなり近いものだけに限定し、 - // 移動中の複製機が周辺アイテムを吸い込む挙動を避ける。 - if (worldRef.items) { - for (const candidate of worldRef.items) { - if (!candidate || candidates.includes(candidate) || candidate === item || candidate.dead) continue; - const approxReach = pickupRadius + Math.max(16, candidate.r || 12); - if (distXY(item.x, item.y, candidate.x, candidate.y) <= approxReach) candidates.push(candidate); - } - } - - let best = null, bestType = "", bestSlotD = Infinity, bestScore = Infinity; - for (const it of candidates) { - if (!it || it === item || it.dead || it.playerHeld || it._heldByPlayer) continue; - const loadType = loadTypeForItem(it); - if (!loadType) continue; - if ((it.foodServingsRemaining ?? it.amount ?? 0) <= 0) continue; - const slotD = distXY(loadPoint.x, loadPoint.y, it.x, it.y); - const centerD = distXY(item.x, item.y, it.x, it.y); - const slotReach = typeof candidateReach === "function" ? candidateReach(it) : Math.max(20, Math.min(36, (it?.r || 12) * 0.82 + 13)); - const bodyReach = Math.max(22, (item.r || 34) * 0.46 + (it.r || 12) * 0.36); - if (slotD > slotReach && centerD > bodyReach) continue; - // 基本は投入口に近いものを優先。同距離なら置かれたばかりのものを優先し、 - // 複数候補があるときに意図しない古いアイテムへ寄りにくくする。 - const score = slotD + Math.max(0, Number(it.age || 0)) * 0.012; - if (score < bestScore) { - best = it; - bestType = loadType; - bestSlotD = slotD; - bestScore = score; - } - } - - if (!best || !bestType) { - item._duplicatorCandidateKey = ""; - item._duplicatorCandidateSince = 0; - return true; - } - - const candidateKey = `${best.id || ""}:${bestType}`; - const now = Number(worldRef.time || 0); - if (item._duplicatorCandidateKey !== candidateKey) { - item._duplicatorCandidateKey = candidateKey; - item._duplicatorCandidateSince = now; - return true; - } - const dwell = item.storedFoodType ? (s.DUPLICATOR_REPLACE_DWELL_SECONDS ?? 0.70) : (s.DUPLICATOR_LOAD_DWELL_SECONDS ?? 0.38); - if (now - Number(item._duplicatorCandidateSince || 0) < dwell) return true; - - const changed = item.storedFoodType !== bestType; - if (!changed && item.storedFoodType) { - item._duplicatorCandidateKey = ""; - item._duplicatorCandidateSince = 0; - return true; - } - - setStoredType(item, bestType, worldRef, { consumeItem: best, direct: false, force: false, log: false, emit: false }); - // Item.dead is a getter based on amount. Do not assign to it. - worldRef.markItemBucketsDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded"); - worldRef.markSpatialDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded"); - worldRef.markTerrainDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded"); - worldRef.emit?.("duplicator:loaded", { duplicator: item, foodType: item.storedFoodType, replaced: changed, slotDistance: bestSlotD }); - worldRef.log?.(`複製機に${item.storedFoodLabel}をセットした。`, typeof isPinType === "function" && isPinType(item.storedFoodType) ? "observe" : "food"); + // 39.15.27: auto-loading by contact is intentionally disabled. + // A duplicator accepts a stored item only through the explicit player + // click path (World.directSetDuplicatorAt / direct:true). This prevents + // dropped or pushed items from accidentally changing its setup. + item._duplicatorCandidateKey = ""; + item._duplicatorCandidateSince = 0; return true; } diff --git a/js/item_lifecycle_support.js b/js/item_lifecycle_support.js index 4acad1b..0480e5d 100644 --- a/js/item_lifecycle_support.js +++ b/js/item_lifecycle_support.js @@ -14,7 +14,7 @@ const DUPLICATOR_STORABLE_TYPES = new Set([ function duplicatorLoadTypeForItem(item) { if (!item || item.dead || item.type === "duplicator") return ""; const type = String(item.type || ""); - if (!type || type === "water_bowl" || type === "grass") return ""; + if (!type || type === "grass") return ""; if ((typeof isPinType === "function" && isPinType(type)) && item.pinState === "lodged") return ""; // Explicit list first. これで「ずんだ餅」「へこ餅」「けんか餅」が // serving-food 判定や medicine role の揺れに左右されず複製機へ入る。 @@ -23,7 +23,7 @@ function duplicatorLoadTypeForItem(item) { if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return type; const foodDef = global.TarinaiItemRegistry?.food?.get?.(type); if (foodDef && (Number(foodDef.nutrition || 0) > 0 || foodDef.effectId)) return type; - if (typeof roleMatches === "function" && roleMatches(item, "medicine") && type !== "water_bowl") return type; + if (typeof roleMatches === "function" && roleMatches(item, "medicine")) return type; return ""; } @@ -66,6 +66,7 @@ function syncDuplicatorRoles(item) { function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, options = {}) { if (!duplicator || duplicator.type !== "duplicator") return false; + if (options.direct !== true && options.force !== true) return false; const type = String(storedType || ""); if (!type) return false; const probe = { type, amount: 1, foodServingsRemaining: 1, dead: false }; diff --git a/js/item_registry.js b/js/item_registry.js index 5b1f399..f5ded3c 100644 --- a/js/item_registry.js +++ b/js/item_registry.js @@ -43,8 +43,9 @@ const TOOL_DEFINITIONS = Object.freeze({ pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "たりないや一部の置きものを移動させる。" }, new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" }, water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" }, - rope: { id: "rope", label: "ヒモ", placeable: false, scalable: false, iconText: "╰", tooltip: "2つの対象を順にクリックして、伸び縮みしないヒモで接続する。機械体・柵・ヒモ・棒はクリック地点、それ以外は中心につなぐ。" }, + rope: { id: "rope", label: "ヒモ", placeable: false, scalable: false, iconText: "╰", tooltip: "2つの対象を順にクリックしてふにゃふにゃしたヒモで繋ぐ。" }, rod: { id: "rod", label: "棒", placeable: false, scalable: false, iconText: "━", tooltip: "2つの対象を順にクリックして、曲がらない棒で接続する。" }, + shoot: { id: "shoot", label: "射撃", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "銃で攻撃する。" }, 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" }, sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" }, love_mochi: { id: "love_mochi", itemType: "love_mochi", label: "\u3078\u3053\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_love_mochi.webp", tooltip: "\u98df\u5f8c\u3057\u3070\u3089\u304f\u7e41\u6b96\u884c\u52d5\u304c\u8d77\u304d\u3084\u3059\u304f\u306a\u308b\u3002" }, @@ -78,7 +79,7 @@ const TOOL_DEFINITIONS = Object.freeze({ 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\u901f\u3081\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" }, rotator: { id: "rotator", itemType: "rotator", label: "回転体", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "↻", collisionShape: "custom_rotator", tooltip: "回転する機械。クリックすると形状や速度を設定できる。" }, - poison_block: { id: "poison_block", itemType: "poison_block", label: "毒ブロック", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "☠", collisionShape: "custom_poison_block", tooltip: "自由に形を描ける毒の物理ブロック。外力で動き、触れたたりないにダメージ。当たり判定ON/OFFを切り替えられる。" }, + poison_block: { id: "poison_block", itemType: "poison_block", label: "毒ブロック", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "☠", collisionShape: "custom_poison_block", tooltip: "当たると皮膚が痺れる。クリックで形状が変更できる。" }, reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "往復体", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "⇄", collisionShape: "custom_reciprocator", tooltip: "往復する機械。クリックすると形状や速度を設定できる。" }, water: { id: "water", itemType: "water", label: "水滴", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "💧", tooltip: "雨の日にも現れる水滴。たりないが飲める。複製機にもセットできる。" }, trace: { id: "trace", itemType: "trace", label: "死骸", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true }, @@ -165,7 +166,7 @@ const TOOL_CATEGORIES = Object.freeze([ { id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] }, { id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ball", "signboard"] }, { id: "physics", label: "物理", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS }, - { id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "ant_nest"] }, + { id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "shoot", "ant_nest"] }, ]); function toolCategories() { @@ -247,21 +248,6 @@ function toolTipsFromDefinitions() { "fieldScale": 1, "accessibleShapeTraits": ["horizontal", "oval", "liquid", "silver"] }, - "water_bowl": { - "renderer": "water_bowl", - "shape": "bowl_with_horizontal_oval_water", - "palette": { - "bowlFill": "rgba(171,120,76,0.72)", - "bowlStroke": "rgba(94,69,48,0.55)", - "waterFill": "rgba(86,157,224,0.58)", - "waterStroke": "rgba(62,113,178,0.36)", - "highlight": "rgba(255,255,255,0.54)" - }, - "scale": 1, - "uiPreviewScale": 1, - "fieldScale": 1, - "accessibleShapeTraits": ["bowl", "horizontal", "oval", "water", "blue"] - }, "sleep_drug": { "renderer": "sleep_tablet", "shape": "moon_tablet", @@ -379,7 +365,6 @@ function toolTipsFromDefinitions() { zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true }, grass: { label: "\u8349", nutrition: 2.6, hungerRelief: 0.55, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 }, water: { label: "水滴", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 }, - water_bowl: { label: "水の皿", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 }, ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 4.2, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 } }; diff --git a/js/item_render_runtime.js b/js/item_render_runtime.js index eba3de7..3fc4a49 100644 --- a/js/item_render_runtime.js +++ b/js/item_render_runtime.js @@ -209,22 +209,6 @@ draw(ctx, t, lighting = null) { ctx.stroke(); } ctx.restore(); - } else if (this.type === "water_bowl") { - const palette = visualPaletteFor("water_bowl"); - ctx.fillStyle = palette.bowlFill || "rgba(171, 120, 76, 0.72)"; - ctx.strokeStyle = palette.bowlStroke || "rgba(94, 69, 48, 0.55)"; - ctx.lineWidth = 2.2; - ctx.beginPath(); - ctx.ellipse(0, 2, this.r * 1.35, this.r * 0.78, 0, 0, Math.PI * 2); - ctx.fill(); ctx.stroke(); - ctx.fillStyle = palette.waterFill || "rgba(86, 157, 224, 0.58)"; - ctx.strokeStyle = palette.waterStroke || "rgba(62, 113, 178, 0.36)"; - ctx.lineWidth = 1.6; - ctx.beginPath(); - ctx.ellipse(0, -1, this.r * 1.02, this.r * 0.50, 0, 0, Math.PI * 2); - ctx.fill(); ctx.stroke(); - ctx.fillStyle = palette.highlight || "rgba(255,255,255,0.54)"; - ctx.beginPath(); ctx.arc(-this.r * 0.32, -this.r * 0.18, 2.8, 0, Math.PI * 2); ctx.fill(); } else if (this.type === "water") { drawOvalWaterDropSprite(ctx, this.r * visualFieldScaleFor("water"), visualPaletteFor("water")); } else if (this.type === "grass") { @@ -661,31 +645,30 @@ draw(ctx, t, lighting = null) { ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.globalAlpha *= enabled ? 1 : 0.48; - if (!enabled) ctx.setLineDash([10, 7]); ctx.strokeStyle = styleNight ? "#4d8e4e" : "#5cac4d"; ctx.lineWidth = thick; - ctx.shadowColor = enabled ? (styleNight ? "rgba(92,190,86,0.34)" : "rgba(62,170,54,0.28)") : "rgba(70,120,70,0.16)"; + ctx.shadowColor = styleNight ? "rgba(92,190,86,0.34)" : "rgba(62,170,54,0.28)"; ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.18) : Math.max(4, thick * 0.32)); strokeSegments(ctx, this, segments, "body"); ctx.shadowBlur = 0; ctx.setLineDash([]); if (tier !== "low") { - ctx.strokeStyle = enabled ? (styleNight ? "rgba(226,255,218,0.46)" : "rgba(255,255,255,0.50)") : "rgba(245,255,236,0.28)"; + ctx.strokeStyle = styleNight ? "rgba(226,255,218,0.46)" : "rgba(255,255,255,0.50)"; ctx.lineWidth = Math.max(1.4, thick * 0.22); strokeSegments(ctx, this, segments, "body"); } ctx.restore(); - ctx.fillStyle = enabled ? (styleNight ? "#263b27" : "#efffe9") : (styleNight ? "#303934" : "#eef4e8"); - ctx.strokeStyle = enabled ? (styleNight ? "#80c56e" : "#3e8f35") : "rgba(78,118,74,0.72)"; + ctx.fillStyle = styleNight ? "#263b27" : "#efffe9"; + ctx.strokeStyle = styleNight ? "#80c56e" : "#3e8f35"; ctx.lineWidth = 2.2; ctx.beginPath(); ctx.arc(0, 0, Math.max(8, thick * 0.74), 0, Math.PI * 2); ctx.fill(); ctx.stroke(); - ctx.fillStyle = enabled ? (styleNight ? "#d8ffd0" : "#26711f") : "rgba(70,92,68,0.70)"; + ctx.fillStyle = styleNight ? "#d8ffd0" : "#26711f"; ctx.font = `bold ${Math.max(10, thick * 0.84)}px ui-rounded, sans-serif`; ctx.textAlign = "center"; ctx.textBaseline = "middle"; - ctx.fillText(enabled ? "毒" : "透", 0, 0); + ctx.fillText("毒", 0, 0); } else if (this.type === "reciprocator") { const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0); const pb = window.TarinaiPhysicsBodySystem; diff --git a/js/item_type_initializers.js b/js/item_type_initializers.js index 3f40446..b33ca0a 100644 --- a/js/item_type_initializers.js +++ b/js/item_type_initializers.js @@ -7,8 +7,8 @@ function initializeItemTypeState(item, type, x, y) { const bodySchemaVersion = globalThis.TarinaiPhysicsBodySystem?.BODY_SCHEMA_VERSION || 4; const constraintSchemaVersion = globalThis.TarinaiPhysicsBodySystem?.CONSTRAINT_SCHEMA_VERSION || 4; if (isServingFoodType(type) || ["grass", "ant_corpse", "zunchi"].includes(type)) item.roles.food = true; - if (type === "water" || type === "water_bowl" || type === "zunda_juice") item.roles.drink = true; - if (["sweet", "water", "water_bowl", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true; + if (type === "water" || type === "zunda_juice") item.roles.drink = true; + if (["sweet", "water", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true; if (type === "bed" || type === "nest_box") item.roles.sleepPlace = true; if (["firecracker", "genkotsu", "pushpin", "oshibyo", "poison_block", "splat"].includes(type)) item.roles.danger = true; if (type === "grass") item.roles.grassMaterial = true; diff --git a/js/item_update_policy.js b/js/item_update_policy.js index 80a2780..bcbaa78 100644 --- a/js/item_update_policy.js +++ b/js/item_update_policy.js @@ -11,7 +11,7 @@ "ball", "genkotsu", "firecracker", "pushpin", "oshibyo", "plushie", "grass_bed", "zunchi", "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", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "water_bowl", + "stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", ]); function itemUpdateInterval(item) { diff --git a/js/items.js b/js/items.js index 5543685..417acc8 100644 --- a/js/items.js +++ b/js/items.js @@ -572,7 +572,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) { }; let scale = scaleMap[type] || 0.92; if (isConsumableSpriteType(type)) scale *= 1.95; - else if (["grass", "water", "water_bowl", "zunda_juice"].includes(type)) scale *= 1.85; + else if (["grass", "water", "zunda_juice"].includes(type)) scale *= 1.85; else if (type === "duplicator") scale *= 1.55; else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "genkotsu"].includes(type)) scale *= 1.08; else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) scale *= 1.35; diff --git a/js/mechanical_system.js b/js/mechanical_system.js index 30bf84f..83cf9fd 100644 --- a/js/mechanical_system.js +++ b/js/mechanical_system.js @@ -430,7 +430,7 @@ function obstacleRects(item) { if (!item || item.dead || !isMechanicalType(item)) return []; - if (item.type === "poison_block" && bodyOf(item)?.collision?.solid === false) return []; + if (bodyOf(item)?.collision?.solid === false) return []; return rectsFor(item, "obstacle"); } @@ -1018,6 +1018,17 @@ return out; } + function hasFenceNearby(item, worldRef, radius) { + if (!item || !worldRef?.items) return false; + const source = worldRef.nearbyItems?.(item.x, item.y, radius, true) || worldRef.items; + for (const other of source) { + if (!other || other === item || other.dead || !worldRef.isFenceType?.(other.type)) continue; + if (other.type === "gate_fence" && other.gateOpen) continue; + return true; + } + return false; + } + function resolveFenceContacts(item, dt, worldRef, radius = null) { if (!item || !worldRef || !isMechanicalType(item)) return false; // 往復体の停止・反転対象は物理アイテムに限定する。 @@ -1029,7 +1040,7 @@ const now = worldRef.time || 0; for (const mechRect of rects) { for (const fenceRect of fences) { - let info = FP().rectOverlapInfo?.(mechRect, fenceRect, 0.5); + let info = FP().rectOverlapInfo?.(mechRect, fenceRect, item.type === "rotator" ? 1.35 : 0.5); if (!info) continue; const separated = applyMechanicalFenceSeparation(item, info, dt, worldRef); if (separated) { @@ -1042,6 +1053,7 @@ const impulse = clamp(closing * 0.18 + info.overlap * 6.6 + 4.5, 3.5, separated ? 58 : 92); applyImpulse(item, info.x, info.y, -info.nx * impulse, -info.ny * impulse, separated ? 0.92 : 1.10); applyRailCorrection(item, info.nx, info.ny, info.overlap, 0.82); + if (item.type === "rotator") brakePoweredRotatorOnMechanicalContact(item, dt, worldRef, "fence"); const key = `f:${item.id}:${fenceRect.item?.id || "?"}`; worldRef._mechanicalContactAt = worldRef._mechanicalContactAt || Object.create(null); if ((worldRef._mechanicalContactAt[key] || -999) + 0.08 <= now) { @@ -1320,7 +1332,7 @@ function shouldCollideMechanical(item) { if (!item || item.dead || !isMechanicalType(item)) return false; if (item.playerHeld || item._heldByPlayer) return false; - if (item.type === "poison_block" && bodyOf(item)?.collision?.solid === false) return false; + if (bodyOf(item)?.collision?.solid === false) return false; return obstacleRects(item).length > 0; } @@ -1633,13 +1645,21 @@ const movingWork = active.length; let maxMotion = 0; for (const item of active) maxMotion = Math.max(maxMotion, estimatedBodyMotion(item, clampedDt)); - const substeps = Math.max(1, Math.min(Number(opts.maxSubsteps || 4) || 4, Math.max(Math.ceil(movingWork / 36), Math.ceil(maxMotion / 14)))); + let fenceSensitiveMotion = false; + for (const item of active) { + if (item?.type === "rotator" && estimatedBodyMotion(item, clampedDt) > 9 && hasFenceNearby(item, worldRef, reach(item) + 94)) { fenceSensitiveMotion = true; break; } + } + const requestedMaxSubsteps = Number(opts.maxSubsteps || 4) || 4; + const substepCap = fenceSensitiveMotion ? Math.min(6, Math.max(requestedMaxSubsteps, requestedMaxSubsteps + 2)) : requestedMaxSubsteps; + const motionSlice = fenceSensitiveMotion ? 8 : 14; + const substeps = Math.max(1, Math.min(substepCap, Math.max(Math.ceil(movingWork / 36), Math.ceil(maxMotion / motionSlice)))); const subDt = clampedDt / substeps; let ran = 0; let moved = false; let stepSolved = 0; let stepChecked = 0; let stepPairs = 0; + let stepFenceSolved = 0; // Resolve body pairs between substeps, not only once after all motion. // This keeps thin, visually-aligned rotator/reciprocator strokes from // tunneling through each other without inflating their collision width. @@ -1653,13 +1673,21 @@ else if (item.type === "poison_block") changed = updatePoisonBlock(item, subDt, worldRef, { centralStep: true, skipInteractions: true, deferDirty: true }); if (changed) { moved = true; subMoved = true; ran += 1; } } - if (subMoved && active.length > 1) { - const subFrame = buildMechanicalFrame(worldRef, { bodies }); - const subStats = resolveMechanicalPairsBroadphase(worldRef, subDt, { frame: subFrame, maxPairs: opts.maxSubstepPairs || 140, focusThreshold: opts.focusThreshold || 48 }); - if (subStats.solved) moved = true; - stepSolved += subStats.solved || 0; - stepChecked += subStats.checked || 0; - stepPairs += subStats.pairs || 0; + if (subMoved) { + const subFrame = active.length > 1 || active.some(it => it?.type === "rotator") ? buildMechanicalFrame(worldRef, { bodies }) : null; + if (active.length > 1 && subFrame) { + const subStats = resolveMechanicalPairsBroadphase(worldRef, subDt, { frame: subFrame, maxPairs: opts.maxSubstepPairs || 140, focusThreshold: opts.focusThreshold || 48 }); + if (subStats.solved) moved = true; + stepSolved += subStats.solved || 0; + stepChecked += subStats.checked || 0; + stepPairs += subStats.pairs || 0; + } + // Fence contacts are checked during high-speed rotator substeps rather + // than only after the final pose. This closes the tunneling gap where + // a thin/hand-drawn rotator can pass entirely across a fence between + // two final-frame overlap checks. + const subFenceSolved = resolveMechanicalFences(worldRef, subDt, active, subFrame?.activeSet || new Set(active), { maxFenceItems: opts.maxSubstepFenceItems || 32 }); + if (subFenceSolved) { moved = true; stepFenceSolved += subFenceSolved; } } } if (moved) { @@ -1676,12 +1704,12 @@ pairStats.solved = (pairStats.solved || 0) + stepSolved; pairStats.checked = (pairStats.checked || 0) + stepChecked; pairStats.pairs = (pairStats.pairs || 0) + stepPairs; - const fenceSolved = resolveMechanicalFences(worldRef, clampedDt, bodies, frame.activeSet, opts); + const fenceSolved = stepFenceSolved + resolveMechanicalFences(worldRef, clampedDt, bodies, frame.activeSet, opts); if (pairStats.solved || fenceSolved || links) { worldRef.drawListDirty = true; worldRef.markSpatialDirty?.("mechanical-world-solve"); } - worldRef._mechanicalWorldStats = { bodies: bodies.length, active: frame.active, collidable: frame.collidable, sourceCount: frame.sourceCount || frame.records.length, focused: Boolean(frame.focused), candidates: pairStats.candidates || 0, gridCells: pairStats.gridCells || 0, substeps }; + worldRef._mechanicalWorldStats = { bodies: bodies.length, active: frame.active, collidable: frame.collidable, sourceCount: frame.sourceCount || frame.records.length, focused: Boolean(frame.focused), candidates: pairStats.candidates || 0, gridCells: pairStats.gridCells || 0, substeps, fenceSensitive: fenceSensitiveMotion }; return { ran, active: frame.active, solved: pairStats.solved, checked: pairStats.checked, pairs: pairStats.pairs, fenceSolved, links, candidates: pairStats.candidates || 0, gridCells: pairStats.gridCells || 0, substeps, sourceCount: frame.sourceCount || frame.records.length, focused: Boolean(frame.focused) }; } finally { if (end) end(); diff --git a/js/render.js b/js/render.js index 32fa9d6..48c790e 100644 --- a/js/render.js +++ b/js/render.js @@ -6,7 +6,7 @@ const toolCursorIconImages = new Map(); function toolCursorIconDefinition(world) { const tool = String(world?.tool || ""); - if (!tool || tool === "observe" || tool === "undo" || tool === "redo") return null; + if (!tool || tool === "observe" || tool === "undo" || tool === "redo" || tool === "shoot") return null; const def = typeof toolDefinition === "function" ? toolDefinition(tool) : null; if (!def) return null; return { tool, def, type: def.itemType || (def.placeable ? tool : "") }; @@ -120,6 +120,224 @@ function drawSelectedToolCursorIcon(ctx, world) { ctx.restore(); } + +function strokeWorldObstacleRect(ctx, rect) { + if (!rect) return false; + if (rect.oriented) { + ctx.save(); + ctx.translate(rect.cx || 0, rect.cy || 0); + ctx.rotate(rect.angle || 0); + ctx.strokeRect(-(rect.halfW || 0), -(rect.halfH || 0), (rect.halfW || 0) * 2, (rect.halfH || 0) * 2); + ctx.restore(); + return true; + } + ctx.strokeRect(rect.left || 0, rect.top || 0, Math.max(1, (rect.right || 0) - (rect.left || 0)), Math.max(1, (rect.bottom || 0) - (rect.top || 0))); + return true; +} + +function drawToolTargetHoverHighlight(ctx, world) { + if (!world) return; + const tool = String(world.tool || ""); + const target = tool === "pinch" ? world.hoverGrabTarget : (tool === "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: "削除" } + : { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "つまむ" }; + 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; + const drawLabel = (cx, cy, ry) => { + const labelY = cy - ry - (world.screenSizeToWorld ? world.screenSizeToWorld(14) : 14); + const labelW = Math.max(42, (world.screenSizeToWorld ? world.screenSizeToWorld(52) : 52)); + const labelH = Math.max(18, (world.screenSizeToWorld ? world.screenSizeToWorld(21) : 21)); + ctx.save(); + ctx.shadowBlur = 0; + ctx.globalAlpha = 0.96; + ctx.fillStyle = "rgba(255,255,255,0.94)"; + ctx.strokeStyle = style.color; + ctx.lineWidth = Math.max(1.2, outerLine); + if (typeof roundedRect === "function") { roundedRect(ctx, cx - labelW / 2, labelY - labelH / 2, labelW, labelH, labelH / 2); ctx.fill(); ctx.stroke(); } + else ctx.strokeRect(cx - labelW / 2, labelY - labelH / 2, labelW, labelH); + ctx.fillStyle = style.color; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.font = `${Math.max(10, world.screenSizeToWorld ? world.screenSizeToWorld(12) : 12)}px sans-serif`; + ctx.fillText(style.label, cx, labelY + 0.5); + ctx.restore(); + }; + const strokeEllipse = (cx, cy, rx, ry) => { + ctx.fillStyle = style.fill; + ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.fill(); + ctx.shadowColor = style.color; + ctx.shadowBlur = Math.max(12, world.screenSizeToWorld ? world.screenSizeToWorld(18) : 18); + ctx.strokeStyle = style.color; + ctx.lineWidth = Math.max(3.6, worldLine + pulse * 1.8); + ctx.beginPath(); ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2); ctx.stroke(); + ctx.shadowBlur = 0; + ctx.setLineDash([10, 6]); + ctx.strokeStyle = "rgba(255,255,255,0.96)"; + ctx.lineWidth = Math.max(1.6, outerLine); + ctx.beginPath(); ctx.ellipse(cx, cy, rx * 1.10, ry * 1.12, 0, 0, Math.PI * 2); ctx.stroke(); + ctx.setLineDash([]); + drawLabel(cx, cy, ry * 1.12); + }; + ctx.save(); + ctx.globalAlpha = 0.92; + const isTarinaiTarget = typeof Tarinai !== "undefined" && target instanceof Tarinai; + if (isTarinaiTarget) { + const r = Math.max(17, target.radius || 22); + strokeEllipse(target.x || 0, target.y || 0, r * 1.02, r * 0.80); + } else if (target.type === "rope" || target.type === "rod") { + const runtime = TARINAI_RENDER_GLOBAL.TarinaiLinkRuntime; + const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem; + const a = runtime?.endpointWorld?.(pb?.endpoint?.(target, 0), world); + const b = runtime?.endpointWorld?.(pb?.endpoint?.(target, 1), world); + if (a && b) { + ctx.shadowColor = style.color; + ctx.shadowBlur = Math.max(10, world.screenSizeToWorld ? world.screenSizeToWorld(15) : 15); + ctx.strokeStyle = style.color; + ctx.lineWidth = Math.max(5, worldLine + 2); + ctx.lineCap = "round"; + ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); + ctx.shadowBlur = 0; + ctx.setLineDash([10, 6]); + ctx.strokeStyle = "rgba(255,255,255,0.95)"; + ctx.lineWidth = Math.max(2, outerLine); + ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); + ctx.setLineDash([]); + drawLabel((a.x + b.x) / 2, (a.y + b.y) / 2, 20); + } + } else { + const rects = world.solidObstacleRects?.(target) || []; + if (rects.length) { + ctx.strokeStyle = style.color; + ctx.shadowColor = style.color; + ctx.shadowBlur = Math.max(10, world.screenSizeToWorld ? world.screenSizeToWorld(16) : 16); + ctx.lineWidth = Math.max(3.2, worldLine); + let cx = target.x || 0, cy = target.y || 0, top = cy; + for (const rect of rects.slice(0, 14)) { + if (rect.oriented) { cx = rect.cx || cx; cy = rect.cy || cy; top = Math.min(top, cy - (rect.halfH || 18)); } + strokeWorldObstacleRect(ctx, rect); + } + ctx.shadowBlur = 0; + ctx.setLineDash([9, 6]); + ctx.strokeStyle = "rgba(255,255,255,0.96)"; + ctx.lineWidth = Math.max(1.6, outerLine); + for (const rect of rects.slice(0, 14)) strokeWorldObstacleRect(ctx, rect); + ctx.setLineDash([]); + drawLabel(cx, top, 14); + } else { + const r = Math.max(14, target.r || 16) * (target.type === "ball" ? 1.30 : 1.18); + strokeEllipse(target.x || 0, target.y || 0, r, r * 0.76); + } + } + ctx.restore(); +} + +function drawShootHoverHighlight(ctx, world) { + if (!world || world.tool !== "shoot") return; + const aim = world.shootAimPoint?.(); + if (!aim?.inside) return; + const target = world.hoverShootTarget; + if (!target || target.dead) return; + ctx.save(); + const pulse = 0.52 + Math.sin((world.time || 0) * 14) * 0.12; + ctx.globalAlpha = 0.72 + pulse * 0.12; + ctx.lineWidth = Math.max(1.8, (world.screenSizeToWorld ? world.screenSizeToWorld(2.0) : 2.0)); + ctx.strokeStyle = "rgba(255,106,92,0.94)"; + ctx.shadowColor = "rgba(255,188,146,0.44)"; + ctx.shadowBlur = Math.max(4, (world.screenSizeToWorld ? world.screenSizeToWorld(6) : 6)); + const isTarinaiTarget = typeof Tarinai !== "undefined" && target instanceof Tarinai; + const isItemTarget = Boolean(target.type); + if (isTarinaiTarget) { + const r = Math.max(16, target.radius || 22) * 1.12; + ctx.beginPath(); + ctx.ellipse(target.x || 0, target.y || 0, r * 0.95, r * 0.76, 0, 0, Math.PI * 2); + ctx.stroke(); + } else if (isItemTarget) { + const rects = world.solidObstacleRects?.(target) || []; + if (target.type === "rope" || target.type === "rod") { + const runtime = world.TarinaiLinkRuntime || TARINAI_RENDER_GLOBAL.TarinaiLinkRuntime; + const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem; + const a = runtime?.endpointWorld?.(pb?.endpoint?.(target, 0), world); + const b = runtime?.endpointWorld?.(pb?.endpoint?.(target, 1), world); + if (a && b) { + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + } + } else if (rects.length) { + for (const rect of rects.slice(0, 12)) strokeWorldObstacleRect(ctx, rect); + } else { + const r = Math.max(12, target.r || 14) * (target.type === "ball" ? 1.24 : 1.14); + ctx.beginPath(); + ctx.ellipse(target.x || 0, target.y || 0, r, r * 0.76, 0, 0, Math.PI * 2); + ctx.stroke(); + } + } else { + const r = Math.max(8, target.r || 8) * 1.45; + ctx.beginPath(); + ctx.ellipse(target.x || 0, target.y || 0, r, r * 0.86, 0, 0, Math.PI * 2); + ctx.stroke(); + } + ctx.restore(); +} + +function drawShootCursorFallback(ctx, x, y, size) { + const half = size * 0.5; + const tick = size * 0.16; + const gap = size * 0.11; + ctx.save(); + ctx.translate(x, y); + ctx.strokeStyle = "rgba(94,118,138,0.96)"; + ctx.lineWidth = Math.max(1.3, size * 0.045); + ctx.lineCap = "square"; + for (const sign of [-1, 1]) { + ctx.beginPath(); + ctx.moveTo(0, sign * gap); + ctx.lineTo(0, sign * (half - 2)); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(sign * gap, 0); + ctx.lineTo(sign * (half - 2), 0); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(-tick * 0.55, sign * (gap + tick)); + ctx.lineTo(tick * 0.55, sign * (gap + tick)); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(sign * (gap + tick), -tick * 0.55); + ctx.lineTo(sign * (gap + tick), tick * 0.55); + ctx.stroke(); + } + ctx.restore(); +} + +function drawShootCursorOverlay(ctx, world) { + if (!world || world.tool !== "shoot") return; + const aim = world.shootAimPoint?.(); + if (!aim?.inside) return; + const screen = world.worldToScreen ? world.worldToScreen(aim.x, aim.y) : { x: aim.x, y: aim.y }; + const canvasW = world.viewportW || world.w || ctx.canvas?.width || 1; + const canvasH = world.viewportH || world.h || ctx.canvas?.height || 1; + const size = Math.max(90, Math.min(160, Math.round(Math.min(canvasW, canvasH) * 0.205))); + const def = typeof toolDefinition === "function" ? toolDefinition("shoot") : null; + const img = cachedToolCursorIconImage(def?.icon || ""); + ctx.save(); + ctx.globalAlpha = 0.95; + ctx.shadowColor = "rgba(255,255,255,0.12)"; + ctx.shadowBlur = Math.max(2, size * 0.08); + if (img && img.complete && (img.naturalWidth || img.width)) { + ctx.drawImage(img, screen.x - size / 2, screen.y - size / 2, size, size); + } else { + drawShootCursorFallback(ctx, screen.x, screen.y, size); + } + ctx.restore(); +} + function randSeed(seed, min, max) { const s = Math.sin(seed * 12.9898) * 43758.5453; return min + (s - Math.floor(s)) * (max - min); @@ -278,7 +496,7 @@ function placementPreviewFor(world) { } if ((type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") && world.fenceRect) { const rawRect = world.fenceRect(tmp); - return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) || world.fencePlacementBlocked?.(tmp) }; + return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) }; } if (type === "rotator" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) { const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem; @@ -366,7 +584,7 @@ function drawPlacementPreview(ctx, world) { ctx.strokeRect(part.left, part.top, part.right - part.left, part.bottom - part.top); } } - } else if (type === "water_bowl" || type === "stone" || type === "ball" || type === "firecracker" || isPinType(type) || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") { + } else if (type === "stone" || type === "ball" || type === "firecracker" || isPinType(type) || type === "zunchi" || type.includes("mochi") || type === "sweet" || type === "sleep_drug") { ctx.beginPath(); ctx.arc(x, y, Math.max(8, r * (1.18 + pulse * 0.04)), 0, Math.PI * 2); ctx.fill(); @@ -1414,6 +1632,8 @@ function render() { } renderPerfEnd(endAttachments); + drawToolTargetHoverHighlight(ctx, world); + const endEffects = renderPerfBegin("render.draw.effects"); for (const ef of world.effects) { if (isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx); @@ -1444,6 +1664,7 @@ function render() { const endUiOverlay = renderPerfBegin("render.uiOverlay"); drawAtScreenPosition(world.selected, () => drawSelectedCard(ctx, world, lighting)); drawPointerItemTooltip(ctx, world); + drawShootCursorOverlay(ctx, world); drawSelectedToolCursorIcon(ctx, world); renderPerfEnd(endUiOverlay); diff --git a/js/sim_core.js b/js/sim_core.js index 27d5d79..6c2e2e0 100644 --- a/js/sim_core.js +++ b/js/sim_core.js @@ -512,6 +512,28 @@ class Effect { ctx.beginPath(); ctx.arc(0, 0, this.size + (1 - alpha) * 18, 0, Math.PI * 2); ctx.stroke(); + } else if (this.type === "shoot_impact") { + const s = this.size * (0.65 + (1 - alpha) * 0.55); + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.strokeStyle = this.color || "rgba(255,226,150,0.92)"; + ctx.lineWidth = Math.max(1.2, this.size * 0.16); + for (let i = 0; i < 7; i++) { + const a = this.seed + i * 0.897; + const inner = s * randSeed(this.seed + i + 2, 0.16, 0.34); + const outer = s * randSeed(this.seed + i + 9, 0.72, 1.18) + (1 - alpha) * this.size * 1.1; + ctx.globalAlpha = alpha * randSeed(this.seed + i + 31, 0.42, 0.95); + ctx.beginPath(); + ctx.moveTo(Math.cos(a) * inner, Math.sin(a) * inner); + ctx.lineTo(Math.cos(a) * outer, Math.sin(a) * outer); + ctx.stroke(); + } + ctx.globalAlpha = alpha * 0.75; + ctx.strokeStyle = "rgba(255,255,255,0.70)"; + ctx.lineWidth = Math.max(1, this.size * 0.08); + ctx.beginPath(); + ctx.arc(0, 0, s * 0.68 + (1 - alpha) * this.size * 0.65, 0, Math.PI * 2); + ctx.stroke(); } else if (this.type === "fight") { ctx.lineCap = "round"; ctx.lineJoin = "round"; @@ -790,7 +812,7 @@ class SpatialGrid { const foodMap = dynamic ? this.dynamicFoodCells : this.staticFoodCells; const hazardMap = dynamic ? this.dynamicHazardCells : this.staticHazardCells; const hasObstacle = typeof itemHasTrait === "function" ? itemHasTrait(type, "obstacle") : (type === "stone" || type === "ball" || type === "nest_box" || type === "bed" || type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v"); - const hasFood = typeof itemHasTrait === "function" ? itemHasTrait(type, "food_interest") : (type === "sweet" || type === "love_mochi" || type === "fight_mochi" || type === "grass" || type === "water" || type === "water_bowl" || type === "ant_corpse" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))); + const hasFood = typeof itemHasTrait === "function" ? itemHasTrait(type, "food_interest") : (type === "sweet" || type === "love_mochi" || type === "fight_mochi" || type === "grass" || type === "water" || type === "ant_corpse" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))); const hasHazard = typeof itemHasTrait === "function" && itemHasTrait(type, "hazard"); if (type === "poison_block") { const body = (typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem?.ensureBody?.(it, null, { syncFromLegacy: false }); diff --git a/js/simulation_environment_system.js b/js/simulation_environment_system.js index 6562ce1..1061627 100644 --- a/js/simulation_environment_system.js +++ b/js/simulation_environment_system.js @@ -28,6 +28,16 @@ function updateFrameSignals(worldRef, dt) { if (worldRef.pointer) worldRef.pointer.motion = Math.max(0, (worldRef.pointer.motion || 0) - dt * 2.4); + const recoilX = Number(worldRef.shootCursorOffsetX) || 0; + const recoilY = Number(worldRef.shootCursorOffsetY) || 0; + if (recoilX || recoilY) { + // Recoil is a real aim offset, but it should settle back to the actual + // mouse/touch position in roughly 1-2 seconds instead of drifting forever. + const k = Math.exp(-Math.max(0, Number(dt) || 0) * 3.15); + worldRef.shootCursorOffsetX = Math.abs(recoilX * k) < 0.25 ? 0 : recoilX * k; + worldRef.shootCursorOffsetY = Math.abs(recoilY * k) < 0.25 ? 0 : recoilY * k; + worldRef.drawListDirty = true; + } worldRef.shakeTimer = Math.max(0, (worldRef.shakeTimer || 0) - dt); if ((worldRef.shakeTimer || 0) <= 0) { worldRef.shakeDuration = 0; worldRef.shakeStrength = 0; } } diff --git a/js/sound_pack.js b/js/sound_pack.js index e67e8ab..634df74 100644 --- a/js/sound_pack.js +++ b/js/sound_pack.js @@ -3,7 +3,7 @@ (function (global) { // External audio replacement point. Add files and edit paths here; audio.js consumes this pack at startup. global.TARINAI_SOUND_PACK = Object.freeze({ - version: "1.2", + version: "1.3", categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 }, idGain: { sfx_notify: 2.60, @@ -21,6 +21,8 @@ sfx_ground_rumble: 1.75, sfx_firecracker_fuse: 1.55, sfx_firecracker_explode: 1.70, + sfx_shoot: 0.725, + sfx_bolt_action: 1.35, }, idMinGap: { sfx_ui_click: 0.030, @@ -31,6 +33,8 @@ sfx_notify: 0.10, sfx_log_push: 0.10, voice_sunbath: 0.95, + sfx_shoot: 0.045, + sfx_bolt_action: 0.10, }, samplePaths: { voice_hau: "assets/sounds/voice_001_hau.wav", @@ -40,7 +44,9 @@ voice_stress: "assets/sounds/voice_005_stress.wav", sfx_sleep: "assets/sounds/voice_006_sleep.wav", voice_sunbath: "assets/sounds/voice_007_sunbath_pokapoka.wav", - sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3" + sfx_firecracker_explode: "assets/sounds/firecracker_explosion.mp3", + sfx_shoot: "assets/sounds/shoot_pistol.mp3", + sfx_bolt_action: "assets/sounds/shoot_bolt_action.mp3" }, categories: { voice: "\u305f\u308a\u306a\u3044\u306e\u58f0", @@ -56,6 +62,7 @@ sfx_ui_click: "ops", sfx_ui_fold: "ops", sfx_place: "ops", sfx_delete: "ops", sfx_poke: "ops", sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_water_clean: "ops", sfx_drink: "ops", sfx_ball_hit: "ops", sfx_stone_impact: "ops", sfx_firecracker_fuse: "ops", sfx_firecracker_explode: "ops", + sfx_shoot: "ops", sfx_bolt_action: "ops", sfx_genkotsu_fall: "ops", sfx_genkotsu_impact: "ops", sfx_ground_rumble: "ops", sfx_grass: "ops", sfx_zunchi: "ops", sfx_ant_nest: "ops" } diff --git a/js/tarinai_consumable_behavior.js b/js/tarinai_consumable_behavior.js index 4db1db7..91d9878 100644 --- a/js/tarinai_consumable_behavior.js +++ b/js/tarinai_consumable_behavior.js @@ -33,8 +33,8 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) { return true; } - if ((role === "drink" || role === "medicine") && (target.type === "water" || target.type === "water_bowl")) { - const text = target.type === "water_bowl" ? "\u6c34\u306e\u76bf\u3067\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b" : "\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b"; + if ((role === "drink" || role === "medicine") && target.type === "water") { + const text = "\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b"; t.setActionState?.("seek_water", { target, reason: text, sleeping: false }); t.thought = text; t.applyWaterEffect?.(0.55, target.type); diff --git a/js/tarinai_contact_item_system.js b/js/tarinai_contact_item_system.js index 886ca7e..722be2c 100644 --- a/js/tarinai_contact_item_system.js +++ b/js/tarinai_contact_item_system.js @@ -11,8 +11,8 @@ if (!it || it.dead) return false; const d = it?.type === "duplicator" ? foodInteractionDistance(tarinai, it) : dist(tarinai, it); const contactReach = foodInteractionReach(tarinai, it, "food") + (it === tarinai.target && foodActionActive && it?.type !== "duplicator" ? 4 : 0); if (d > contactReach) return false; - if (it.type === "water" || it.type === "water_bowl") { - tarinai.thought = it.type === "water_bowl" ? "\u6c34\u306e\u76bf\u3067\u6c34\u3092\u98f2\u3093\u3067\u3044\u308b" : "\u6c34\u3092\u6d74\u3073\u3066\u843d\u3061\u7740\u3044\u3066\u3044\u308b"; + if (it.type === "water") { + tarinai.thought = "\u6c34\u3092\u6d74\u3073\u3066\u843d\u3061\u7740\u3044\u3066\u3044\u308b"; tarinai.target = it; tarinai.applyWaterEffect(dt, it.type); if ((tarinai.world?.time || 0) > (tarinai.nextDrinkSound || -999)) { diff --git a/js/tarinai_disease_nest.js b/js/tarinai_disease_nest.js index 64652a4..d10e507 100644 --- a/js/tarinai_disease_nest.js +++ b/js/tarinai_disease_nest.js @@ -201,20 +201,18 @@ freezeSleepDiseaseMotion(dt = 0) { if (!this.sleepDisease) return false; const beingDraggedByAnt = Boolean((this.antDraggedTimer || 0) > 0 && this.antDraggedBy); + const heldByPlayer = Boolean(this.playerHeld || this._heldByPlayer); if (!Number.isFinite(this.sleepDiseaseAnchorX)) this.sleepDiseaseAnchorX = this.x; if (!Number.isFinite(this.sleepDiseaseAnchorY)) this.sleepDiseaseAnchorY = this.y; - this.startSleeping(null, beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b"); - this.vx = 0; - this.vy = 0; - if (beingDraggedByAnt) { + this.startSleeping(null, beingDraggedByAnt ? "眠ったままアリに運ばれている" : "ねむり病で眠り続けている"); + // ねむり病は意識だけを固定し、物理位置は固定しない。 + // 以前は anchor に毎回スナップしていたため、外力・つまみ・機械接触を無視していた。 + if (heldByPlayer || beingDraggedByAnt || Math.hypot(Number(this.vx || 0) + Number(this.impulseVx || 0), Number(this.vy || 0) + Number(this.impulseVy || 0)) > 0.1) { this.sleepDiseaseAnchorX = this.x; this.sleepDiseaseAnchorY = this.y; - } else { - this.x = this.sleepDiseaseAnchorX; - this.y = this.sleepDiseaseAnchorY; } this.wanderAngle = this.wanderAngle || 0; - this.thought = beingDraggedByAnt ? "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b" : "\u306d\u3080\u308a\u75c5\u3067\u7720\u308a\u7d9a\u3051\u3066\u3044\u308b"; + this.thought = beingDraggedByAnt ? "眠ったままアリに運ばれている" : "ねむり病で眠り続けている"; if (this.world && this.world.time > (this.nextSleepBubbleAt || 0)) { this.nextSleepBubbleAt = this.world.time + 5.0; this.world.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(65,70,92,0.72)"); diff --git a/js/tarinai_item_interaction_context.js b/js/tarinai_item_interaction_context.js index b2d4d0f..04e9924 100644 --- a/js/tarinai_item_interaction_context.js +++ b/js/tarinai_item_interaction_context.js @@ -25,7 +25,7 @@ if (tarinai.target && !tarinai.target.dead && (tarinai.target.type === "duplicator" || roleMatches(tarinai.target, "food") || roleMatches(tarinai.target, "medicine"))) foodCandidates.push(tarinai.target); for (const it of nearbyItems) { if (!it || it.dead || foodCandidates.includes(it)) continue; - if (!(it.type === "duplicator" || roleMatches(it, "food") || roleMatches(it, "medicine") || it.type === "water" || it.type === "water_bowl")) continue; + if (!(it.type === "duplicator" || roleMatches(it, "food") || roleMatches(it, "medicine") || it.type === "water")) continue; foodCandidates.push(it); if (foodCandidates.length >= scanLimit) break; } diff --git a/js/tarinai_item_targeting.js b/js/tarinai_item_targeting.js index 8697e09..caf00e8 100644 --- a/js/tarinai_item_targeting.js +++ b/js/tarinai_item_targeting.js @@ -24,8 +24,8 @@ function effectiveFoodRoleMatches(item, role) { const type = effectiveFoodTypeForItem(item); if (!type) return false; if (role === "food") return ["sweet", "food", "grass", "ant_corpse", "zunchi"].includes(type) || (typeof isServingFoodType === "function" && isServingFoodType(type)); - if (role === "drink") return type === "water" || type === "water_bowl" || type === "zunda_juice"; - if (role === "medicine") return type === "sweet" || type === "water" || type === "water_bowl" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)); + if (role === "drink") return type === "water" || type === "zunda_juice"; + if (role === "medicine") return type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)); return false; } @@ -66,7 +66,7 @@ function foodPriorityRank(tarinai, item) { const socialNeed = Number(tarinai?.needRaw?.social ?? tarinai?.needs?.social ?? 0) || 0; const healthNeed = Number(tarinai?.needRaw?.health ?? tarinai?.needs?.health ?? 0) || 0; if (type === "sweet") return 4100 + Math.max(0, healthNeed - 40) * 6; - if (type === "water" || type === "water_bowl") return 3650 + Math.max(0, healthNeed - 42) * 7; + if (type === "water") return 3650 + Math.max(0, healthNeed - 42) * 7; if (type === "zunda_juice") return 3720 + Math.max(0, healthNeed - 42) * 6; if (type === "love_mochi") return 3300 + Math.max(0, socialNeed - 45) * 7; if (type === "fight_mochi") return 2600 + Math.max(0, Number(tarinai?.fightMochiTimer || 0)) * 4; @@ -103,7 +103,7 @@ function hasBetterFoodThanZunchiNearby(tarinai, world, maxDist = 180) { if (tarinai.shouldAvoidTarget?.(it)) continue; const type = effectiveFoodTypeForItem(it); if (!type) continue; - if (["sweet", "food", "grass", "water", "water_bowl", "zunda_juice", "ant_corpse"].includes(type) || (typeof isServingFoodType === "function" && isServingFoodType(type))) { + if (["sweet", "food", "grass", "water", "zunda_juice", "ant_corpse"].includes(type) || (typeof isServingFoodType === "function" && isServingFoodType(type))) { if (it.type === "grass" && (it.amount || 0) <= 0) continue; if (typeof tarinai.foodItemHasServingLeft === "function" && ["sweet", "food", "zunda_juice"].includes(type) && !tarinai.foodItemHasServingLeft(it)) continue; return true; diff --git a/js/tarinai_needs_items.js b/js/tarinai_needs_items.js index e5fa49c..8cbe6ed 100644 --- a/js/tarinai_needs_items.js +++ b/js/tarinai_needs_items.js @@ -541,14 +541,17 @@ function shouldWakeFromSleep(tarinai, world, needs) { if (!tarinai || tarinai.dead) return true; if (tarinai.sleepDisease) return false; const now = world?.time || 0; - const session = tarinai.sleepSession || { startedAt: now, minDuration: 12, targetEnergy: 82, maxDuration: 38 }; - const elapsed = Math.max(0, now - (Number(session.startedAt) || now)); + const session = tarinai.sleepSession || { startedAt: now, minDuration: 8, targetEnergy: 74, maxDuration: 28 }; + const started = Number(session.startedAt); + const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now)); if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true; if (findNearbyDanger(world, tarinai, 160)) return true; - // \u6b32\u6c42\u5024\u306e\u81ea\u7136\u5909\u52d5\u3060\u3051\u3067\u306f\u7761\u7720\u3092\u4e2d\u65ad\u3057\u306a\u3044\u3002\u76f4\u63a5\u306e\u5371\u967a\u30fb\u30c0\u30e1\u30fc\u30b8\u306e\u307f\u8d77\u5e8a\u8981\u56e0\u306b\u3059\u308b\u3002 - if (elapsed < (Number(session.minDuration) || 12)) return false; - if ((tarinai.energy || 0) >= (Number(session.targetEnergy) || 84)) return true; - if (elapsed >= (Number(session.maxDuration) || 42)) return true; + // 自然な睡眠は短い安全ロック後、十分に回復したら起きる。 + if (elapsed < (Number(session.minDuration) || 8)) return false; + const sleepNeed = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0; + if (sleepNeed <= ((typeof needThreshold === "function" ? needThreshold("sleep", "continue") : 34) + 4)) return true; + if ((tarinai.energy || 0) >= (Number(session.targetEnergy) || 74)) return true; + if (elapsed >= (Number(session.maxDuration) || 28)) return true; return false; } @@ -557,10 +560,17 @@ function protectSleepSession(tarinai, world, needs) { tarinai.sleeping = true; if (!tarinai.sleepSession) { const now = world?.time || 0; - tarinai.sleepSession = { startedAt: now, minDuration: rand(12, 22), targetEnergy: rand(80, 92), maxDuration: rand(34, 54), targetId: tarinai.target?.id || null, consumesGrassBedOnWake: tarinai.target?.type === "grass_bed" }; + const bedLike = tarinai.target && typeof tarinai.isSleepFurniture === "function" && tarinai.isSleepFurniture(tarinai.target); + tarinai.sleepSession = { startedAt: now, minDuration: rand(bedLike ? 8 : 7, bedLike ? 14 : 12), targetEnergy: rand(bedLike ? 70 : 64, bedLike ? 82 : 76), maxDuration: rand(bedLike ? 22 : 18, bedLike ? 34 : 28), targetId: tarinai.target?.id || null, consumesGrassBedOnWake: tarinai.target?.type === "grass_bed" }; if (tarinai.target?.type === "grass_bed") tarinai.pendingGrassBedWakeId = tarinai.target.id || tarinai.pendingGrassBedWakeId || ""; } if (!shouldWakeFromSleep(tarinai, world, needs)) { + const step = Math.max(0.10, Math.min(1.0, Number(tarinai.aiTimer || 0.42) || 0.42)); + const bedLike = tarinai.target && typeof tarinai.isSleepFurniture === "function" && tarinai.isSleepFurniture(tarinai.target); + const comfort = bedLike ? Math.max(0.5, Math.min(1.4, Number(world?.bedComfort?.(tarinai.target) || tarinai.target?.comfort || 1) || 1)) : 0.55; + const energyMax = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100)); + tarinai.energy = Math.min(energyMax, (Number(tarinai.energy || 0) || 0) + step * (bedLike ? (1.2 + comfort * 1.35) : 0.65)); + if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { sleep: -step * (bedLike ? (5.2 + comfort * 4.5) : 3.2), safety: bedLike ? -step * 0.9 : 0 }); tarinai.thought = tarinai.thought || "\u7720\u3063\u3066\u3044\u308b"; setBehavior(tarinai, { id: "sleep", need: "sleep", label: "\u7720\u3063\u3066\u3044\u308b", reason: "\u306d\u3080\u3044\u306e\u3067\u3001\u7720\u3063\u3066\u3044\u308b\u3002", text: "\u7720\u3063\u3066\u3044\u308b", state: "sleep", tiedNeeds: ["sleep"], startedAt: tarinai.sleepSession.startedAt, minDuration: tarinai.sleepSession.minDuration, lockSeconds: tarinai.sleepSession.minDuration, phase: "perform" }); tarinai.behaviorLockTimer = Math.max(tarinai.behaviorLockTimer || 0, 0.8); diff --git a/js/tarinai_social_move_life.js b/js/tarinai_social_move_life.js index 109e048..01c204b 100644 --- a/js/tarinai_social_move_life.js +++ b/js/tarinai_social_move_life.js @@ -266,6 +266,7 @@ const dx = spot.x - this.x; const dy = spot.y - this.y; const d = Math.hypot(dx, dy); + if (externalActive && d > 8.5 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "動かされて目が覚めた")) return; if (d > 2.2) { const restore = externalActive ? (2.0 + bedComfort * 2.0) : (12 + bedComfort * 14); const maxRestore = externalActive ? Math.min(d, 3.2) : d; @@ -297,6 +298,7 @@ marker?.markSleepExternalMotion?.(this, dt, "sleep-full-contact"); const cdx = (Number(this.x || 0) || 0) - beforeContactX; const cdy = (Number(this.y || 0) || 0) - beforeContactY; + if (Math.hypot(cdx, cdy) > 1.8 && !this.sleepDisease && marker?.wakeSleepingFromExternalMotion?.(this, "ぶつかって目が覚めた")) return; if (Math.hypot(cdx, cdy) > 0.05) { this.vx = clamp((Number(this.vx) || 0) + cdx / Math.max(0.016, dt) * 0.085, -170, 170); this.vy = clamp((Number(this.vy) || 0) + cdy / Math.max(0.016, dt) * 0.085, -170, 170); diff --git a/js/tarinai_update_step_frame.js b/js/tarinai_update_step_frame.js index f792513..e3a07fd 100644 --- a/js/tarinai_update_step_frame.js +++ b/js/tarinai_update_step_frame.js @@ -160,6 +160,12 @@ t.hunger += CONFIG.hungerPerMinute * minute * t.trait.hunger * hungerScale * (1 + tooCold * 0.8); t.loneliness += CONFIG.lonelinessPerMinute * minute * (2 - t.trait.social); t.energy -= CONFIG.energyPerMinute * minute * t.trait.sleep * (t.state === "sleep" ? 0.025 : 1); + if (t.state === "sleep" || t.sleeping) { + const sleepBedLike = t.target && typeof t.isSleepFurniture === "function" && t.isSleepFurniture(t.target); + const sleepEnergyMax = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100)); + t.energy = Math.min(sleepEnergyMax, (Number(t.energy || 0) || 0) + dt * (sleepBedLike ? 0.52 : 0.24)); + if (typeof applyNeedRelief === "function") applyNeedRelief(t, { sleep: -dt * (sleepBedLike ? 2.0 : 1.1) }); + } // stress is derived from needs; no independent stress decay. if (tooCold > 0) applyNeedShock(t, { health: tooCold * dt * 4, safety: tooCold * dt * 2 }); diff --git a/js/tarinai_update_step_movement.js b/js/tarinai_update_step_movement.js index b1ac843..8dc14ca 100644 --- a/js/tarinai_update_step_movement.js +++ b/js/tarinai_update_step_movement.js @@ -66,6 +66,34 @@ return Boolean(t && (t.state === "sleep" || t.sleeping) && now < Number(t._sleepExternalMotionUntil || 0)); } + function wakeSleepingFromExternalMotion(t, reason = "外力で目が覚めた") { + if (!t || t.dead || t.sleepDisease || !(t.state === "sleep" || t.sleeping)) return false; + const world = t.world || null; + if (world?.isTarinaiHiddenInNestBox?.(t)) return false; + const now = Number(world?.time || 0) || 0; + if (now < Number(t._sleepExternalWakeSuppressUntil || -999)) return false; + t._sleepExternalWakeSuppressUntil = now + 0.45; + t._sleepExternalMotionUntil = 0; + t._sleepExternalMotionReason = ""; + t.consumePendingGrassBedOnWake?.("外力で起きた"); + t.sleeping = false; + t.sleepSession = null; + t.target = null; + t.targetKey = ""; + t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 5.2); + t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.45); + t.fearTimer = Math.max(t.fearTimer || 0, 0.18); + t.setActionState?.("idle", { target: null, clearTarget: true, reason, wake: true, sleeping: false }); + t.thought = reason; + t.pinchThoughtTimer = Math.max(t.pinchThoughtTimer || 0, 1.0); + if (t.bubble && now >= Number(t._sleepExternalWakeBubbleAt || -999) + 0.6) { + t._sleepExternalWakeBubbleAt = now; + t.bubble("!", 0.55, "rgba(88,82,118,0.78)"); + } + world?.emit?.("tarinai:woke", { tarinai: t, reason: "external-motion" }); + return true; + } + function passivePhysicalMotion(t, dt, context = {}) { if (!t || t.dead) return { done: true }; const world = t.world; @@ -182,6 +210,14 @@ world.resolveFenceCollision(t); } } + + if (state === "sleep" || t.sleeping) { + const finalDx = numeric(t.x) - beforeX; + const finalDy = numeric(t.y) - beforeY; + const rawSpeed = Math.hypot(numeric(t.vx) + numeric(t.impulseVx), numeric(t.vy) + numeric(t.impulseVy)); + const movedByExternal = Math.hypot(finalDx, finalDy) > 4.2 || rawSpeed > 26 || (sleepExternalMotionActive(t) && Math.hypot(finalDx, finalDy) > 2.2); + if (movedByExternal) wakeSleepingFromExternalMotion(t, "動かされて目が覚めた"); + } return { done: false, passive: true }; } @@ -236,5 +272,5 @@ return { done: false }; } - global.TarinaiMovementUpdateStep = Object.freeze({ update, applyVelocityShockDamage, passivePhysicalMotion, markSleepExternalMotion, sleepExternalMotionActive }); + global.TarinaiMovementUpdateStep = Object.freeze({ update, applyVelocityShockDamage, passivePhysicalMotion, markSleepExternalMotion, sleepExternalMotionActive, wakeSleepingFromExternalMotion }); })(typeof window !== "undefined" ? window : globalThis); diff --git a/js/text_catalog.js b/js/text_catalog.js index aad96c1..0ea7ea7 100644 --- a/js/text_catalog.js +++ b/js/text_catalog.js @@ -56,7 +56,7 @@ const TEXT_CATALOG = { { title: "コロニーの見方", image: "assets/sprites/tarinai_11_weak.webp", - text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。たりないデータでは選択中の個体の健康、性格、関係、行動理由、記録を追えます。", + text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。観察では選択中の個体の健康、性格、関係、行動理由、記録を追えます。", }, ],}; diff --git a/js/ui_input_mouse.js b/js/ui_input_mouse.js index ff92473..6d212a8 100644 --- a/js/ui_input_mouse.js +++ b/js/ui_input_mouse.js @@ -29,6 +29,13 @@ }, 90); return; } + if (e.button === 0 && world.tool === "shoot") { + const p = ctx.screenToWorldEvent(e); + if (!p.inside) return; + e.preventDefault(); + ctx.beginShoot(p, e.clientX, e.clientY); + return; + } if (e.button === 0 && world.tool === "pinch") { const p = ctx.screenToWorldEvent(e); if (!p.inside) return; @@ -45,6 +52,7 @@ window.addEventListener("mouseup", (e) => { if (ctx.endAreaDelete()) return; if (ctx.endHose()) { if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } return; } + if (ctx.endShoot()) return; if (ctx.endGrab(e.clientX, e.clientY)) return; ctx.endPan(e.button); }); @@ -60,6 +68,11 @@ ctx.moveHose(p, e.clientX, e.clientY); return; } + if (uiCache.shooting) { + const p = ctx.screenToWorldEvent(e); + ctx.moveShoot(p, e.clientX, e.clientY); + return; + } if (uiCache.grabbing) { const p = ctx.screenToWorldEvent(e); const totalMove = Math.hypot(e.clientX - (uiCache.grabStartX || e.clientX), e.clientY - (uiCache.grabStartY || e.clientY)); @@ -83,6 +96,10 @@ uiCache.grabMoved = false; return; } + if (uiCache.shootSuppressClick) { + uiCache.shootSuppressClick = false; + return; + } if (uiCache.panMoved) { uiCache.panMoved = false; return; @@ -98,6 +115,9 @@ canvas.addEventListener("mouseleave", () => { world.pointer.inside = false; world.pointer.motion = 0; + world.hoverGrabTarget = null; + world.hoverDeleteTarget = null; + world.hoverShootTarget = null; }); return true; } diff --git a/js/ui_input_shared.js b/js/ui_input_shared.js index 06d39cc..2994808 100644 --- a/js/ui_input_shared.js +++ b/js/ui_input_shared.js @@ -3,6 +3,7 @@ (function (global) { function cursorForTool(tool) { if (tool === "water_hose") return "crosshair"; + if (tool === "shoot") return "none"; if (tool === "pinch") return "grab"; if (tool === "observe") return "default"; if (tool === "poke" || tool === "delete" || tool === "area_delete") return "pointer"; @@ -328,6 +329,89 @@ return true; } + function shootAimPoint() { + return world.shootAimPoint?.() || { x: world.pointer?.x || 0, y: world.pointer?.y || 0, inside: Boolean(world.pointer?.inside) }; + } + + function updateShootHoverTarget(point = null) { + // 射撃ツールは照準だけを表示し、対象ハイライトは出さない。 + if (world.hoverShootTarget) { + world.hoverShootTarget = null; + world.drawListDirty = true; + } + return null; + } + + function beginShoot(p, clientX, clientY, options = {}) { + if (!p?.inside) return false; + global.TarinaiHistory?.capture?.(world, "shoot"); + uiCache.shooting = true; + uiCache.shootMoved = false; + uiCache.shootSuppressClick = true; + uiCache.shootFired = false; + uiCache.shootStartX = clientX; + uiCache.shootStartY = clientY; + uiCache.shootLastClientX = clientX; + uiCache.shootLastClientY = clientY; + world.pointer.x = p.x; + world.pointer.y = p.y; + world.pointer.inside = true; + world.setShootCursorFromPointer?.(p); + uiCache.shootLastRawWorldX = p.x; + uiCache.shootLastRawWorldY = p.y; + updateShootHoverTarget(); + uiCache.shootFired = Boolean(world.fireShootTool?.()) || Boolean(uiCache.shootFired); + if (options.touchMode) uiCache.touchMode = options.touchMode; + if (uiCache.shootHoldTimer) clearInterval(uiCache.shootHoldTimer); + uiCache.shootHoldTimer = setInterval(() => { + if (!uiCache.shooting) { + clearInterval(uiCache.shootHoldTimer); + uiCache.shootHoldTimer = null; + return; + } + uiCache.shootFired = Boolean(world.fireShootTool?.()) || Boolean(uiCache.shootFired); + }, 120); + canvas.style.cursor = "none"; + render(); + return true; + } + + function moveShoot(p, clientX, clientY, options = {}) { + uiCache.shootLastClientX = clientX; + uiCache.shootLastClientY = clientY; + const rawDx = (Number(p.x) || 0) - (Number(uiCache.shootLastRawWorldX ?? p.x) || 0); + const rawDy = (Number(p.y) || 0) - (Number(uiCache.shootLastRawWorldY ?? p.y) || 0); + uiCache.shootLastRawWorldX = p.x; + uiCache.shootLastRawWorldY = p.y; + world.pointer.x = p.x; + world.pointer.y = p.y; + world.pointer.inside = !!p.inside; + if (p.inside) world.moveShootCursorBy?.(rawDx, rawDy); + const totalMove = Math.hypot(clientX - (uiCache.shootStartX || clientX), clientY - (uiCache.shootStartY || clientY)); + if (totalMove > 3) { + uiCache.shootMoved = true; + if (options.touch) uiCache.touchMoved = true; + } + updateShootHoverTarget(); + canvas.style.cursor = "none"; + render(); + return true; + } + + function endShoot() { + if (!uiCache.shooting) return false; + uiCache.shooting = false; + if (uiCache.shootHoldTimer) { + clearInterval(uiCache.shootHoldTimer); + uiCache.shootHoldTimer = null; + } + if (uiCache.shootFired) audio.boltAction?.(); + uiCache.shootFired = false; + updateShootHoverTarget(); + canvas.style.cursor = cursorForTool(world.tool); + renderStats(); + return true; + } function beginAreaDelete(p, clientX, clientY, options = {}) { @@ -417,6 +501,50 @@ const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0)); world.pointer.motion = clamp(delta / 18, 0, 2); world.pointer.movedAt = performance.now(); + if (world.tool === "shoot") { + if (p.inside) { + if (world.shootCursorActive) { + const lastRawX = Number.isFinite(Number(world.shootPointerRawX)) ? Number(world.shootPointerRawX) : prevX; + const lastRawY = Number.isFinite(Number(world.shootPointerRawY)) ? Number(world.shootPointerRawY) : prevY; + world.moveShootCursorBy?.((Number(p.x) || 0) - (Number(lastRawX) || 0), (Number(p.y) || 0) - (Number(lastRawY) || 0)); + } else { + world.setShootCursorFromPointer?.(p); + } + world.shootPointerRawX = p.x; + world.shootPointerRawY = p.y; + } + updateShootHoverTarget(); + canvas.style.cursor = "none"; + } else if (world.tool === "pinch" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) { + const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null; + if (world.hoverGrabTarget !== target) { + world.hoverGrabTarget = target || null; + world.drawListDirty = true; + } + 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) { + const target = world.findDeleteToolTargetAt ? world.findDeleteToolTargetAt(p.x, p.y) : null; + if (world.hoverDeleteTarget !== target) { + world.hoverDeleteTarget = target || null; + world.drawListDirty = true; + } + if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; } + canvas.style.cursor = target ? "pointer" : "default"; + } else { + if (world.hoverGrabTarget) { + world.hoverGrabTarget = null; + world.drawListDirty = true; + } + if (world.hoverDeleteTarget) { + world.hoverDeleteTarget = null; + world.drawListDirty = true; + } + if (world.hoverShootTarget) { + world.hoverShootTarget = null; + world.drawListDirty = true; + } + } } function cancelPointerActions() { @@ -424,10 +552,18 @@ uiCache.touchMoved = false; uiCache.hosing = false; if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } + uiCache.shooting = false; + uiCache.shootFired = false; + if (uiCache.shootHoldTimer) { clearInterval(uiCache.shootHoldTimer); uiCache.shootHoldTimer = null; } uiCache.areaDeleting = false; uiCache.grabbing = false; uiCache.grabTarget = null; uiCache.grabKind = ""; + if (world) { + world.hoverGrabTarget = null; + world.hoverDeleteTarget = null; + world.hoverShootTarget = null; + } } return { @@ -453,6 +589,9 @@ beginHose, moveHose, endHose, + beginShoot, + moveShoot, + endShoot, beginAreaDelete, moveAreaDelete, endAreaDelete, diff --git a/js/ui_input_touch.js b/js/ui_input_touch.js index 16f6d56..fa3a33b 100644 --- a/js/ui_input_touch.js +++ b/js/ui_input_touch.js @@ -41,6 +41,10 @@ uiCache.touchMode = "toolTapOnly"; return; } + if (world.tool === "shoot" && p.inside) { + ctx.beginShoot(p, t.clientX, t.clientY, { touchMode: "shoot" }); + return; + } if (world.tool === "area_delete" && p.inside) { ctx.beginAreaDelete(p, t.clientX, t.clientY, { touchMode: "areaDelete" }); return; @@ -95,6 +99,10 @@ if (totalMove > 8) uiCache.touchMoved = true; return; } + if (uiCache.touchMode === "shoot" && uiCache.shooting) { + ctx.moveShoot(p, t.clientX, t.clientY, { touch: true }); + return; + } if (uiCache.touchMode === "areaDelete" && uiCache.areaDeleting) { ctx.moveAreaDelete(p, t.clientX, t.clientY, { touch: true }); return; @@ -121,6 +129,7 @@ e.preventDefault(); if (uiCache.areaDeleting) ctx.endAreaDelete(); if (uiCache.hosing) { ctx.endHose(); if (uiCache.hoseHoldTimer) { clearInterval(uiCache.hoseHoldTimer); uiCache.hoseHoldTimer = null; } } + if (uiCache.shooting) ctx.endShoot(); if (uiCache.grabbing) { ctx.endGrab(e.changedTouches?.[0]?.clientX || uiCache.touchLastX || 0, e.changedTouches?.[0]?.clientY || uiCache.touchLastY || 0); } else if (uiCache.touchMode === "toolTapOnly" && !uiCache.touchMoved) { diff --git a/js/ui_layout_dialogs.js b/js/ui_layout_dialogs.js index 331ef1b..3d0bfcf 100644 --- a/js/ui_layout_dialogs.js +++ b/js/ui_layout_dialogs.js @@ -138,7 +138,7 @@ function selectTool(tool) { btn.classList.toggle("selected", btn.dataset.tool === activeTool); } syncToolSizeBadges(); - canvas.style.cursor = activeTool === "observe" ? "default" : (activeTool === "pinch" ? "grab" : (activeTool === "poke" || activeTool === "delete" ? "pointer" : "crosshair")); + canvas.style.cursor = window.TarinaiUIInputShared?.cursorForTool?.(activeTool) || (activeTool === "observe" ? "default" : (activeTool === "pinch" ? "grab" : (activeTool === "poke" || activeTool === "delete" ? "pointer" : "crosshair"))); } function showToast(text) { diff --git a/js/ui_selected.js b/js/ui_selected.js index 75b89e4..0e1a0a5 100644 --- a/js/ui_selected.js +++ b/js/ui_selected.js @@ -188,7 +188,7 @@ function renderSelected() { uiCache.selectedEmpty = true; uiCache.selectedEmptyVisible = showEmpty; ui.selectedInfo.className = "selected-info empty"; - ui.selectedInfo.textContent = "\u672a\u9078\u629e"; + ui.selectedInfo.textContent = "たりないを選択してください"; return; } @@ -224,7 +224,7 @@ function renderSelected() { ["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), fmt((t.growth || 0) * 100), itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","), selectedInventoryItems(t).join(","), - (t.records || []).slice(0, 6).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~") + (t.records || []).slice(0, 7).map(r => `${Math.round(r.time || 0)}:${r.kind || ""}:${r.text || ""}`).join("~") ].join("|"); if (uiCache.selectedSnapshot === snapshot) return; uiCache.selectedSnapshot = snapshot; @@ -249,11 +249,11 @@ function renderSelected() {
\u73fe\u5728\u6027\u683c${personalityRowsHtml(t.currentPersonality)}
\u4eca\u65e5\u306e\u5909\u5316
    ${recentChangeHtml}
`; - const recordHtml = (t.records || []).slice(0, 8).map(r => { + const recentMemoryHtml = (t.records || []).slice(0, 7).map(r => { const timeText = world.clockString ? (world.clockStringFromTime ? world.clockStringFromTime(r.time || 0) : formatRecordTime(r.time || 0)) : formatRecordTime(r.time || 0); const recordText = quoteNameInRecordText(r.text || "", t.name); return `
  • ${escapeHtml(timeText)}${escapeHtml(recordText)}
  • `; - }).join("") || `
  • -\u8a18\u9332\u306a\u3057
  • `; + }).join("") || `
  • -まだ記憶がありません
  • `; const basicHtml = `
    \u4e16\u4ee3${escapeHtml(generationDisplay || "\u4e0d\u660e")}
    \u5e74\u9f62${fmt(t.age)} / ${fmt(t.lifeSpan)}
    @@ -296,7 +296,7 @@ function renderSelected() { ${selectedDataCategoryHtml("personality", "\u6027\u683c", personalityHtml)} ${selectedDataCategoryHtml("relation", "\u95a2\u4fc2\u3068\u5bb6\u65cf", relationHtml)} ${selectedDataCategoryHtml("inventory", "\u6240\u6301\u54c1", selectedInventoryHtml(t))} - ${selectedDataCategoryHtml("records", "\u8a18\u9332", `
      ${recordHtml}
    `)} + ${selectedDataCategoryHtml("memories", "最近の記憶", `
      ${recentMemoryHtml}
    `)}
    `; diff --git a/js/version.js b/js/version.js index 20aa6af..14b453d 100644 --- a/js/version.js +++ b/js/version.js @@ -1,8 +1,8 @@ "use strict"; (function () { - const APP_VERSION = "39.15.18"; - const APP_BUILD = "tarinai-nearby-constraint-reduce-v39-15-18"; + const APP_VERSION = "39.15.27"; + const APP_BUILD = "tarinai-grab-sleep-fence-perf-v39-15-19"; const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const STATIC_VERSION_PARAM = `v=${APP_VERSION}`; diff --git a/js/world.js b/js/world.js index 17572cc..91f052e 100644 --- a/js/world.js +++ b/js/world.js @@ -40,6 +40,17 @@ class World { constructor() { this.relationNotices = {}; this.resolvedFightIds = {}; this.pointer = { x: 0, y: 0, inside: false, motion: 0, movedAt: 0 }; + this.hoverGrabTarget = null; + this.hoverDeleteTarget = null; + this.hoverShootTarget = null; + this.shootCursorActive = false; + this.shootCursorX = 0; + this.shootCursorY = 0; + this.shootPointerRawX = 0; + this.shootPointerRawY = 0; + this.shootCursorOffsetX = 0; + this.shootCursorOffsetY = 0; + this.lastShootAt = -999; this.lastPhase = ""; this.weather = "sunny"; this.worldSeed = window.TarinaiSeedFactory?.createWorldSeed?.() || `w${Date.now().toString(36)}`; diff --git a/js/world_combat_effects.js b/js/world_combat_effects.js index 594f374..ecc99e8 100644 --- a/js/world_combat_effects.js +++ b/js/world_combat_effects.js @@ -475,8 +475,19 @@ }, applyImpactDamage(target, amount, cause = "\u885d\u7a81", options = {}) { if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false; - const normalized = options.cause || cause || "\u885d\u7a81"; - const physicalHit = String(normalized).includes("衝突") || String(normalized).includes("collision") || String(cause || "").includes("急激な速度変化") || String(normalized).includes("爆竹") || String(normalized).includes("落ちてきた"); + const sourceLabel = (src) => { + if (!src) return ""; + if (src.name) return String(src.name); + if (src.type && typeof toolLabel === "function") return toolLabel(src.type); + if (src.type) return String(src.type); + return ""; + }; + let normalized = options.cause || cause || "\u885d\u7a81"; + const rawCause = String(cause || ""); + const isPlainCollision = String(normalized).includes("衝突") || String(normalized).includes("collision") || rawCause.includes("急激な速度変化"); + const label = sourceLabel(options.source); + if (isPlainCollision && label) normalized = `${label}に衝突`; + const physicalHit = String(normalized).includes("衝突") || String(normalized).includes("collision") || rawCause.includes("急激な速度変化") || String(normalized).includes("爆竹") || String(normalized).includes("落ちてきた"); if (target instanceof Tarinai && (target.birthRitualTimer || 0) > 0.04 && physicalHit && amount >= (options.birthCancelDamageThreshold ?? 8)) { this.cancelBirthRitualOnForce?.(target, Math.max(180, amount * 18), { reason: "強い衝撃で繁殖が中断された", @@ -586,7 +597,7 @@ const radius = Math.max(32, Math.min(220, (it.r || 14) * 1.35 + impactSpeed * 0.055)); const damage = physicalDamageFromImpactSpeed(impactSpeed, { min: 1.2, max: isGenkotsu ? 58 : 34, scale: 8.6 }); const push = clamp((impactSpeed - 120) * 0.62, 42, isGenkotsu ? 1040 : 260); - const reason = impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"; + const reason = isStone ? "石に潰された" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"); if (isGenkotsu) { it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); } let hit = 0; for (const t of this.nearbyTarinai(it.x, it.y, radius + 54)) { @@ -838,7 +849,7 @@ fearTimer: 0.95, thought: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", }); - this.applyImpactDamage(t, damage, "\u885d\u7a81"); + this.applyImpactDamage(t, damage, "\u885d\u7a81", { source: ball }); const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0); const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18); if (t.addStress) t.addStress(stressGain, { threshold: 8 }); @@ -998,8 +1009,8 @@ }); impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10); impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10); - this.applyImpactDamage(target, damage, "衝突"); - if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "衝突"); + this.applyImpactDamage(target, damage, "衝突", { source: impactor }); + if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "衝突", { source: target }); for (const t of [target, impactor]) { if (!t || t.dead) continue; if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "高速でぶつかって混乱している", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" }); diff --git a/js/world_environment.js b/js/world_environment.js index 2c79e91..1e8fe4f 100644 --- a/js/world_environment.js +++ b/js/world_environment.js @@ -38,6 +38,13 @@ return Math.max(0, distXY(x, y, Number(it.x) || 0, Number(it.y) || 0) - reach); } + function rectDistanceToPointLocal(rect, x, y) { + if (!rect) return Infinity; + const px = Math.max(Number(rect.left || 0), Math.min(Number(rect.right || 0), Number(x) || 0)); + const py = Math.max(Number(rect.top || 0), Math.min(Number(rect.bottom || 0), Number(y) || 0)); + return Math.hypot((Number(x) || 0) - px, (Number(y) || 0) - py); + } + function visitNearestObstaclesLocal(x, y, candidates, limit, visitor, opts = {}) { if (!candidates || typeof visitor !== "function") return 0; const source = candidates; @@ -476,8 +483,9 @@ return Boolean(aroundDoor || inDoorColumn); }, - nearbySolidObstacleRects(x, y, radius, { include = null, exclude = null, maxChecks = CONFIG.maxFenceCollisionChecks ?? 24 } = {}) { + nearbySolidObstacleRects(x, y, radius, { include = null, exclude = null, maxChecks = CONFIG.maxFenceCollisionChecks ?? 24, maxRects: requestedMaxRects = 0 } = {}) { const limit = Math.max(1, Number(maxChecks || 0) || 24); + const maxRects = Math.max(12, Math.min(72, Number(requestedMaxRects || 0) || Math.ceil(limit * 2.25))); const cacheable = !include && !exclude; const stats = this.nearbyQueryStatsThisFrame || null; const q = 48; @@ -487,9 +495,9 @@ const qcy = (qy + 0.5) * q; const inflatedRadius = Math.max(1, Number(radius || 0) || 1) + q * 0.82; const qr = Math.ceil(inflatedRadius / q); - const cacheLimit = Math.min(96, Math.max(limit + 8, Math.ceil(limit * 1.55))); + const cacheLimit = Math.min(72, Math.max(limit + 6, Math.ceil(limit * 1.35))); const cacheKey = cacheable - ? `${this.spatialVersion || 0}:${this.spatialDirtyMarksTotal || 0}:${qx},${qy},${qr},${cacheLimit}` + ? `${this.spatialVersion || 0}:${this.spatialDirtyMarksTotal || 0}:${qx},${qy},${qr},${cacheLimit},${maxRects}` : ""; if (cacheable) { if (!this._solidObstacleRectQueryCache) this._solidObstacleRectQueryCache = new Map(); @@ -516,7 +524,16 @@ if (!partRects.length) return false; it._obstacleRectQueryStamp = stamp; checked += 1; - for (const rect of partRects) rects.push(rect); + const localLimit = it.type === "rotator" || it.type === "poison_block" || it.type === "reciprocator" ? maxRects : maxRects + 8; + for (const rect of partRects) { + if (rects.length >= localLimit) break; + // Dense custom mechanical shapes can produce dozens of segment rects. + // Keep only rects that can plausibly interact with this query point; + // otherwise one smooth-motion collision probe spends most of its time + // iterating far-away segments of the same object. + if (rectDistanceToPointLocal(rect, queryX, queryY) > queryRadius + 42) continue; + rects.push(rect); + } return true; }; const candidates = this.nearbyObstacles?.(queryX, queryY, queryRadius, false) || this.nearbyItems?.(queryX, queryY, queryRadius, false) || []; diff --git a/js/world_placement_log.js b/js/world_placement_log.js index 4f4a77c..a50f508 100644 --- a/js/world_placement_log.js +++ b/js/world_placement_log.js @@ -232,7 +232,7 @@ const poweredLabel = poweredInput?.closest?.("label"); if (speedLabel) speedLabel.style.display = isPoisonBlock ? "none" : ""; if (poweredLabel) poweredLabel.style.display = isPoisonBlock ? "none" : ""; - if (poisonCollisionLabel) poisonCollisionLabel.style.display = isPoisonBlock ? "" : "none"; + if (poisonCollisionLabel) poisonCollisionLabel.style.display = ""; if (poisonCollisionInput) poisonCollisionInput.checked = phyScalar(item, "solid", true) !== false; if (speed) speed.value = String(Math.max(-360, Math.min(360, deg))); if (speedNum) speedNum.value = String(deg); @@ -405,8 +405,8 @@ cleanSegments(); physicsApi()?.setSegments?.(item, segments.map(seg => seg.slice(0, 4)), isPoisonBlock ? "poison-shape-edit" : "rotator-shape-edit"); phySet(item, "thickness", Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || (isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12))), "editor-thickness"); + phySet(item, "solid", poisonCollisionInput ? Boolean(poisonCollisionInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid"); if (isPoisonBlock) { - phySet(item, "solid", poisonCollisionInput ? Boolean(poisonCollisionInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid"); phySet(item, "damage", Math.max(1, Number(phyScalar(item, "damage", 7) || 7) || 7), "editor-damage"); } else { phySet(item, "motorSpeed", (Number(speedNum?.value || speed?.value || 0) || 0) * Math.PI / 180, "editor-speed"); @@ -452,6 +452,7 @@ +