This commit is contained in:
33333-33333 2026-06-27 21:50:05 +09:00
commit 5ed977788c
49 changed files with 1084 additions and 392 deletions

View file

@ -1,5 +1,5 @@
{
"version": "39.15.18",
"version": "39.15.27",
"css": [
"css/base.css",
"css/layout.css",

Binary file not shown.

Binary file not shown.

BIN
assets/ui/tool_shoot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -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"); }

View file

@ -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));

View file

@ -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; }

View file

@ -7,11 +7,11 @@
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
<link rel="stylesheet" href="css/base.css?v=39.15.18" />
<link rel="stylesheet" href="css/layout.css?v=39.15.18" />
<link rel="stylesheet" href="css/panel.css?v=39.15.18" />
<link rel="stylesheet" href="css/components.css?v=39.15.18" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.18" />
<link rel="stylesheet" href="css/base.css?v=39.15.27" />
<link rel="stylesheet" href="css/layout.css?v=39.15.27" />
<link rel="stylesheet" href="css/panel.css?v=39.15.27" />
<link rel="stylesheet" href="css/components.css?v=39.15.27" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.27" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -66,17 +66,17 @@
<aside class="panel">
<nav id="panelQuickTabs" class="panel-quick-tabs" aria-label="右メニューのタブ">
<button type="button" data-panel-tab="selected">たりないデータ</button>
<button type="button" data-panel-tab="selected">観察</button>
<button type="button" data-panel-tab="tools">道具</button>
<button type="button" data-panel-tab="colony">コロニー</button>
<button type="button" data-panel-tab="events">イベント</button>
</nav>
<section id="selectedCard" class="card selected-card hidden">
<div class="selected-head">
<h2>たりないデータ</h2>
<h2>観察</h2>
<button id="selectedCloseBtn" class="selected-close" type="button" aria-label="&#x89B3;&#x5BDF;&#x3092;&#x3084;&#x3081;&#x308B;">&times;</button>
</div>
<div id="selectedInfo" class="selected-info empty">&#x672A;&#x9078;&#x629E;</div>
<div id="selectedInfo" class="selected-info empty">たりないを選択してください</div>
</section>
<section id="toolCard" class="card tool-card collapsible-card" data-card-key="tools">
@ -189,141 +189,141 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=39.15.18" defer></script>
<script src="js/event_bus.js?v=39.15.18" defer></script>
<script src="js/registry_base.js?v=39.15.18" defer></script>
<script src="js/disease_registry.js?v=39.15.18" defer></script>
<script src="js/sound_pack.js?v=39.15.18" defer></script>
<script src="js/item_registry.js?v=39.15.18" defer></script>
<script src="js/data.js?v=39.15.18" defer></script>
<script src="js/ground_types.js?v=39.15.18" defer></script>
<script src="js/input_mode_manager.js?v=39.15.18" defer></script>
<script src="js/math.js?v=39.15.18" defer></script>
<script src="js/physics_helpers.js?v=39.15.18" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.18" defer></script>
<script src="js/mechanical_system.js?v=39.15.18" defer></script>
<script src="js/constraint_system.js?v=39.15.18" defer></script>
<script src="js/physics_world_system.js?v=39.15.18" defer></script>
<script src="js/assets.js?v=39.15.18" defer></script>
<script src="js/audio.js?v=39.15.18" defer></script>
<script src="js/perf_profiler.js?v=39.15.18" defer></script>
<script src="js/render.js?v=39.15.18" defer></script>
<script src="js/sim_core.js?v=39.15.18" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.18" defer></script>
<script src="js/structures.js?v=39.15.18" defer></script>
<script src="js/items.js?v=39.15.18" defer></script>
<script src="js/item_type_initializers.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_runtime.js?v=39.15.18" defer></script>
<script src="js/item_update_policy.js?v=39.15.18" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.18" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.18" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.18" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.18" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.18" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.18" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.18" defer></script>
<script src="js/item_runtime.js?v=39.15.18" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.18" defer></script>
<script src="js/item_render_runtime.js?v=39.15.18" defer></script>
<script src="js/ants.js?v=39.15.18" defer></script>
<script src="js/health.js?v=39.15.18" defer></script>
<script src="js/tarinai.js?v=39.15.18" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.18" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.18" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.18" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.18" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.18" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.18" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.18" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.18" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.18" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.18" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.18" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.18" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.18" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.18" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.18" defer></script>
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_action_planner_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.18" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.18" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.18" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.18" defer></script>
<script src="js/tarinai_render.js?v=39.15.18" defer></script>
<script src="js/world.js?v=39.15.18" defer></script>
<script src="js/world_view.js?v=39.15.18" defer></script>
<script src="js/family_graph.js?v=39.15.18" defer></script>
<script src="js/world_family_social.js?v=39.15.18" defer></script>
<script src="js/world_combat_effects.js?v=39.15.18" defer></script>
<script src="js/world_environment.js?v=39.15.18" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.18" defer></script>
<script src="js/world_ants_system.js?v=39.15.18" defer></script>
<script src="js/weather_system.js?v=39.15.18" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.18" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.18" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.18" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.18" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.18" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.18" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.18" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.18" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.18" defer></script>
<script src="js/simulation_systems.js?v=39.15.18" defer></script>
<script src="js/world_update_phases.js?v=39.15.18" defer></script>
<script src="js/system_order.js?v=39.15.18" defer></script>
<script src="js/simulation.js?v=39.15.18" defer></script>
<script src="js/world_update.js?v=39.15.18" defer></script>
<script src="js/world_tool_actions.js?v=39.15.18" defer></script>
<script src="js/world_placement_log.js?v=39.15.18" defer></script>
<script src="js/world_event_effects.js?v=39.15.18" defer></script>
<script src="js/command_dispatcher.js?v=39.15.18" defer></script>
<script src="js/text_catalog.js?v=39.15.18" defer></script>
<script src="js/ui.js?v=39.15.18" defer></script>
<script src="js/ui_log.js?v=39.15.18" defer></script>
<script src="js/ui_helpers.js?v=39.15.18" defer></script>
<script src="js/ui_selected.js?v=39.15.18" defer></script>
<script src="js/save_schema.js?v=39.15.18" defer></script>
<script src="js/snapshot_system.js?v=39.15.18" defer></script>
<script src="js/restore_coordinator.js?v=39.15.18" defer></script>
<script src="js/history_system.js?v=39.15.18" defer></script>
<script src="js/save_codec.js?v=39.15.18" defer></script>
<script src="js/save_storage.js?v=39.15.18" defer></script>
<script src="js/save_system.js?v=39.15.18" defer></script>
<script src="js/patch_helpers.js?v=39.15.18" defer></script>
<script src="js/ui_tooltips.js?v=39.15.18" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.18" defer></script>
<script src="js/ui_ground.js?v=39.15.18" defer></script>
<script src="js/ui_tools.js?v=39.15.18" defer></script>
<script src="js/ui_input_shared.js?v=39.15.18" defer></script>
<script src="js/ui_input_touch.js?v=39.15.18" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.18" defer></script>
<script src="js/ui_bind.js?v=39.15.18" defer></script>
<script src="js/ui_charts.js?v=39.15.18" defer></script>
<script src="js/ui_family_data.js?v=39.15.18" defer></script>
<script src="js/ui_family_async.js?v=39.15.18" defer></script>
<script src="js/ui_family_layout.js?v=39.15.18" defer></script>
<script src="js/ui_family_paths.js?v=39.15.18" defer></script>
<script src="js/ui_family_render.js?v=39.15.18" defer></script>
<script src="js/main.js?v=39.15.18" defer></script>
<script src="js/debug_tools.js?v=39.15.18" defer></script>
<script src="js/version.js?v=39.15.27" defer></script>
<script src="js/event_bus.js?v=39.15.27" defer></script>
<script src="js/registry_base.js?v=39.15.27" defer></script>
<script src="js/disease_registry.js?v=39.15.27" defer></script>
<script src="js/sound_pack.js?v=39.15.27" defer></script>
<script src="js/item_registry.js?v=39.15.27" defer></script>
<script src="js/data.js?v=39.15.27" defer></script>
<script src="js/ground_types.js?v=39.15.27" defer></script>
<script src="js/input_mode_manager.js?v=39.15.27" defer></script>
<script src="js/math.js?v=39.15.27" defer></script>
<script src="js/physics_helpers.js?v=39.15.27" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.27" defer></script>
<script src="js/mechanical_system.js?v=39.15.27" defer></script>
<script src="js/constraint_system.js?v=39.15.27" defer></script>
<script src="js/physics_world_system.js?v=39.15.27" defer></script>
<script src="js/assets.js?v=39.15.27" defer></script>
<script src="js/audio.js?v=39.15.27" defer></script>
<script src="js/perf_profiler.js?v=39.15.27" defer></script>
<script src="js/render.js?v=39.15.27" defer></script>
<script src="js/sim_core.js?v=39.15.27" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.27" defer></script>
<script src="js/structures.js?v=39.15.27" defer></script>
<script src="js/items.js?v=39.15.27" defer></script>
<script src="js/item_type_initializers.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_runtime.js?v=39.15.27" defer></script>
<script src="js/item_update_policy.js?v=39.15.27" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.27" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.27" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.27" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.27" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.27" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.27" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.27" defer></script>
<script src="js/item_runtime.js?v=39.15.27" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.27" defer></script>
<script src="js/item_render_runtime.js?v=39.15.27" defer></script>
<script src="js/ants.js?v=39.15.27" defer></script>
<script src="js/health.js?v=39.15.27" defer></script>
<script src="js/tarinai.js?v=39.15.27" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.27" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.27" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.27" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.27" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.27" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.27" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.27" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.27" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.27" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.27" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.27" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.27" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.27" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.27" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.27" defer></script>
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_action_planner_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.27" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.27" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.27" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.27" defer></script>
<script src="js/tarinai_render.js?v=39.15.27" defer></script>
<script src="js/world.js?v=39.15.27" defer></script>
<script src="js/world_view.js?v=39.15.27" defer></script>
<script src="js/family_graph.js?v=39.15.27" defer></script>
<script src="js/world_family_social.js?v=39.15.27" defer></script>
<script src="js/world_combat_effects.js?v=39.15.27" defer></script>
<script src="js/world_environment.js?v=39.15.27" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.27" defer></script>
<script src="js/world_ants_system.js?v=39.15.27" defer></script>
<script src="js/weather_system.js?v=39.15.27" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.27" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.27" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.27" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.27" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.27" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.27" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.27" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.27" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.27" defer></script>
<script src="js/simulation_systems.js?v=39.15.27" defer></script>
<script src="js/world_update_phases.js?v=39.15.27" defer></script>
<script src="js/system_order.js?v=39.15.27" defer></script>
<script src="js/simulation.js?v=39.15.27" defer></script>
<script src="js/world_update.js?v=39.15.27" defer></script>
<script src="js/world_tool_actions.js?v=39.15.27" defer></script>
<script src="js/world_placement_log.js?v=39.15.27" defer></script>
<script src="js/world_event_effects.js?v=39.15.27" defer></script>
<script src="js/command_dispatcher.js?v=39.15.27" defer></script>
<script src="js/text_catalog.js?v=39.15.27" defer></script>
<script src="js/ui.js?v=39.15.27" defer></script>
<script src="js/ui_log.js?v=39.15.27" defer></script>
<script src="js/ui_helpers.js?v=39.15.27" defer></script>
<script src="js/ui_selected.js?v=39.15.27" defer></script>
<script src="js/save_schema.js?v=39.15.27" defer></script>
<script src="js/snapshot_system.js?v=39.15.27" defer></script>
<script src="js/restore_coordinator.js?v=39.15.27" defer></script>
<script src="js/history_system.js?v=39.15.27" defer></script>
<script src="js/save_codec.js?v=39.15.27" defer></script>
<script src="js/save_storage.js?v=39.15.27" defer></script>
<script src="js/save_system.js?v=39.15.27" defer></script>
<script src="js/patch_helpers.js?v=39.15.27" defer></script>
<script src="js/ui_tooltips.js?v=39.15.27" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.27" defer></script>
<script src="js/ui_ground.js?v=39.15.27" defer></script>
<script src="js/ui_tools.js?v=39.15.27" defer></script>
<script src="js/ui_input_shared.js?v=39.15.27" defer></script>
<script src="js/ui_input_touch.js?v=39.15.27" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.27" defer></script>
<script src="js/ui_bind.js?v=39.15.27" defer></script>
<script src="js/ui_charts.js?v=39.15.27" defer></script>
<script src="js/ui_family_data.js?v=39.15.27" defer></script>
<script src="js/ui_family_async.js?v=39.15.27" defer></script>
<script src="js/ui_family_layout.js?v=39.15.27" defer></script>
<script src="js/ui_family_paths.js?v=39.15.27" defer></script>
<script src="js/ui_family_render.js?v=39.15.27" defer></script>
<script src="js/main.js?v=39.15.27" defer></script>
<script src="js/debug_tools.js?v=39.15.27" defer></script>
</body>
</html>

View file

@ -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); },

View file

@ -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;

View file

@ -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 });
}

View file

@ -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;

View file

@ -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);
})();

View file

@ -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,
});
})();

View file

@ -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;
}

View file

@ -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 };

View file

@ -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 }
};

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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();

View file

@ -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);

View file

@ -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 });

View file

@ -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; }
}

View file

@ -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"
}

View file

@ -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);

View file

@ -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)) {

View file

@ -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)");

View file

@ -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;
}

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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 });

View file

@ -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);

View file

@ -56,7 +56,7 @@ const TEXT_CATALOG = {
{
title: "コロニーの見方",
image: "assets/sprites/tarinai_11_weak.webp",
text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。たりないデータでは選択中の個体の健康、性格、関係、行動理由、記録を追えます。",
text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。観察では選択中の個体の健康、性格、関係、行動理由、記録を追えます。",
},
],};

View file

@ -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;
}

View file

@ -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,

View file

@ -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) {

View file

@ -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) {

View file

@ -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() {
<div class="info-row personality-values"><span>\u73fe\u5728\u6027\u683c</span><strong>${personalityRowsHtml(t.currentPersonality)}</strong></div>
<div class="info-row selected-personality-changes"><span>\u4eca\u65e5\u306e\u5909\u5316</span><strong><ol class="selected-change-list">${recentChangeHtml}</ol></strong></div>
`;
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 `<li><span>${escapeHtml(timeText)}</span><strong>${escapeHtml(recordText)}</strong></li>`;
}).join("") || `<li><span>-</span><strong>\u8a18\u9332\u306a\u3057</strong></li>`;
}).join("") || `<li><span>-</span><strong>まだ記憶がありません</strong></li>`;
const basicHtml = `
<div class="info-row"><span>\u4e16\u4ee3</span><strong>${escapeHtml(generationDisplay || "\u4e0d\u660e")}</strong></div>
<div class="info-row"><span>\u5e74\u9f62</span><strong>${fmt(t.age)} / ${fmt(t.lifeSpan)}</strong></div>
@ -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", `<div class="selected-records"><ol>${recordHtml}</ol></div>`)}
${selectedDataCategoryHtml("memories", "最近の記憶", `<div class="selected-records selected-memories"><ol>${recentMemoryHtml}</ol></div>`)}
</div>
<div class="selected-actions"><button type="button" class="favorite-toggle ${t.favorite ? "active" : ""}" data-selected-action="favorite">${t.favorite ? "\u2605 \u304a\u6c17\u306b\u5165\u308a\u89e3\u9664" : "\u2606 \u304a\u6c17\u306b\u5165\u308a"}</button></div>
`;

View file

@ -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}`;

View file

@ -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)}`;

View file

@ -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" });

View file

@ -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) || [];

View file

@ -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 @@
<label>往復幅 <input id="railTravelControl" type="range" min="48" max="360" step="4"><input id="railTravelControlNum" type="number" min="24" max="520" step="4"> px</label>
<label>太さ <input id="reciprocatorThickness" type="range" min="4" max="30" step="1"><input id="reciprocatorThicknessNum" type="number" min="4" max="34" step="1"></label>
<label><input id="railMotorPowered" type="checkbox"> 動力ON</label>
<label><input id="reciprocatorSolidEnabled" type="checkbox"> 当たり判定ON</label>
</div>
<div class="rotator-editor-tools" role="toolbar" aria-label="描画方法">
<button type="button" data-reciprocator-mode="line" class="active">直線</button>
@ -488,6 +489,7 @@
const thick = dialog.querySelector("#reciprocatorThickness");
const thickNum = dialog.querySelector("#reciprocatorThicknessNum");
const powered = dialog.querySelector("#railMotorPowered");
const solidInput = dialog.querySelector("#reciprocatorSolidEnabled");
const apply = dialog.querySelector("#reciprocatorApply");
const cancel = dialog.querySelector("#reciprocatorCancel");
const closeBtn = dialog.querySelector("#reciprocatorEditorClose");
@ -514,6 +516,7 @@
if (thick) thick.value = String(initialThickness);
if (thickNum) thickNum.value = String(initialThickness);
if (powered) powered.checked = phyScalar(item, "railOn", true) !== false;
if (solidInput) solidInput.checked = phyScalar(item, "solid", true) !== false;
const toLocal = (e) => {
const rect = canvas.getBoundingClientRect();
const sx = canvas.width / Math.max(1, rect.width);
@ -690,6 +693,7 @@
phySet(item, "railMotorSpeed", Math.max(0, Math.min(360, Number(speedNum?.value || speed?.value || 92) || 0)), "editor-rail-speed");
phySet(item, "railTravel", Math.max(24, Math.min(520, Number(travelNum?.value || travel?.value || 150) || 150)), "editor-rail-travel");
phySet(item, "railOn", powered ? Boolean(powered.checked) : phyScalar(item, "railOn", true) !== false, "editor-rail-power");
phySet(item, "solid", solidInput ? Boolean(solidInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid");
if (phyScalar(item, "railOn", true)) phySet(item, "slideSpeed", 0, "editor-slide-reset");
phySet(item, "thickness", Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)), "editor-thickness");
physicsApi()?.setSegments?.(item, segments.map(seg => seg.slice(0, 4)), "editor-segments");
@ -783,13 +787,13 @@
if (item.type === "gate_fence") data.gateOpen = Boolean(item.gateOpen);
if (item.type === "duplicator") { data.storedFoodType = item.storedFoodType || ""; data.storedFoodLabel = item.storedFoodLabel || ""; }
if (item.type === "rotator") {
data.physics = { motorOn: phyScalar(item, "motorOn", true) !== false, motorSpeed: Number(phyScalar(item, "motorSpeed", 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
data.physics = { solid: phyScalar(item, "solid", true) !== false, motorOn: phyScalar(item, "motorOn", true) !== false, motorSpeed: Number(phyScalar(item, "motorSpeed", 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
}
if (item.type === "poison_block") {
data.physics = { solid: phyScalar(item, "solid", true) !== false, damage: Number(phyScalar(item, "damage", 7) || 7), thickness: Number(phyScalar(item, "thickness", 14) || 14), segments: cloneSegmentsForRotator(item) };
}
if (item.type === "reciprocator") {
data.physics = { railOn: phyScalar(item, "railOn", true) !== false, railMotorSpeed: Number(phyScalar(item, "railMotorSpeed", 92) || 92), railTravel: Number(phyScalar(item, "railTravel", 150) || 150), railPhase: Number(phyScalar(item, "railPhase", 0) || 0), railDir: Number(phyScalar(item, "railDir", 1) || 1), railAxis: Number(phyScalar(item, "railAxis", item.angle || 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
data.physics = { solid: phyScalar(item, "solid", true) !== false, railOn: phyScalar(item, "railOn", true) !== false, railMotorSpeed: Number(phyScalar(item, "railMotorSpeed", 92) || 92), railTravel: Number(phyScalar(item, "railTravel", 150) || 150), railPhase: Number(phyScalar(item, "railPhase", 0) || 0), railDir: Number(phyScalar(item, "railDir", 1) || 1), railAxis: Number(phyScalar(item, "railAxis", item.angle || 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
}
return data;
}
@ -827,6 +831,7 @@
}
if (item.type === "rotator") {
const ph = data.physics || {};
phySet(item, "solid", ph.solid !== false, "copy");
phySet(item, "motorOn", ph.motorOn !== false, "copy");
phySet(item, "motorSpeed", Number(ph.motorSpeed || 0) || 0, "copy");
phySet(item, "spin", 0, "copy");
@ -846,6 +851,7 @@
}
if (item.type === "reciprocator") {
const ph = data.physics || {};
phySet(item, "solid", ph.solid !== false, "copy");
phySet(item, "railOn", ph.railOn !== false, "copy");
phySet(item, "railMotorSpeed", Math.max(0, Number(ph.railMotorSpeed || 92) || 92), "copy");
phySet(item, "railTravel", Math.max(24, Number(ph.railTravel || 150) || 150), "copy");
@ -1142,11 +1148,13 @@
if (!item || !this.isFenceType(item.type)) return false;
const rect = this.fenceRect(item);
if (!rect) return false;
const margin = Math.max(6, (item.r || 42) * 0.24);
const baseMargin = item.type === "gate_fence" ? 1.5 : Math.max(3, (item.r || 42) * 0.12);
if (rect.left < CONFIG.worldPadding || rect.top < CONFIG.worldPadding || rect.right > this.w - CONFIG.worldPadding || rect.bottom > this.h - CONFIG.worldPadding) return true;
const searchRadius = Math.max(rect.right - rect.left, rect.bottom - rect.top) * 0.5 + 72;
const searchRadius = Math.max(rect.right - rect.left, rect.bottom - rect.top) * 0.5 + 56;
for (const it of this.nearbyItems(item.x, item.y, searchRadius)) {
if (!it || it.dead) continue;
if (!it || it === item || it.dead) continue;
const otherIsGate = it.type === "gate_fence";
const margin = (item.type === "gate_fence" || otherIsGate) ? 1.5 : baseMargin;
for (const r of this.solidObstacleRects(it)) {
if (global.TarinaiCollisionFootprints?.rectsOverlap?.(rect, r, margin)) return true;
}

View file

@ -233,6 +233,190 @@
return true;
},
shootAimPoint() {
const p = this.pointer || {};
const pad = Math.max(0, CONFIG.worldPadding || 0);
const x = clamp((Number(p.x) || 0) + (Number(this.shootCursorOffsetX) || 0), pad, Math.max(pad, (this.w || 0) - pad));
const y = clamp((Number(p.y) || 0) + (Number(this.shootCursorOffsetY) || 0), pad, Math.max(pad, (this.h || 0) - pad));
return { x, y, inside: Boolean(p.inside) };
},
setShootCursorFromPointer(p = this.pointer || {}) {
this.shootPointerRawX = Number(p.x) || 0;
this.shootPointerRawY = Number(p.y) || 0;
this.shootCursorActive = true;
return this.shootAimPoint?.() || { x: this.shootPointerRawX, y: this.shootPointerRawY };
},
moveShootCursorBy(dx = 0, dy = 0) {
// Mouse/touch movement should move the base pointer, not accumulate extra
// logical drift. Only recoil changes shootCursorOffsetX/Y.
this.shootCursorActive = true;
this.drawListDirty = true;
return this.shootAimPoint?.() || { x: this.pointer?.x || 0, y: this.pointer?.y || 0 };
},
applyShootToolRecoil(power = 1) {
const unit = this.screenSizeToWorld ? this.screenSizeToWorld(rand(18, 34)) : rand(18, 34);
const mag = Math.max(8, unit) * Math.max(0.65, Number(power) || 1);
const angle = -Math.PI / 2 + rand(-0.52, 0.52);
const dx = Math.cos(angle) * mag;
const dy = Math.sin(angle) * mag;
this.shootCursorOffsetX = clamp((Number(this.shootCursorOffsetX) || 0) + dx, -130, 130);
this.shootCursorOffsetY = clamp((Number(this.shootCursorOffsetY) || 0) + dy, -150, 70);
this.shootCursorActive = true;
this.drawListDirty = true;
return this.shootAimPoint?.() || { x: this.pointer?.x || 0, y: this.pointer?.y || 0 };
},
findShootToolTargetAt(x, y) {
let best = null;
let bestD = Infinity;
for (let i = this.tarinai.length - 1; i >= 0; i -= 1) {
const t = this.tarinai[i];
if (!t || t.dead || this.isTarinaiHiddenInNestBox?.(t)) continue;
const hit = t.contains ? t.contains(x, y) : distXY(x, y, t.x, t.y) <= (t.radius || 22) * 1.28;
const d = distXY(x, y, t.x, t.y);
if (hit && d < bestD) { best = { kind: "tarinai", target: t, distance: d }; bestD = d; }
}
for (let i = (this.ants || []).length - 1; i >= 0; i -= 1) {
const ant = this.ants[i];
if (!ant || ant.dead) continue;
const hitR = Math.max(10, (ant.r || 6) * 1.85);
const d = distXY(x, y, ant.x, ant.y);
if (d <= hitR && d < bestD) { best = { kind: "ant", target: ant, distance: d }; bestD = d; }
}
for (let i = (this.items || []).length - 1; i >= 0; i -= 1) {
const it = this.items[i];
if (!it || it.dead || it.type === "trace" || it.type === "splat") continue;
let d = distXY(x, y, it.x, it.y);
let hit = false;
if (it.type === "rope" || it.type === "rod") {
const runtime = global.TarinaiLinkRuntime;
const pb = global.TarinaiPhysicsBodySystem;
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), this);
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), this);
d = runtime?.pointSegmentDistance?.(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y) ?? d;
hit = d < 18;
} else if (global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type)) {
const result = global.TarinaiMechanicalSystem.hitTest(this, it, x, y, { padding: 14 });
hit = Boolean(result.hit);
d = Math.min(d, result.distance ?? d);
} else {
const result = global.TarinaiCollisionFootprints?.hitTestItem?.(this, it, x, y, { radiusMultiplier: it.type === "ball" ? 3.2 : 1.65, padding: 10 }) || { hit: false, distance: d };
hit = Boolean(result.hit);
d = Math.min(d, result.distance ?? d);
}
if (hit && d < bestD) { best = { kind: "item", target: it, distance: d }; bestD = d; }
}
return best;
},
shootFleeTargetFor(t, x, y, minDist = 150, maxDist = 250) {
if (!t) return null;
const dx = (Number(t.x) || 0) - (Number(x) || 0);
const dy = (Number(t.y) || 0) - (Number(y) || 0);
const d = Math.hypot(dx, dy);
const a = d > 3 ? Math.atan2(dy, dx) : rand(-Math.PI, Math.PI);
const fleeDist = rand(minDist, maxDist);
const pad = Math.max(CONFIG.worldPadding || 0, (t.radius || 20) + 8);
return {
x: clamp((Number(t.x) || 0) + Math.cos(a) * fleeDist, pad, Math.max(pad, (this.w || 0) - pad)),
y: clamp((Number(t.y) || 0) + Math.sin(a) * fleeDist, pad, Math.max(pad, (this.h || 0) - pad)),
dead: false,
detour: true,
panic: true,
};
},
panicTarinaiFromShootImpact(t, x, y, options = {}) {
if (!t || t.dead || this.isTarinaiHiddenInNestBox?.(t)) return false;
const direct = Boolean(options.direct);
const strength = Math.max(0.25, Number(options.strength) || 1);
const target = this.shootFleeTargetFor?.(t, x, y, direct ? 170 : 115, direct ? 260 : 205) || null;
const reason = direct ? "撃たれてパニックで逃げている" : "近くに弾が当たってパニックで逃げている";
const threat = { x: Number(x) || t.x, y: Number(y) || t.y, type: "shoot", dead: false };
t.fearTimer = Math.max(t.fearTimer || 0, (direct ? 2.8 : 1.25) * strength);
t.surpriseTimer = Math.max(t.surpriseTimer || 0, (direct ? 0.34 : 0.28) * strength);
if (direct) t.hurtTimer = Math.max(t.hurtTimer || 0, 1.35);
if (t.enterPanic) t.enterPanic({ threat, destination: target, target, forceDestination: true, reason, fear: direct ? 1.25 : 0.85 * strength, wake: true, cause: direct ? "shoot_tool" : "shoot_impact_nearby", hurtTimer: direct ? 1.35 : undefined, surpriseTimer: direct ? 0.34 : 0.25, awakeLockTimer: direct ? 1.0 : 0.45 });
t.setActionState?.("panic", { target, reason, wake: true, sleeping: false });
t.target = target;
t.panicTarget = target;
t.panicTargetUntil = (this.time || 0) + (direct ? 2.4 : 1.55 * strength);
const dx = (Number(t.x) || 0) - (Number(x) || 0);
const dy = (Number(t.y) || 0) - (Number(y) || 0);
const d = Math.hypot(dx, dy);
const a = d > 3 ? Math.atan2(dy, dx) : rand(-Math.PI, Math.PI);
t.vx = clamp((t.vx || 0) + Math.cos(a) * rand(direct ? 28 : 12, direct ? 56 : 30) * strength, -220, 220);
t.vy = clamp((t.vy || 0) + Math.sin(a) * rand(direct ? 24 : 10, direct ? 48 : 26) * strength, -220, 220);
return true;
},
panicTarinaiAroundShootImpact(x, y, directTarget = null) {
const radius = 138;
let count = 0;
for (const t of this.tarinai || []) {
if (!t || t.dead || t === directTarget || this.isTarinaiHiddenInNestBox?.(t)) continue;
const d = distXY(x, y, t.x, t.y);
if (d > radius) continue;
const strength = clamp(1 - d / radius, 0.22, 1.0);
if (this.panicTarinaiFromShootImpact?.(t, x, y, { direct: false, strength })) count += 1;
}
return count;
},
fireShootTool(x = null, y = null) {
const aim = (x == null || y == null) ? this.shootAimPoint?.() : { x: Number(x) || 0, y: Number(y) || 0, inside: true };
if (!aim?.inside) return false;
const hit = this.findShootToolTargetAt?.(aim.x, aim.y);
audio.shoot?.();
this.effects?.push(new Effect("shoot_impact", aim.x, aim.y, { size: hit ? 10 : 7, life: hit ? 0.20 : 0.14, color: hit ? "rgba(255,208,116,0.96)" : "rgba(190,212,240,0.58)" }));
this.effects?.push(new Effect("ring", aim.x, aim.y, { size: hit ? 8 : 5, life: hit ? 0.11 : 0.08, color: hit ? "rgba(255,246,206,0.72)" : "rgba(196,220,255,0.34)" }));
let handled = false;
if (hit?.kind === "tarinai") {
const t = hit.target;
const damage = 16;
t.damage?.(damage, "射撃");
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.35);
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.22);
t.fearTimer = Math.max(t.fearTimer || 0, 2.8);
this.panicTarinaiFromShootImpact?.(t, aim.x, aim.y, { direct: true, strength: 1 });
handled = true;
} else if (hit?.kind === "ant") {
const ant = hit.target;
const damage = ant.kind === "queen" ? 24 : 34;
ant.hp = Math.max(0, (Number(ant.hp) || Number(ant.maxHp) || 0) - damage);
ant.hpBarTimer = Math.max(ant.hpBarTimer || 0, 1.3);
if (ant.hp <= 0) ant.dieAsCorpse?.(ant.homeNest?.() || null);
handled = true;
} else if (hit?.kind === "item") {
handled = true;
const target = hit.target;
const shouldRemove = Math.random() < 0.30;
if (shouldRemove) {
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(this, target, {
reason: "shoot-tool",
userReason: "射撃で消えた",
wake: true,
});
if (removed) {
this.compactItems?.();
this.updateItemCounts?.();
this.rebuildSpatial?.();
} else {
}
} else {
}
} else {
}
this.panicTarinaiAroundShootImpact?.(aim.x, aim.y, hit?.kind === "tarinai" ? hit.target : null);
this.lastShootAt = this.time || 0;
this.applyShootToolRecoil?.(handled ? 1 : 0.88);
this.drawListDirty = true;
return true;
},
useToolAt(x, y) {
if (this.tool === "delete") return this.deleteItemAt(x, y);
if (this.tool === "area_delete") return this.deleteItemsInArea(x, y);

View file

@ -37,19 +37,6 @@ def draw_drop(fill, stroke, highlight):
d.ellipse([24, 38, 45, 48], fill=highlight)
return fn
def draw_water_bowl(palette):
bowl_fill = rgba(palette.get("bowlFill"), (171, 120, 76, 184))
bowl_stroke = rgba(palette.get("bowlStroke"), (94, 69, 48, 140))
water_fill = rgba(palette.get("waterFill"), (86, 157, 224, 148))
water_stroke = rgba(palette.get("waterStroke"), (62, 113, 178, 92))
highlight = rgba(palette.get("highlight"), (255, 255, 255, 138))
def fn(d: ImageDraw.ImageDraw) -> None:
d.ellipse([9, 29, 87, 72], fill=bowl_fill, outline=bowl_stroke, width=4)
d.ellipse([19, 35, 77, 62], fill=water_fill, outline=water_stroke, width=3)
d.ellipse([34, 38, 44, 47], fill=highlight)
return fn
def rgba(value, fallback):
if not value:
return fallback
@ -145,8 +132,6 @@ def main() -> None:
highlight = rgba(palette.get("highlight") or palette.get("accent"), (255, 255, 255, 150))
if renderer == "oval_droplet":
draw_fn = draw_drop(fill, stroke, highlight)
elif renderer == "water_bowl":
draw_fn = draw_water_bowl(palette)
elif renderer == "powder_pile":
draw_fn = draw_powder(fill, stroke, highlight)
elif renderer == "split_pill":

View file

@ -1,6 +1,6 @@
"use strict";
const APP_VERSION = "39.15.18";
const APP_VERSION = "39.15.27";
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const v = `v=${APP_VERSION}`;
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.