stable
This commit is contained in:
parent
0f1cb2fe6c
commit
129c07183a
51 changed files with 5711 additions and 1122 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.00.00",
|
||||
"version": "39.15.18",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
"js/collision_footprint_system.js",
|
||||
"js/mechanical_system.js",
|
||||
"js/constraint_system.js",
|
||||
"js/physics_world_system.js",
|
||||
"js/assets.js",
|
||||
"js/audio.js",
|
||||
"js/perf_profiler.js",
|
||||
|
|
|
|||
281
index.html
281
index.html
|
|
@ -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.00.00" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.00.00" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.00.00" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.00.00" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.00.00" />
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -189,140 +189,141 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.00.00" defer></script>
|
||||
<script src="js/event_bus.js?v=39.00.00" defer></script>
|
||||
<script src="js/registry_base.js?v=39.00.00" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.00.00" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_registry.js?v=39.00.00" defer></script>
|
||||
<script src="js/data.js?v=39.00.00" defer></script>
|
||||
<script src="js/ground_types.js?v=39.00.00" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.00.00" defer></script>
|
||||
<script src="js/math.js?v=39.00.00" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.00.00" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/assets.js?v=39.00.00" defer></script>
|
||||
<script src="js/audio.js?v=39.00.00" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.00.00" defer></script>
|
||||
<script src="js/render.js?v=39.00.00" defer></script>
|
||||
<script src="js/sim_core.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.00.00" defer></script>
|
||||
<script src="js/structures.js?v=39.00.00" defer></script>
|
||||
<script src="js/items.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_runtime.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.00.00" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.00.00" defer></script>
|
||||
<script src="js/ants.js?v=39.00.00" defer></script>
|
||||
<script src="js/health.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_forced_action_planner_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_action_planner_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.00.00" defer></script>
|
||||
<script src="js/world.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_view.js?v=39.00.00" defer></script>
|
||||
<script src="js/family_graph.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_environment.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/weather_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.00.00" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_update_phases.js?v=39.00.00" defer></script>
|
||||
<script src="js/system_order.js?v=39.00.00" defer></script>
|
||||
<script src="js/simulation.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_update.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.00.00" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.00.00" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.00.00" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_log.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.00.00" defer></script>
|
||||
<script src="js/save_schema.js?v=39.00.00" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.00.00" defer></script>
|
||||
<script src="js/history_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/save_codec.js?v=39.00.00" defer></script>
|
||||
<script src="js/save_storage.js?v=39.00.00" defer></script>
|
||||
<script src="js/save_system.js?v=39.00.00" defer></script>
|
||||
<script src="js/patch_helpers.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.00.00" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.00.00" defer></script>
|
||||
<script src="js/main.js?v=39.00.00" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.00.00" defer></script>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -164,7 +164,8 @@
|
|||
function hitTestItem(worldRef, item, x, y, opts = {}) {
|
||||
if (!item || item.dead) return { hit: false, distance: Infinity };
|
||||
const padding = Math.max(0, num(opts.padding, 0));
|
||||
const rects = opts.rects || worldRef?.solidObstacleRects?.(item) || [];
|
||||
let rects = opts.rects || worldRef?.solidObstacleRects?.(item) || [];
|
||||
if ((!rects || !rects.length) && item?.type === "poison_block") rects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(item) || [];
|
||||
let bestDistance = Math.hypot(num(x) - num(item.x), num(y) - num(item.y));
|
||||
let hit = false;
|
||||
if (rects.length) {
|
||||
|
|
@ -175,7 +176,7 @@
|
|||
}
|
||||
const mechHit = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type);
|
||||
if (mechHit) {
|
||||
const hub = Math.max(18, Math.min(40, num(item.rotatorThickness, 12) * 2.3));
|
||||
const hub = Math.max(18, Math.min(40, (global.TarinaiPhysicsBodySystem?.scalar?.(item, "thickness", 12) ?? 12) * 2.3));
|
||||
if (Math.hypot(num(x) - num(item.x), num(y) - num(item.y)) <= hub + padding) hit = true;
|
||||
} else if (!rects.length) {
|
||||
const mul = num(opts.radiusMultiplier, item.type === "ball" ? 4.0 : 2.2);
|
||||
|
|
@ -187,7 +188,7 @@
|
|||
|
||||
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
||||
if (!worldRef || !item || item.dead) return true;
|
||||
const important = opts.important || new Set(["grass", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "nest_box", "water"]);
|
||||
const important = opts.important || new Set(["grass", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box", "water"]);
|
||||
const isPhysicsItem = (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || item.type === "rope" || item.type === "rod" || (typeof isFenceItemType === "function" && isFenceItemType(item.type)));
|
||||
if (!important.has(item.type) && !isPhysicsItem) return false;
|
||||
const placingGrass = item.type === "grass";
|
||||
|
|
@ -197,6 +198,11 @@
|
|||
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) {
|
||||
if (!other || other === item || other.dead) continue;
|
||||
if (isPhysicsItem && (other.type === "water" || other.type === "grass_bed")) continue;
|
||||
if (other.type === "poison_block") {
|
||||
const body = global.TarinaiPhysicsBodySystem?.ensureBody?.(other, worldRef, { syncFromLegacy: false });
|
||||
const solid = body?.collision ? body.collision.solid !== false : true;
|
||||
if (!solid) continue;
|
||||
}
|
||||
if (!important.has(other.type)) continue;
|
||||
if (!placingGrass && other.type === "grass") continue;
|
||||
const otherRects = worldRef.solidObstacleRects?.(other) || [];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -4,47 +4,174 @@
|
|||
const params = new URLSearchParams(location.search || "");
|
||||
const enabled = params.get("debug") === "1" || params.get("debug") === "true";
|
||||
if (!enabled) return;
|
||||
const panel = document.createElement("div");
|
||||
panel.id = "debugOverlay";
|
||||
panel.style.cssText = [
|
||||
"position:fixed", "left:8px", "top:76px", "z-index:99999", "min-width:230px", "max-width:360px",
|
||||
"font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff",
|
||||
"background:rgba(18,24,30,.78)", "border:1px solid rgba(255,255,255,.18)", "border-radius:10px",
|
||||
"padding:8px 10px", "pointer-events:none", "white-space:pre-wrap", "box-shadow:0 8px 24px rgba(0,0,0,.22)"
|
||||
|
||||
const host = document.createElement("div");
|
||||
host.id = "debugOverlay";
|
||||
host.style.cssText = [
|
||||
"position:fixed", "left:8px", "top:76px", "z-index:99999", "width:min(620px, calc(100vw - 16px))", "max-height:calc(100vh - 92px)",
|
||||
"overflow:hidden", "font:11px/1.42 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff",
|
||||
"background:rgba(18,24,30,.86)", "border:1px solid rgba(255,255,255,.18)", "border-radius:10px",
|
||||
"pointer-events:auto", "box-shadow:0 8px 24px rgba(0,0,0,.22)"
|
||||
].join(";");
|
||||
document.addEventListener("DOMContentLoaded", () => document.body.appendChild(panel));
|
||||
|
||||
const bar = document.createElement("div");
|
||||
bar.style.cssText = [
|
||||
"display:flex", "gap:6px", "align-items:center", "padding:6px 8px", "border-bottom:1px solid rgba(255,255,255,.14)",
|
||||
"background:rgba(255,255,255,.06)", "user-select:none", "-webkit-user-select:none"
|
||||
].join(";");
|
||||
const title = document.createElement("span");
|
||||
title.style.cssText = "flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;";
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.type = "button";
|
||||
copyBtn.textContent = "Copy";
|
||||
const minBtn = document.createElement("button");
|
||||
minBtn.type = "button";
|
||||
minBtn.textContent = "Min";
|
||||
for (const btn of [copyBtn, minBtn]) {
|
||||
btn.style.cssText = [
|
||||
"font:11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace", "color:#f7fbff", "background:rgba(255,255,255,.12)",
|
||||
"border:1px solid rgba(255,255,255,.22)", "border-radius:6px", "padding:3px 7px", "cursor:pointer"
|
||||
].join(";");
|
||||
}
|
||||
bar.append(title, copyBtn, minBtn);
|
||||
|
||||
const body = document.createElement("pre");
|
||||
body.style.cssText = [
|
||||
"margin:0", "padding:8px 10px", "max-height:calc(100vh - 128px)", "overflow:auto", "white-space:pre-wrap",
|
||||
"user-select:text", "-webkit-user-select:text", "cursor:text"
|
||||
].join(";");
|
||||
body.tabIndex = 0;
|
||||
host.append(bar, body);
|
||||
document.addEventListener("DOMContentLoaded", () => document.body.appendChild(host));
|
||||
|
||||
let copiedMessageUntil = 0;
|
||||
let minimized = false;
|
||||
let lastText = "";
|
||||
|
||||
function selectBodyText() {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(body);
|
||||
const selection = window.getSelection?.();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
|
||||
copyBtn.addEventListener("click", async (ev) => {
|
||||
ev.preventDefault();
|
||||
const text = lastText || body.textContent || "";
|
||||
try {
|
||||
await navigator.clipboard?.writeText?.(text);
|
||||
copiedMessageUntil = Date.now() + 1400;
|
||||
copyBtn.textContent = "Copied";
|
||||
} catch (_) {
|
||||
selectBodyText();
|
||||
copiedMessageUntil = Date.now() + 1400;
|
||||
copyBtn.textContent = "Select";
|
||||
}
|
||||
});
|
||||
|
||||
minBtn.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
minimized = !minimized;
|
||||
body.style.display = minimized ? "none" : "block";
|
||||
host.style.width = minimized ? "min(360px, calc(100vw - 16px))" : "min(620px, calc(100vw - 16px))";
|
||||
minBtn.textContent = minimized ? "Open" : "Min";
|
||||
});
|
||||
|
||||
let lastEvents = [];
|
||||
let lastAudio = [];
|
||||
window.TarinaiEvents?.on("*", ev => { lastEvents.unshift(ev.type); if (lastEvents.length > 10) lastEvents.length = 10; });
|
||||
window.TarinaiEvents?.on("audio:play", ev => { lastAudio.unshift(`${ev.detail?.id || "?"}:${ev.detail?.category || "?"}`); if (lastAudio.length > 8) lastAudio.length = 8; });
|
||||
|
||||
function fmtMs(v) {
|
||||
const n = Number(v || 0);
|
||||
return n >= 10 ? n.toFixed(1) : n.toFixed(2);
|
||||
}
|
||||
|
||||
function shortLabel(label) {
|
||||
return String(label || "?")
|
||||
.replace(/^update\.phase\./, "phase.")
|
||||
.replace(/^update\./, "u.")
|
||||
.replace(/^render\./, "r.")
|
||||
.replace(/^frame\./, "f.");
|
||||
}
|
||||
|
||||
function topMap(map, limit = 4) {
|
||||
return Object.entries(map || {}).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([k, v]) => `${k}:${v}`).join(", ") || "-";
|
||||
}
|
||||
|
||||
function perfLines(perf) {
|
||||
const self = (perf.topSelf || perf.entries || []).filter(e => e.label !== "frame.total").slice(0, 10);
|
||||
const incl = (perf.topInclusive || perf.entries || []).filter(e => e.label !== "frame.total").slice(0, 6);
|
||||
const selfLines = self.map((e, i) => `${String(i + 1).padStart(2, " ")}. ${shortLabel(e.label).padEnd(28, " ")} self ${fmtMs(e.selfAvg)}ms total ${fmtMs(e.avg)}ms ${e.selfPct || 0}%`);
|
||||
const inclLine = incl.map(e => `${shortLabel(e.label)}:${fmtMs(e.avg)}`).join(" ");
|
||||
return [
|
||||
`heavy self avg`,
|
||||
...(selfLines.length ? selfLines : [" -"]),
|
||||
`heavy inclusive ${inclLine || "-"}`,
|
||||
];
|
||||
}
|
||||
|
||||
function diagnosticHints(diag, perf) {
|
||||
const hints = [];
|
||||
const entries = perf.topSelf || perf.entries || [];
|
||||
const top = entries[0];
|
||||
if (top && top.selfAvg >= 4) hints.push(`hot:${shortLabel(top.label)} ${fmtMs(top.selfAvg)}ms self`);
|
||||
if ((diag.spatial?.rebuildsThisFrame || 0) > 1) hints.push(`spatial rebuild/frame ${diag.spatial.rebuildsThisFrame}`);
|
||||
if ((diag.creatures?.smooth || 0) > 0) hints.push(`smooth motion ${diag.creatures.smooth}`);
|
||||
if ((diag.scheduler?.activeRealtime || 0) > 120) hints.push(`realtime items ${diag.scheduler.activeRealtime}`);
|
||||
if ((diag.scheduler?.heap || 0) > 700) hints.push(`scheduled heap ${diag.scheduler.heap}`);
|
||||
if ((diag.items?.bucketRebuildsTotal || 0) > 0 && diag.items?.bucketsDirty) hints.push("item buckets dirty");
|
||||
const m = diag.scheduler?.mechanicalStats || diag.items?.mechanicalStats || diag.scheduler?.physics?.mechanical || null;
|
||||
if (m?.candidates > 250 || m?.pairs > 180) hints.push(`mechanical candidates ${m.candidates || "?"} pairs ${m.pairs || m.solved || "?"}`);
|
||||
const c = diag.scheduler?.constraintStats || null;
|
||||
if (c?.totalLinks > 80) hints.push(`constraints links ${c.totalLinks} ran ${c.ran}`);
|
||||
return hints.join(" ") || "-";
|
||||
}
|
||||
|
||||
window.setInterval(() => {
|
||||
const w = window.world;
|
||||
const input = window.TarinaiInputMode?.snapshot?.() || {};
|
||||
const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {};
|
||||
const behaviorCounts = diag.behavior?.counts || {};
|
||||
const topBehaviors = Object.entries(behaviorCounts).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([id, n]) => `${id}:${n}`).join(", ");
|
||||
const topBehaviors = topMap(behaviorCounts, 5);
|
||||
const perf = diag.perf || window.TarinaiPerf?.snapshot?.() || {};
|
||||
const perfTop = (perf.entries || []).slice(0, 5).map(e => `${e.label}:${e.avg}ms`).join(", ");
|
||||
panel.textContent = [
|
||||
`tarinai v${window.TARINAI_VERSION || "?"}`,
|
||||
`fps ${window.__tarinaiFps ?? "?"}`,
|
||||
`live ${diag.runtime?.liveTarinai ?? (w?.tarinai || []).length} dpr ${diag.runtime?.dpr || uiCache?.canvasDpr || "?"}`,
|
||||
`items ${(w?.items || []).length} tarinai ${(w?.tarinai || []).length} ants ${(w?.ants || []).length}`,
|
||||
`effects ${(w?.effects || []).length}`,
|
||||
`terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0}`,
|
||||
`spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`,
|
||||
`drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)} logs ${(w?.logs || []).length}`,
|
||||
`item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} rebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`,
|
||||
const metrics = perf.metrics || {};
|
||||
const currentDpr = (typeof uiCache !== "undefined" && uiCache) ? uiCache.canvasDpr : "?";
|
||||
const creature = diag.creatures || {};
|
||||
const scheduler = diag.scheduler || {};
|
||||
const terrain = diag.terrain || {};
|
||||
const lines = [
|
||||
`tarinai v${window.TARINAI_VERSION || "?"} debug profiler:${perf.enabled ? "on" : "off"}${Date.now() < copiedMessageUntil ? " copied" : " copy:button/select"}`,
|
||||
`fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms quality ${perf.tier || "?"} dprScale:${perf.dprScale || 1}`,
|
||||
`live ${diag.runtime?.liveTarinai ?? (w?.tarinai || []).length} items ${(w?.items || []).length} ants ${(w?.ants || []).length} effects ${(w?.effects || []).length}`,
|
||||
`visible layered:${metrics["render.visibleLayered"] ?? "?"} back:${metrics["render.visibleBackItems"] ?? "?"} effects:${metrics["render.visibleEffects"] ?? "?"}`,
|
||||
`creatures full:${creature.full ?? 0} realtime:${creature.realtime ?? 0} smooth:${creature.smooth ?? 0} skipped:${creature.skipped ?? 0} selected:${creature.selected ?? 0} urgent:${creature.urgent ?? 0} visible:${creature.visible ?? 0} sleepPhys:${creature.sleepPhysical ?? 0} passive:${creature.passivePhysical ?? 0}`,
|
||||
`creature lanes ${topMap(creature.lanes, 6)} smoothStates ${topMap(creature.smoothStates, 4)}`,
|
||||
`spatial rebuild ${diag.spatial?.rebuildsThisFrame ?? 0}/frame total:${diag.spatial?.rebuildsTotal ?? 0} partial:${diag.spatial?.partialRebuildsTotal ?? 0} dirty:${diag.spatial?.dirtyMarksThisFrame ?? 0} deferred:${diag.spatial?.deferredReads ?? 0} reason:${diag.spatial?.dirtyReason || diag.spatial?.lastRebuildReason || "-"}`,
|
||||
`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 ?? "?"}`,
|
||||
`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}`,
|
||||
`scheduler heap:${diag.scheduler?.heap ?? 0} realtime:${diag.scheduler?.realtime ?? 0} ran:${diag.scheduler?.lastRan ?? 0} rebuilds:${diag.scheduler?.rebuilds ?? 0}`,
|
||||
`perf ${perf.tier || "?"} dprScale:${perf.dprScale || 1} ${perfTop || "-"}`,
|
||||
`hints ${diagnosticHints(diag, perf)}`,
|
||||
...perfLines(perf),
|
||||
`behavior forced:${diag.behavior?.forced ?? 0} locked:${diag.behavior?.locked ?? 0} top ${topBehaviors || "-"}`,
|
||||
`dpr ${uiCache?.canvasDpr || "?"} cache ${window.TARINAI_APP?.cacheName || "?"}`,
|
||||
`sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"} soundPack ${window.TarinaiAudio?.soundPackVersion || "?"}`,
|
||||
`input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)} uaMobile:${Boolean(input.userAgentMobile)}`,
|
||||
`terrain v${w?.terrainVersion ?? 0} spatial v${w?.spatialVersion ?? 0} drawList ${(w?.drawList || []).length} dirty:${Boolean(diag.render?.drawListDirty)}`,
|
||||
`item buckets:${diag.items?.bucketTypes ?? 0} dirty:${Boolean(diag.items?.bucketsDirty)} id-map:${diag.items?.idMapSize ?? 0} bucketRebuilds:${diag.items?.bucketRebuildsTotal ?? 0}`,
|
||||
`dpr ${currentDpr} cache ${window.TARINAI_APP?.cacheName || "?"} sw ${navigator.serviceWorker?.controller ? "controlled" : "uncontrolled"}`,
|
||||
`input ${input.currentMode || "?"} touchFirst:${Boolean(input.touchFirst)} coarse:${Boolean(input.pointerCoarse)} hover:${Boolean(input.hasHover)} compact:${Boolean(input.isCompactViewport)}`,
|
||||
`audio ${lastAudio.join(", ") || window.TarinaiAudio?.lastPlayedId || "-"}`,
|
||||
`effects ${window.TarinaiItemRegistry?.effect?.debugSnapshot?.().length || 0} registered`,
|
||||
`events ${lastEvents.join(", ")}`,
|
||||
].join("\n");
|
||||
];
|
||||
lastText = lines.join("\n");
|
||||
title.textContent = minimized
|
||||
? `tarinai v${window.TARINAI_VERSION || "?"} fps ${window.__tarinaiFps ?? "?"} frame ${fmtMs(perf.frameAvg || 0)}ms`
|
||||
: `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);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const HEALTH = (() => {
|
|||
poke: "\u3064\u3064\u304b\u308c\u3059\u304e",
|
||||
firecracker: "\u7206\u7af9",
|
||||
genkotsu: "\u3052\u3093\u3053\u3064",
|
||||
poisonBlock: "毒ブロック",
|
||||
accident: "\u4e8b\u6545",
|
||||
collision: "\u885d\u7a81",
|
||||
outOfBounds: "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664",
|
||||
|
|
@ -31,6 +32,7 @@ const HEALTH = (() => {
|
|||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
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;
|
||||
if (/\u5de8\u5927\u85ac|giant/.test(text)) return CAUSES.giantDrug;
|
||||
if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug;
|
||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Layer: entity-runtime/items/dynamic-system
|
||||
// Owns pushpin/oshibyo item motion, attachment, lodged damage, and detach
|
||||
|
|
@ -85,7 +115,7 @@
|
|||
} else {
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2);
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 1.3);
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + deterministicRange(worldRef, "pin-panic-target-x", -80, 80, item, t), y: t.y + deterministicRange(worldRef, "pin-panic-target-y", -80, 80, item, t) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
if (t.addStress) t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 });
|
||||
}
|
||||
if (t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "\u753b\u92f2");
|
||||
|
|
@ -127,12 +157,12 @@
|
|||
if (t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "\u753b\u92f2");
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50);
|
||||
if (t.addStress) t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 });
|
||||
if (Math.random() < 0.22) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)");
|
||||
if (deterministicChance(worldRef, "pin-pain-bubble", 0.22, item, t)) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)");
|
||||
}
|
||||
t.sleeping = false;
|
||||
if (t.enterPanic) t.enterPanic({ threat: item, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.65, surpriseTimer: 0.22, awakeLockTimer: 2.4, cause: "pushpin_lodged" });
|
||||
else {
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + rand(-80, 80), y: t.y + rand(-80, 80) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + deterministicRange(worldRef, "pin-panic-target-x", -80, 80, item, t), y: t.y + deterministicRange(worldRef, "pin-panic-target-y", -80, 80, item, t) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true });
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.65);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.22);
|
||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.4);
|
||||
|
|
@ -140,7 +170,7 @@
|
|||
item.pinFallCheckTimer = (item.pinFallCheckTimer || 0) + dt;
|
||||
while (item.pinFallCheckTimer >= 1.0) {
|
||||
item.pinFallCheckTimer -= 1.0;
|
||||
if (Math.random() < 0.10) {
|
||||
if (deterministicChance(worldRef, "pin-fall-out", 0.10, item, t)) {
|
||||
detach(item, worldRef, "fall");
|
||||
return;
|
||||
}
|
||||
|
|
@ -174,13 +204,13 @@
|
|||
item.vy = 0;
|
||||
item.spinVelocity = 0;
|
||||
} else {
|
||||
item.vx = rand(-24, 24);
|
||||
item.vy = rand(-10, 18);
|
||||
item.spinVelocity = rand(-1.6, 1.6);
|
||||
item.spin += rand(-0.25, 0.25);
|
||||
item.vx = deterministicRange(worldRef, "pin-detach-vx", -24, 24, item, t, reason);
|
||||
item.vy = deterministicRange(worldRef, "pin-detach-vy", -10, 18, item, t, reason);
|
||||
item.spinVelocity = deterministicRange(worldRef, "pin-detach-spin-velocity", -1.6, 1.6, item, t, reason);
|
||||
item.spin += deterministicRange(worldRef, "pin-detach-spin", -0.25, 0.25, item, t, reason);
|
||||
if (t) {
|
||||
item.x = clamp(t.x + rand(-(t.radius || 16) * 0.75, (t.radius || 16) * 0.75), CONFIG.worldPadding || 30, (worldRef?.w || item.x) - (CONFIG.worldPadding || 30));
|
||||
item.y = clamp(t.y + rand((t.radius || 16) * 0.12, (t.radius || 16) * 0.72), CONFIG.worldPadding || 30, (worldRef?.h || item.y) - (CONFIG.worldPadding || 30));
|
||||
item.x = clamp(t.x + deterministicRange(worldRef, "pin-detach-x", -(t.radius || 16) * 0.75, (t.radius || 16) * 0.75, item, t, reason), CONFIG.worldPadding || 30, (worldRef?.w || item.x) - (CONFIG.worldPadding || 30));
|
||||
item.y = clamp(t.y + deterministicRange(worldRef, "pin-detach-y", (t.radius || 16) * 0.12, (t.radius || 16) * 0.72, item, t, reason), CONFIG.worldPadding || 30, (worldRef?.h || item.y) - (CONFIG.worldPadding || 30));
|
||||
}
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, (item.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" }));
|
||||
if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306e\u753b\u92f2\u304c\u629c\u3051\u843d\u3061\u305f\u3002`, "observe", { participants: [t] });
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
case "firecracker":
|
||||
case "genkotsu":
|
||||
case "rotator":
|
||||
case "poison_block":
|
||||
case "reciprocator":
|
||||
case "gate_fence":
|
||||
case "rope":
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@
|
|||
return Boolean(global.TarinaiMechanicalSystem?.updateReciprocator?.(item, dt, worldRef));
|
||||
}
|
||||
|
||||
function updatePoisonBlock(item, dt, worldRef) {
|
||||
return Boolean(global.TarinaiMechanicalSystem?.updatePoisonBlock?.(item, dt, worldRef));
|
||||
}
|
||||
|
||||
function resolveMechanicalInteractions(item, dt, worldRef) {
|
||||
return Boolean(global.TarinaiMechanicalSystem?.resolveInteractions?.(item, dt, worldRef));
|
||||
}
|
||||
|
|
@ -60,6 +64,7 @@
|
|||
if (item.type === "firecracker") return updateFirecracker(item, dt, worldRef);
|
||||
if (item.type === "genkotsu") return updateGenkotsu(item, dt, worldRef);
|
||||
if (item.type === "rotator") return updateRotator(item, dt, worldRef);
|
||||
if (item.type === "poison_block") return updatePoisonBlock(item, dt, worldRef);
|
||||
if (item.type === "reciprocator") return updateReciprocator(item, dt, worldRef);
|
||||
if (item.type === "gate_fence") return updateGateFence(item, dt, worldRef);
|
||||
if (item.type === "rope") return updateFlexibleLink(item, dt, worldRef);
|
||||
|
|
@ -73,6 +78,7 @@
|
|||
updateGenkotsu,
|
||||
updateRotator,
|
||||
updateReciprocator,
|
||||
updatePoisonBlock,
|
||||
updateFlexibleLink,
|
||||
updateRigidLink,
|
||||
resolveMechanicalInteractions,
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
const DUPLICATOR_STORABLE_TYPES = new Set([
|
||||
"food", "sweet", "love_mochi", "fight_mochi", "sleep_drug",
|
||||
"laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug",
|
||||
"zunda_juice", "grass", "water", "pushpin", "oshibyo"
|
||||
"zunda_juice", "water", "pushpin", "oshibyo"
|
||||
]);
|
||||
|
||||
function duplicatorLoadTypeForItem(item) {
|
||||
if (!item || item.dead || item.type === "duplicator") return "";
|
||||
const type = String(item.type || "");
|
||||
if (!type || type === "water_bowl") return "";
|
||||
if (!type || type === "water_bowl" || type === "grass") return "";
|
||||
if ((typeof isPinType === "function" && isPinType(type)) && item.pinState === "lodged") return "";
|
||||
// Explicit list first. これで「ずんだ餅」「へこ餅」「けんか餅」が
|
||||
// serving-food 判定や medicine role の揺れに左右されず複製機へ入る。
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Unified item definition source: tools, item traits, visuals, food metadata, and item effects.
|
||||
|
||||
|
|
@ -10,11 +40,11 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
undo: { id: "undo", label: "戻す", placeable: false, scalable: false, iconText: "↶", tooltip: "直前の操作を取り消す。" },
|
||||
redo: { id: "redo", label: "進む", placeable: false, scalable: false, iconText: "↷", tooltip: "取り消した操作をやり直す。" },
|
||||
poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" },
|
||||
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u7269\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002\u51b7\u51cd\u5eab\u306b\u3082\u904b\u3079\u308b\u3002" },
|
||||
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つの対象を順にクリックして、伸び縮みしないヒモで接続する。機械体・柵・ヒモ・棒はクリック地点、それ以外は中心につなぐ。" },
|
||||
rod: { id: "rod", label: "棒", placeable: false, scalable: false, iconText: "━", tooltip: "2つの対象を順にクリックして、曲がらない棒で接続する。距離を常に一定に保つ。" },
|
||||
rod: { id: "rod", label: "棒", placeable: false, scalable: false, iconText: "━", tooltip: "2つの対象を順にクリックして、曲がらない棒で接続する。" },
|
||||
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" },
|
||||
|
|
@ -48,6 +78,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を切り替えられる。" },
|
||||
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 },
|
||||
|
|
@ -58,20 +89,20 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
|
||||
|
||||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator"]),
|
||||
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "genkotsu", "pushpin", "oshibyo", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
hazard: new Set(["firecracker", "genkotsu", "pushpin", "oshibyo", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator"]),
|
||||
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]),
|
||||
});
|
||||
|
||||
|
||||
const FENCE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence"]));
|
||||
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "reciprocator"]));
|
||||
const MECHANICAL_ITEM_TYPES = Object.freeze(new Set(["rotator", "poison_block", "reciprocator"]));
|
||||
const LINK_ITEM_TYPES = Object.freeze(new Set(["rope", "rod"]));
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "fence_h", "bounce_fence", "gate_fence", "rotator", "reciprocator"]);
|
||||
const PHYSICS_TOOL_IDS = Object.freeze(["rope", "rod", "fence_h", "bounce_fence", "gate_fence", "rotator", "poison_block", "reciprocator"]);
|
||||
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set([...FENCE_ITEM_TYPES, ...MECHANICAL_ITEM_TYPES]));
|
||||
function isFenceItemType(type = "") { return FENCE_ITEM_TYPES.has(String(type || "")); }
|
||||
function isMechanicalItemType(type = "") { return MECHANICAL_ITEM_TYPES.has(String(type || "")); }
|
||||
|
|
@ -493,7 +524,7 @@ function toolTipsFromDefinitions() {
|
|||
Object.assign(rawDefinitions.love_mochi, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const scale = context.scale ?? 1;
|
||||
tarinai.loveMochiTimer = Math.max(tarinai.loveMochiTimer || 0, 42 * scale + rand(4, 12));
|
||||
tarinai.loveMochiTimer = Math.max(tarinai.loveMochiTimer || 0, 42 * scale + deterministicRange(tarinai.world, "love-mochi-duration", 4, 12, tarinai, context.item));
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { fulfill: -(context.source === "eating" ? nutrition * 0.5 : 12), social: -4 });
|
||||
if (context.source === "eating") {
|
||||
|
|
@ -506,7 +537,7 @@ function toolTipsFromDefinitions() {
|
|||
Object.assign(rawDefinitions.fight_mochi, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const scale = context.scale ?? 1;
|
||||
tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + rand(10, 18));
|
||||
tarinai.fightMochiTimer = Math.max(tarinai.fightMochiTimer || 0, 52 * scale + deterministicRange(tarinai.world, "fight-mochi-duration", 10, 18, tarinai, context.item));
|
||||
const nutrition = Number(context.nutrition) || 0;
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(tarinai, { safety: context.source === "eating" ? nutrition * 0.5 : 16 });
|
||||
if (context.source === "eating") {
|
||||
|
|
@ -529,7 +560,7 @@ function toolTipsFromDefinitions() {
|
|||
const sleepChance = tarinai.diseaseChance ? tarinai.diseaseChance(baseChance) : baseChance;
|
||||
if (tarinai.forceBehavior) tarinai.forceBehavior("sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
else if (typeof queueForcedTarinaiBehavior === "function") queueForcedTarinaiBehavior(tarinai, "sleep_anywhere", { source: "sleep_drug", priority: 145, ttl: 14, duration: 8, minDuration: 8, causeText: "\u306d\u3080\u308a\u85ac\u306e\u52b9\u679c" });
|
||||
if (!tarinai.sleepDisease && Math.random() < Math.min(0.98, sleepChance)) return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
if (!tarinai.sleepDisease && deterministicChance(tarinai.world, "sleep-drug-disease", Math.min(0.98, sleepChance), tarinai, context.item)) return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
return true;
|
||||
}
|
||||
return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,72 @@
|
|||
|
||||
// Item field rendering. Kept separate from construction and lifecycle logic.
|
||||
|
||||
function segmentPathCacheKey(item, segments, extra = "") {
|
||||
const version = Number(item?._mechanicalShapeVersion || item?._segmentsVersion || 0) || 0;
|
||||
const len = Array.isArray(segments) ? segments.length : 0;
|
||||
let h = len * 2654435761;
|
||||
if (Array.isArray(segments)) {
|
||||
const step = Math.max(1, Math.floor(len / 24));
|
||||
for (let i = 0; i < len; i += step) {
|
||||
const seg = segments[i];
|
||||
if (!Array.isArray(seg)) continue;
|
||||
for (let j = 0; j < 4; j += 1) {
|
||||
h ^= Math.round((Number(seg[j]) || 0) * 10) & 0xffff;
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
}
|
||||
}
|
||||
return `${item?.type || ""}|${version}|${len}|${h >>> 0}|${extra}`;
|
||||
}
|
||||
|
||||
function getSegmentPath(item, segments, extra = "") {
|
||||
if (typeof Path2D === "undefined" || !Array.isArray(segments)) return null;
|
||||
const key = segmentPathCacheKey(item, segments, extra);
|
||||
const cache = item._segmentRenderPathCache || (item._segmentRenderPathCache = Object.create(null));
|
||||
if (cache.key === key && cache.path) return cache.path;
|
||||
const path = new Path2D();
|
||||
for (const seg of segments) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
path.moveTo(Number(seg[0]) || 0, Number(seg[1]) || 0);
|
||||
path.lineTo(Number(seg[2]) || 0, Number(seg[3]) || 0);
|
||||
}
|
||||
cache.key = key;
|
||||
cache.path = path;
|
||||
return path;
|
||||
}
|
||||
|
||||
function strokeSegments(ctx, item, segments, extra = "") {
|
||||
const path = getSegmentPath(item, segments, extra);
|
||||
if (path) {
|
||||
ctx.stroke(path);
|
||||
return;
|
||||
}
|
||||
ctx.beginPath();
|
||||
for (const seg of segments || []) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
ctx.moveTo(Number(seg[0]) || 0, Number(seg[1]) || 0);
|
||||
ctx.lineTo(Number(seg[2]) || 0, Number(seg[3]) || 0);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function mechanicalRenderTier() {
|
||||
return window.TarinaiPerf?.renderQualityTier?.() || "high";
|
||||
}
|
||||
|
||||
function drawRopeParticlePath(ctx, particles, ox, oy) {
|
||||
if (!Array.isArray(particles) || particles.length < 2) return false;
|
||||
const tier = mechanicalRenderTier();
|
||||
const stride = tier === "low" && particles.length > 10 ? 2 : 1;
|
||||
ctx.moveTo((Number(particles[0].x) || 0) - ox, (Number(particles[0].y) || 0) - oy);
|
||||
for (let i = stride; i < particles.length - 1; i += stride) {
|
||||
ctx.lineTo((Number(particles[i].x) || 0) - ox, (Number(particles[i].y) || 0) - oy);
|
||||
}
|
||||
const last = particles[particles.length - 1];
|
||||
ctx.lineTo((Number(last.x) || 0) - ox, (Number(last.y) || 0) - oy);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Object.assign(Item.prototype, {
|
||||
draw(ctx, t, lighting = null) {
|
||||
|
|
@ -549,10 +615,12 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.restore();
|
||||
} else if (this.type === "rotator") {
|
||||
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||
const powered = this.rotatorPowered !== false;
|
||||
const speed = Number((powered ? this.rotatorSpeed : this.rotatorAngularVelocity) || 0) || 0;
|
||||
const segments = (world?.rotatorSegments?.(this) || (Array.isArray(this.rotatorSegments) ? this.rotatorSegments : [[-78, 0, 78, 0], [0, -52, 0, 52]]));
|
||||
const thick = Math.max(4, Math.min(34, Number(this.rotatorThickness || 12) || 12));
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
const powered = pb?.scalar?.(this, "motorOn", true) !== false;
|
||||
const speed = Number((powered ? pb?.scalar?.(this, "motorSpeed", 0) : pb?.scalar?.(this, "spin", 0)) || 0) || 0;
|
||||
const segments = (pb?.segments?.(this) || [[-78, 0, 78, 0], [0, -52, 0, 52]]);
|
||||
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 12) || 12) || 12));
|
||||
const tier = mechanicalRenderTier();
|
||||
ctx.save();
|
||||
ctx.rotate(angle);
|
||||
ctx.lineCap = "round";
|
||||
|
|
@ -561,24 +629,14 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.strokeStyle = styleNight ? "#6e5f89" : "#8b6cc5";
|
||||
ctx.lineWidth = thick;
|
||||
ctx.shadowColor = powered ? (styleNight ? "rgba(116,92,170,0.38)" : "rgba(126,82,190,0.30)") : "rgba(88,80,96,0.20)";
|
||||
ctx.shadowBlur = Math.max(4, thick * 0.28);
|
||||
ctx.beginPath();
|
||||
for (const seg of segments) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
ctx.moveTo(Number(seg[0]) || 0, Number(seg[1]) || 0);
|
||||
ctx.lineTo(Number(seg[2]) || 0, Number(seg[3]) || 0);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.16) : Math.max(4, thick * 0.28));
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = powered ? (styleNight ? "rgba(236,220,255,0.44)" : "rgba(255,255,255,0.46)") : "rgba(255,255,255,0.28)";
|
||||
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
||||
ctx.beginPath();
|
||||
for (const seg of segments) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
ctx.moveTo(Number(seg[0]) || 0, Number(seg[1]) || 0);
|
||||
ctx.lineTo(Number(seg[2]) || 0, Number(seg[3]) || 0);
|
||||
if (tier !== "low") {
|
||||
ctx.strokeStyle = powered ? (styleNight ? "rgba(236,220,255,0.44)" : "rgba(255,255,255,0.46)") : "rgba(255,255,255,0.28)";
|
||||
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
ctx.fillStyle = powered ? (styleNight ? "#3c324e" : "#f4eaff") : (styleNight ? "#34343a" : "#ece8ef");
|
||||
ctx.strokeStyle = powered ? (styleNight ? "#a28cc8" : "#6e4fa4") : "rgba(92,88,98,0.82)";
|
||||
|
|
@ -591,20 +649,59 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(powered ? (speed >= 0 ? "↻" : "↺") : "○", 0, 0);
|
||||
} else if (this.type === "poison_block") {
|
||||
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
const enabled = pb?.scalar?.(this, "solid", true) !== false;
|
||||
const segments = (pb?.segments?.(this) || [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]]);
|
||||
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 14) || 14) || 14));
|
||||
const tier = mechanicalRenderTier();
|
||||
ctx.save();
|
||||
ctx.rotate(angle);
|
||||
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.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.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.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.font = `bold ${Math.max(10, thick * 0.84)}px ui-rounded, sans-serif`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(enabled ? "毒" : "透", 0, 0);
|
||||
} else if (this.type === "reciprocator") {
|
||||
const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
|
||||
const axisAngle = window.TarinaiMechanicalSystem?.reciprocatorAxisAngle?.(this) ?? (Number.isFinite(Number(this.reciprocatorAxisAngle)) ? Number(this.reciprocatorAxisAngle) : bodyAngle);
|
||||
const powered = this.reciprocatorPowered !== false;
|
||||
const thick = Math.max(4, Math.min(34, Number(this.rotatorThickness || 12) || 12));
|
||||
const travel = Math.max(24, Number(this.reciprocatorTravel || 150) || 150);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
const axisAngle = window.TarinaiMechanicalSystem?.railAxisAngle?.(this) ?? pb?.scalar?.(this, "railAxis", bodyAngle) ?? bodyAngle;
|
||||
const powered = pb?.scalar?.(this, "railOn", true) !== false;
|
||||
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 12) || 12) || 12));
|
||||
const tier = mechanicalRenderTier();
|
||||
const travel = Math.max(24, Number(pb?.scalar?.(this, "railTravel", 150) || 150) || 150);
|
||||
const halfTravel = travel * 0.5;
|
||||
const phase = Math.max(-1, Math.min(1, Number(this.reciprocatorPhase || 0) || 0));
|
||||
const reciprocatorDir = Math.sign(Number(this.reciprocatorDirection || 1) || 1) || 1;
|
||||
const passiveVelocity = Number(this.reciprocatorVelocity || 0) || 0;
|
||||
const motorVelocity = powered ? reciprocatorDir * Math.max(0, Number(this.reciprocatorSpeed || 0) || 0) : 0;
|
||||
const phase = Math.max(-1, Math.min(1, Number(pb?.scalar?.(this, "railPhase", 0) || 0) || 0));
|
||||
const reciprocatorDir = Math.sign(Number(pb?.scalar?.(this, "railDir", 1) || 1) || 1) || 1;
|
||||
const passiveVelocity = Number(pb?.scalar?.(this, "slideSpeed", 0) || 0) || 0;
|
||||
const motorVelocity = powered ? reciprocatorDir * Math.max(0, Number(pb?.scalar?.(this, "railMotorSpeed", 0) || 0) || 0) : 0;
|
||||
const visualVelocity = motorVelocity + passiveVelocity;
|
||||
const visualDirection = Math.abs(visualVelocity) > 0.8 ? Math.sign(visualVelocity) : reciprocatorDir;
|
||||
const segments = (world?.rotatorSegments?.(this) || (Array.isArray(this.rotatorSegments) ? this.rotatorSegments : [[-78, 0, 78, 0]]));
|
||||
const segments = (pb?.segments?.(this) || [[-78, 0, 78, 0]]);
|
||||
ctx.save();
|
||||
ctx.rotate(axisAngle);
|
||||
ctx.save();
|
||||
|
|
@ -631,24 +728,14 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.strokeStyle = styleNight ? "#4f7192" : "#6da6cf";
|
||||
ctx.lineWidth = thick;
|
||||
ctx.shadowColor = powered ? (styleNight ? "rgba(86,130,180,0.34)" : "rgba(80,150,205,0.28)") : "rgba(74,82,90,0.20)";
|
||||
ctx.shadowBlur = Math.max(4, thick * 0.28);
|
||||
ctx.beginPath();
|
||||
for (const seg of segments) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
ctx.moveTo(Number(seg[0]) || 0, Number(seg[1]) || 0);
|
||||
ctx.lineTo(Number(seg[2]) || 0, Number(seg[3]) || 0);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = tier === "low" ? 0 : (tier === "mid" ? Math.max(2, thick * 0.16) : Math.max(4, thick * 0.28));
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.strokeStyle = powered ? (styleNight ? "rgba(220,238,255,0.44)" : "rgba(255,255,255,0.48)") : "rgba(255,255,255,0.28)";
|
||||
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
||||
ctx.beginPath();
|
||||
for (const seg of segments) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
ctx.moveTo(Number(seg[0]) || 0, Number(seg[1]) || 0);
|
||||
ctx.lineTo(Number(seg[2]) || 0, Number(seg[3]) || 0);
|
||||
if (tier !== "low") {
|
||||
ctx.strokeStyle = powered ? (styleNight ? "rgba(220,238,255,0.44)" : "rgba(255,255,255,0.48)") : "rgba(255,255,255,0.28)";
|
||||
ctx.lineWidth = Math.max(1.4, thick * 0.22);
|
||||
strokeSegments(ctx, this, segments, "body");
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
ctx.fillStyle = powered ? (styleNight ? "#2d3c4d" : "#eaf6ff") : (styleNight ? "#34383e" : "#e9edf0");
|
||||
ctx.strokeStyle = powered ? (styleNight ? "#7faed0" : "#4d8cba") : "rgba(82,88,94,0.82)";
|
||||
|
|
@ -670,8 +757,9 @@ draw(ctx, t, lighting = null) {
|
|||
}
|
||||
} else if (this.type === "rope" || this.type === "rod") {
|
||||
const runtime = window.TarinaiLinkRuntime;
|
||||
const a = runtime?.endpointWorld?.(this.linkA, world);
|
||||
const b = runtime?.endpointWorld?.(this.linkB, world);
|
||||
const pb = window.TarinaiPhysicsBodySystem;
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(this, 0), world);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(this, 1), world);
|
||||
if (a && b) {
|
||||
const ax = a.x - this.x, ay = a.y - this.y;
|
||||
const bx = b.x - this.x, by = b.y - this.y;
|
||||
|
|
@ -692,19 +780,30 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.lineTo(bx, by);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
const mx = Number.isFinite(Number(this.linkMidX)) ? Number(this.linkMidX) - this.x : (ax + bx) * 0.5;
|
||||
const my = Number.isFinite(Number(this.linkMidY)) ? Number(this.linkMidY) - this.y : (ay + by) * 0.5;
|
||||
const particles = Array.isArray(pb?.particles?.(this)) && pb.particles(this).length >= 2 ? pb.particles(this) : null;
|
||||
ctx.strokeStyle = styleNight ? "rgba(180,150,116,0.88)" : "rgba(132,94,54,0.92)";
|
||||
ctx.lineWidth = 4.2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.quadraticCurveTo(mx, my, bx, by);
|
||||
if (particles) {
|
||||
drawRopeParticlePath(ctx, particles, this.x, this.y);
|
||||
} else {
|
||||
const mx = Number.isFinite(Number(pb?.linkScalar?.(this, "midX", NaN))) ? Number(pb.linkScalar(this, "midX", 0)) - this.x : (ax + bx) * 0.5;
|
||||
const my = Number.isFinite(Number(pb?.linkScalar?.(this, "midY", NaN))) ? Number(pb.linkScalar(this, "midY", 0)) - this.y : (ay + by) * 0.5;
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.quadraticCurveTo(mx, my, bx, by);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = styleNight ? "rgba(236,214,180,0.56)" : "rgba(255,236,190,0.52)";
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.quadraticCurveTo(mx, my, bx, by);
|
||||
if (particles) {
|
||||
drawRopeParticlePath(ctx, particles, this.x, this.y);
|
||||
} else {
|
||||
const mx = Number.isFinite(Number(pb?.linkScalar?.(this, "midX", NaN))) ? Number(pb.linkScalar(this, "midX", 0)) - this.x : (ax + bx) * 0.5;
|
||||
const my = Number.isFinite(Number(pb?.linkScalar?.(this, "midY", NaN))) ? Number(pb.linkScalar(this, "midY", 0)) - this.y : (ay + by) * 0.5;
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.quadraticCurveTo(mx, my, bx, by);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.fillStyle = this.type === "rod" ? (styleNight ? "rgba(120,112,98,0.96)" : "rgba(114,96,72,0.96)") : (styleNight ? "rgba(112,84,54,0.95)" : "rgba(162,112,63,0.95)");
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
function initializeItemTypeState(item, type, x, y) {
|
||||
if (!item) return item;
|
||||
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 === "bed" || type === "nest_box") item.roles.sleepPlace = true;
|
||||
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "poison_block", "splat"].includes(type)) item.roles.danger = true;
|
||||
if (type === "grass") item.roles.grassMaterial = true;
|
||||
if (type === "grass") {
|
||||
item.grassStage = Math.floor(rand(0, 2.999));
|
||||
|
|
@ -110,12 +112,45 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.gateOpen = false;
|
||||
item.gateLastToggleAt = -999;
|
||||
}
|
||||
|
||||
if (type === "poison_block") {
|
||||
item.physicsBody = {
|
||||
schema: bodySchemaVersion,
|
||||
type: "poison_block",
|
||||
kind: "passive",
|
||||
pose: { x, y, angle: Number(item.angle) || 0 },
|
||||
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
||||
motor: { powered: false, speed: 0, direction: 1 },
|
||||
rail: null,
|
||||
shape: { model: "segments", thickness: 14, segments: [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]], version: 0 },
|
||||
collision: { solid: true, hazard: true },
|
||||
hazard: { kind: "poison", damage: 7 },
|
||||
sleep: { awakeUntil: 0 },
|
||||
mass: 4.8,
|
||||
inertia: 36000,
|
||||
};
|
||||
item.prevX = x;
|
||||
item.prevY = y;
|
||||
item.prevAngle = item.angle || 0;
|
||||
item.amount = 999;
|
||||
item.deletable = true;
|
||||
item.roles.danger = true;
|
||||
}
|
||||
|
||||
if (type === "rotator") {
|
||||
item.rotatorPowered = true;
|
||||
item.rotatorSpeed = Math.PI * 0.65;
|
||||
item.rotatorAngularVelocity = 0;
|
||||
item.rotatorThickness = 12;
|
||||
item.rotatorSegments = [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||
item.physicsBody = {
|
||||
schema: bodySchemaVersion,
|
||||
type: "rotator",
|
||||
kind: "rotational",
|
||||
pose: { x, y, angle: Number(item.angle) || 0 },
|
||||
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
||||
motor: { powered: true, speed: Math.PI * 0.65, direction: 1 },
|
||||
rail: null,
|
||||
shape: { model: "segments", thickness: 12, segments: [[-78, 0, 78, 0], [0, -52, 0, 52]], version: 0 },
|
||||
collision: { solid: true, hazard: false },
|
||||
hazard: null,
|
||||
sleep: { awakeUntil: 0 },
|
||||
};
|
||||
item.rotatorEditorOpenAt = -999;
|
||||
item.amount = 999;
|
||||
}
|
||||
|
|
@ -123,30 +158,39 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.amount = 999;
|
||||
item.r = 48;
|
||||
item.deletable = true;
|
||||
item.linkA = null;
|
||||
item.linkB = null;
|
||||
item.linkLength = 80;
|
||||
item.linkMidX = x;
|
||||
item.linkMidY = y + 10;
|
||||
item.linkMidVX = 0;
|
||||
item.linkMidVY = 0;
|
||||
item.physicsConstraint = {
|
||||
schema: constraintSchemaVersion,
|
||||
type,
|
||||
kind: type === "rope" ? "flexible-distance" : "rigid-distance",
|
||||
endpoints: [null, null],
|
||||
length: 80,
|
||||
mid: { x, y: y + 10, vx: 0, vy: 0 },
|
||||
sleep: { awakeUntil: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "reciprocator") {
|
||||
item.reciprocatorPowered = true;
|
||||
item.reciprocatorSpeed = 92;
|
||||
item.reciprocatorTravel = 150;
|
||||
item.reciprocatorPhase = 0;
|
||||
item.reciprocatorDirection = 1;
|
||||
item.reciprocatorAxisAngle = Number.isFinite(Number(item.angle)) ? item.angle : 0;
|
||||
item.reciprocatorVelocity = 0;
|
||||
item.reciprocatorAnchorX = x;
|
||||
item.reciprocatorAnchorY = y;
|
||||
item.rotatorThickness = 12;
|
||||
item.rotatorSegments = [[-78, 0, 78, 0]];
|
||||
item.physicsBody = {
|
||||
schema: bodySchemaVersion,
|
||||
type: "reciprocator",
|
||||
kind: "linear",
|
||||
pose: { x, y, angle: Number(item.angle) || 0 },
|
||||
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
||||
motor: { powered: true, speed: 92, direction: 1 },
|
||||
rail: { axisAngle: Number.isFinite(Number(item.angle)) ? Number(item.angle) : 0, travel: 150, phase: 0, anchorX: x, anchorY: y },
|
||||
shape: { model: "segments", thickness: 12, segments: [[-78, 0, 78, 0]], version: 0 },
|
||||
collision: { solid: true, hazard: false },
|
||||
hazard: null,
|
||||
sleep: { awakeUntil: 0 },
|
||||
};
|
||||
item.amount = 999;
|
||||
}
|
||||
|
||||
if (globalThis.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) {
|
||||
globalThis.TarinaiPhysicsBodySystem.purgeLegacyPhysicsStorage?.(item);
|
||||
globalThis.TarinaiPhysicsBodySystem.invalidateItem?.(item, "item-initialized");
|
||||
}
|
||||
|
||||
if (isServingFoodType(type)) {
|
||||
item.toolSize = "medium";
|
||||
item.foodServingScale = 1;
|
||||
|
|
|
|||
|
|
@ -5,17 +5,21 @@
|
|||
// helpers and the item scheduler both read this single policy instead of
|
||||
// duplicating item-type timing rules.
|
||||
(function (global) {
|
||||
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker", "rotator", "reciprocator", "rope", "rod"]);
|
||||
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker"]);
|
||||
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
|
||||
const SCHEDULED_ITEM_TYPES = Object.freeze([
|
||||
"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", "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", "water_bowl",
|
||||
]);
|
||||
|
||||
function itemUpdateInterval(item) {
|
||||
if (!item || item.dead) return Infinity;
|
||||
// Mechanical bodies and links are owned by the central physics step.
|
||||
// Keeping them out of the per-item scheduler prevents N independent
|
||||
// collision passes and makes future physics-format changes cheaper.
|
||||
if (item.type === "rotator" || item.type === "reciprocator" || item.type === "poison_block" || item.type === "rope" || item.type === "rod") return Infinity;
|
||||
if (REALTIME_ITEM_TYPES.has(item.type)) return 0;
|
||||
if (PIN_ITEM_TYPES.has(item.type)) return 0;
|
||||
if (item.isStructure && item.type === "plushie") return 0;
|
||||
|
|
|
|||
|
|
@ -153,12 +153,43 @@
|
|||
return ran;
|
||||
}
|
||||
|
||||
function physicsBudgetFor(worldRef) {
|
||||
const tier = global.TarinaiPerf?.renderQualityTier?.() || "high";
|
||||
const itemCount = (worldRef?.items || []).length || 0;
|
||||
const activeHint = worldRef?._itemUpdateRealtime?.length || 0;
|
||||
if (tier === "low") return { maxPairs: 140, maxLinks: 90, postMaxPairs: 36, maxSubsteps: 2 };
|
||||
if (tier === "medium" || tier === "mid") {
|
||||
const crowded = itemCount > 64 && activeHint <= 2;
|
||||
return crowded
|
||||
? { maxPairs: 150, maxLinks: 105, postMaxPairs: 44, maxSubsteps: 2 }
|
||||
: { maxPairs: 190, maxLinks: 130, postMaxPairs: 60, maxSubsteps: 3 };
|
||||
}
|
||||
return itemCount > 80
|
||||
? { maxPairs: 240, maxLinks: 170, postMaxPairs: 78, maxSubsteps: 4 }
|
||||
: { maxPairs: 300, maxLinks: 210, postMaxPairs: 100, maxSubsteps: 5 };
|
||||
}
|
||||
|
||||
function run(worldRef, dt) {
|
||||
const end = global.TarinaiPerf?.begin?.("update.items") || null;
|
||||
try {
|
||||
const state = ensure(worldRef);
|
||||
let ran = runRealtime(worldRef, state, dt);
|
||||
ran += runDue(worldRef, state);
|
||||
const realtimeEnd = global.TarinaiPerf?.begin?.("update.items.realtime") || null;
|
||||
let realtimeRan = runRealtime(worldRef, state, dt);
|
||||
let ran = realtimeRan;
|
||||
if (realtimeEnd) realtimeEnd();
|
||||
const dueEnd = global.TarinaiPerf?.begin?.("update.items.scheduled") || null;
|
||||
const dueRan = runDue(worldRef, state);
|
||||
ran += dueRan;
|
||||
if (dueEnd) dueEnd();
|
||||
const physicsEnd = global.TarinaiPerf?.begin?.("update.physicsWorld") || null;
|
||||
const physicsBudget = physicsBudgetFor(worldRef);
|
||||
const physicsWorld = global.TarinaiPhysicsWorldSystem?.updateWorld
|
||||
? global.TarinaiPhysicsWorldSystem.updateWorld(worldRef, dt, physicsBudget)
|
||||
: null;
|
||||
const mechanicalWorld = physicsWorld?.mechanical || (global.TarinaiMechanicalSystem?.updateWorld?.(worldRef, dt, { maxPairs: physicsBudget.maxPairs, maxLinks: 0, skipLinks: true, maxSubsteps: physicsBudget.maxSubsteps }) || null);
|
||||
const constraintWorld = physicsWorld ? physicsWorld.constraintRan : (global.TarinaiConstraintSystem?.updateWorld?.(worldRef, dt, { maxLinks: Math.min(180, physicsBudget.maxLinks) }) || 0);
|
||||
const postConstraintPairs = physicsWorld ? physicsWorld.postConstraintPairs : (constraintWorld ? (global.TarinaiMechanicalSystem?.resolveMechanicalPairs?.(worldRef, dt, { maxPairs: Math.min(80, physicsBudget.postMaxPairs) }) || 0) : 0);
|
||||
if (physicsEnd) physicsEnd();
|
||||
state.signature = schedulerSignature(worldRef);
|
||||
worldRef._itemUpdateSchedulerStats = {
|
||||
heap: state.heap.length,
|
||||
|
|
@ -166,8 +197,18 @@
|
|||
rebuilds: state.rebuilds,
|
||||
lastReason: state.reason,
|
||||
lastRan: ran,
|
||||
realtimeRan,
|
||||
dueRan,
|
||||
sleeping: state.heap.length,
|
||||
activeRealtime: state.realtime.length,
|
||||
physicsBudget,
|
||||
physics: physicsWorld || mechanicalWorld,
|
||||
constraints: constraintWorld,
|
||||
postConstraintPairs,
|
||||
mechanicalStats: worldRef._mechanicalWorldStats || null,
|
||||
constraintStats: worldRef._constraintWorldStats || null,
|
||||
};
|
||||
return ran;
|
||||
return ran + (physicsWorld?.ran || mechanicalWorld?.ran || 0) + constraintWorld;
|
||||
} finally {
|
||||
if (end) end();
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,18 @@
|
|||
|
||||
(function (global) {
|
||||
const nowMs = () => (global.performance?.now ? global.performance.now() : Date.now());
|
||||
const debugEnabled = (() => {
|
||||
try {
|
||||
const params = new URLSearchParams(global.location?.search || "");
|
||||
return params.get("debug") === "1" || params.get("debug") === "true" || params.get("perf") === "1";
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const buckets = new Map();
|
||||
const stack = [];
|
||||
const metrics = new Map();
|
||||
let frameOpen = null;
|
||||
let frameCount = 0;
|
||||
let lastQualityCheckAt = 0;
|
||||
|
|
@ -13,20 +24,33 @@
|
|||
function bucketFor(label) {
|
||||
let b = buckets.get(label);
|
||||
if (!b) {
|
||||
b = { label, ms: 0, avg: 0, max: 0, calls: 0, last: 0 };
|
||||
b = { label, ms: 0, selfMs: 0, avg: 0, selfAvg: 0, max: 0, calls: 0, last: 0, lastSelf: 0 };
|
||||
buckets.set(label, b);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
function begin(label) {
|
||||
const start = nowMs();
|
||||
if (!debugEnabled) return null;
|
||||
const frame = { label: String(label || "unknown"), start: nowMs(), childMs: 0 };
|
||||
stack.push(frame);
|
||||
return () => {
|
||||
const elapsed = Math.max(0, nowMs() - start);
|
||||
const b = bucketFor(label);
|
||||
const top = stack.pop();
|
||||
const active = top === frame ? frame : top || frame;
|
||||
// If calls ended out of order, keep the profiler alive rather than throwing.
|
||||
if (top !== frame) {
|
||||
const idx = stack.lastIndexOf(frame);
|
||||
if (idx >= 0) stack.splice(idx, 1);
|
||||
}
|
||||
const elapsed = Math.max(0, nowMs() - active.start);
|
||||
const selfElapsed = Math.max(0, elapsed - (active.childMs || 0));
|
||||
if (stack.length) stack[stack.length - 1].childMs += elapsed;
|
||||
const b = bucketFor(active.label);
|
||||
b.ms += elapsed;
|
||||
b.selfMs += selfElapsed;
|
||||
b.calls += 1;
|
||||
b.last = elapsed;
|
||||
b.lastSelf = selfElapsed;
|
||||
if (elapsed > b.max) b.max = elapsed;
|
||||
return elapsed;
|
||||
};
|
||||
|
|
@ -34,13 +58,17 @@
|
|||
|
||||
function beginFrame() {
|
||||
frameOpen = nowMs();
|
||||
if (debugEnabled) stack.length = 0;
|
||||
return frameOpen;
|
||||
}
|
||||
|
||||
function smoothBuckets() {
|
||||
if (!debugEnabled) return;
|
||||
for (const b of buckets.values()) {
|
||||
b.avg = b.avg ? b.avg * 0.82 + b.ms * 0.18 : b.ms;
|
||||
b.selfAvg = b.selfAvg ? b.selfAvg * 0.82 + b.selfMs * 0.18 : b.selfMs;
|
||||
b.ms = 0;
|
||||
b.selfMs = 0;
|
||||
b.calls = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -55,15 +83,18 @@
|
|||
|
||||
function endFrame(rawDt = 0, fps = global.__tarinaiFps || 0) {
|
||||
frameCount += 1;
|
||||
if (frameOpen != null) {
|
||||
const b = bucketFor("frame.total");
|
||||
if (frameOpen != null && debugEnabled) {
|
||||
const elapsed = Math.max(0, nowMs() - frameOpen);
|
||||
const b = bucketFor("frame.total");
|
||||
b.ms += elapsed;
|
||||
b.selfMs += Math.max(0, elapsed - stack.reduce((sum, f) => sum + (f.childMs || 0), 0));
|
||||
b.calls += 1;
|
||||
b.last = elapsed;
|
||||
b.lastSelf = b.last;
|
||||
if (elapsed > b.max) b.max = elapsed;
|
||||
frameOpen = null;
|
||||
}
|
||||
frameOpen = null;
|
||||
if (debugEnabled) stack.length = 0;
|
||||
const t = nowMs();
|
||||
if (t - lastQualityCheckAt >= 750) {
|
||||
lastQualityCheckAt = t;
|
||||
|
|
@ -78,38 +109,56 @@
|
|||
}
|
||||
}
|
||||
|
||||
function renderQualityTier() {
|
||||
return tier;
|
||||
}
|
||||
|
||||
function dprScaleValue() {
|
||||
return dprScale;
|
||||
}
|
||||
|
||||
function renderQualityTier() { return tier; }
|
||||
function dprScaleValue() { return dprScale; }
|
||||
function consumeResizeRequest() {
|
||||
const v = resizeRequested;
|
||||
resizeRequested = false;
|
||||
return v;
|
||||
}
|
||||
|
||||
function setMetric(name, value) {
|
||||
if (!debugEnabled) return;
|
||||
metrics.set(String(name || "metric"), value);
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
const frameAvg = buckets.get("frame.total")?.avg || 0;
|
||||
const entries = [...buckets.values()].map(b => ({
|
||||
label: b.label,
|
||||
avg: Number((b.avg || 0).toFixed(2)),
|
||||
selfAvg: Number((b.selfAvg || 0).toFixed(2)),
|
||||
max: Number((b.max || 0).toFixed(2)),
|
||||
last: Number((b.last || 0).toFixed(2)),
|
||||
lastSelf: Number((b.lastSelf || 0).toFixed(2)),
|
||||
calls: b.calls || 0,
|
||||
})).sort((a, b) => b.avg - a.avg);
|
||||
return { tier, dprScale, frameCount, entries };
|
||||
pct: frameAvg > 0 ? Number((((b.avg || 0) / frameAvg) * 100).toFixed(0)) : 0,
|
||||
selfPct: frameAvg > 0 ? Number((((b.selfAvg || 0) / frameAvg) * 100).toFixed(0)) : 0,
|
||||
}));
|
||||
const topInclusive = entries.slice().sort((a, b) => b.avg - a.avg).slice(0, 12);
|
||||
const topSelf = entries.slice().sort((a, b) => b.selfAvg - a.selfAvg).slice(0, 12);
|
||||
return {
|
||||
enabled: debugEnabled,
|
||||
tier,
|
||||
dprScale,
|
||||
frameCount,
|
||||
frameAvg: Number(frameAvg.toFixed(2)),
|
||||
entries: topInclusive,
|
||||
topInclusive,
|
||||
topSelf,
|
||||
metrics: Object.fromEntries(metrics.entries()),
|
||||
};
|
||||
}
|
||||
|
||||
global.TarinaiPerf = Object.freeze({
|
||||
enabled: debugEnabled,
|
||||
begin,
|
||||
beginFrame,
|
||||
endFrame,
|
||||
renderQualityTier,
|
||||
dprScale: dprScaleValue,
|
||||
consumeResizeRequest,
|
||||
setMetric,
|
||||
snapshot,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
589
js/physics_world_system.js
Normal file
589
js/physics_world_system.js
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: physics/world
|
||||
// Central schema, serialization, and orchestration layer for physical tools.
|
||||
// Physical state lives only in item.physicsBody / item.physicsConstraint.
|
||||
(function (global) {
|
||||
const BODY_SCHEMA_VERSION = 4;
|
||||
const CONSTRAINT_SCHEMA_VERSION = 4;
|
||||
const BODY_TYPES = new Set(["rotator", "poison_block", "reciprocator"]);
|
||||
const LINK_TYPES = new Set(["rope", "rod"]);
|
||||
|
||||
function num(value, fallback = 0) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
function bool(value, fallback = true) {
|
||||
if (value == null) return !!fallback;
|
||||
return !!value;
|
||||
}
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, num(value, min)));
|
||||
}
|
||||
function isBodyType(typeOrItem) {
|
||||
const type = typeof typeOrItem === "string" ? typeOrItem : String(typeOrItem?.type || "");
|
||||
return BODY_TYPES.has(type);
|
||||
}
|
||||
function isConstraintType(typeOrItem) {
|
||||
const type = typeof typeOrItem === "string" ? typeOrItem : String(typeOrItem?.type || "");
|
||||
return LINK_TYPES.has(type);
|
||||
}
|
||||
function isPhysicsType(typeOrItem) { return isBodyType(typeOrItem) || isConstraintType(typeOrItem); }
|
||||
|
||||
function defaultSegments(type) {
|
||||
if (type === "poison_block") return [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]];
|
||||
if (type === "rotator") return [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||
return [[-78, 0, 78, 0]];
|
||||
}
|
||||
function defaultKind(type) {
|
||||
if (type === "rotator") return "rotational";
|
||||
if (type === "reciprocator") return "linear";
|
||||
if (type === "poison_block") return "passive";
|
||||
return "none";
|
||||
}
|
||||
function cloneSegments(segments, fallback) {
|
||||
const source = Array.isArray(segments) && segments.length ? segments : fallback;
|
||||
const out = [];
|
||||
for (const seg of source || []) {
|
||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||
const x1 = clamp(seg[0], -420, 420), y1 = clamp(seg[1], -420, 420);
|
||||
const x2 = clamp(seg[2], -420, 420), y2 = clamp(seg[3], -420, 420);
|
||||
if (Math.hypot(x2 - x1, y2 - y1) < 4) continue;
|
||||
out.push([x1, y1, x2, y2]);
|
||||
if (out.length >= 128) break;
|
||||
}
|
||||
return out.length ? out : (fallback || [[-78, 0, 78, 0]]).map(seg => seg.slice());
|
||||
}
|
||||
function q(value, scale = 1000) { return Math.round(num(value) * scale); }
|
||||
|
||||
function defaultBody(item) {
|
||||
const type = String(item?.type || "");
|
||||
const x = num(item?.x), y = num(item?.y), angle = num(item?.angle);
|
||||
return {
|
||||
schema: BODY_SCHEMA_VERSION,
|
||||
type,
|
||||
kind: defaultKind(type),
|
||||
pose: { x, y, angle },
|
||||
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
||||
motor: { powered: type !== "poison_block", speed: type === "rotator" ? Math.PI * 0.65 : (type === "reciprocator" ? 92 : 0), direction: 1 },
|
||||
rail: type === "reciprocator" ? { axisAngle: angle, travel: 150, phase: 0, anchorX: x, anchorY: y } : null,
|
||||
shape: { model: "segments", thickness: type === "poison_block" ? 14 : 12, segments: cloneSegments(null, defaultSegments(type)), version: 0 },
|
||||
collision: { solid: true, hazard: type === "poison_block" },
|
||||
hazard: type === "poison_block" ? { kind: "poison", damage: 7 } : null,
|
||||
sleep: { awakeUntil: 0 },
|
||||
mass: type === "poison_block" ? 4.8 : 1,
|
||||
inertia: type === "poison_block" ? 36000 : 22000,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBody(body, item) {
|
||||
const type = String(item?.type || body?.type || "");
|
||||
const base = defaultBody({ type, x: item?.x, y: item?.y, angle: item?.angle });
|
||||
const out = body && typeof body === "object" ? body : base;
|
||||
out.schema = BODY_SCHEMA_VERSION;
|
||||
out.type = type;
|
||||
out.kind = out.kind || base.kind;
|
||||
out.pose = out.pose && typeof out.pose === "object" ? out.pose : base.pose;
|
||||
out.velocity = out.velocity && typeof out.velocity === "object" ? out.velocity : base.velocity;
|
||||
out.motor = out.motor && typeof out.motor === "object" ? out.motor : base.motor;
|
||||
out.rail = type === "reciprocator" ? ((out.rail && typeof out.rail === "object") ? out.rail : base.rail) : null;
|
||||
out.shape = out.shape && typeof out.shape === "object" ? out.shape : base.shape;
|
||||
out.collision = out.collision && typeof out.collision === "object" ? out.collision : base.collision;
|
||||
out.sleep = out.sleep && typeof out.sleep === "object" ? out.sleep : base.sleep;
|
||||
out.hazard = type === "poison_block" ? ((out.hazard && typeof out.hazard === "object") ? out.hazard : base.hazard) : null;
|
||||
out.mass = Math.max(0.2, num(out.mass, base.mass));
|
||||
out.inertia = Math.max(1, num(out.inertia, base.inertia));
|
||||
out.pose.x = num(out.pose.x, item?.x);
|
||||
out.pose.y = num(out.pose.y, item?.y);
|
||||
out.pose.angle = num(out.pose.angle, item?.angle);
|
||||
out.velocity.x = num(out.velocity.x);
|
||||
out.velocity.y = num(out.velocity.y);
|
||||
out.velocity.angular = num(out.velocity.angular);
|
||||
out.velocity.linear = num(out.velocity.linear);
|
||||
out.motor.powered = type === "poison_block" ? false : out.motor.powered !== false;
|
||||
out.motor.speed = Math.max(0, num(out.motor.speed, base.motor.speed));
|
||||
out.motor.direction = Math.sign(num(out.motor.direction, 1)) || 1;
|
||||
if (out.rail) {
|
||||
out.rail.axisAngle = num(out.rail.axisAngle, out.pose.angle);
|
||||
out.rail.travel = Math.max(24, num(out.rail.travel, 150));
|
||||
out.rail.phase = clamp(out.rail.phase, -1, 1);
|
||||
out.rail.anchorX = num(out.rail.anchorX, out.pose.x);
|
||||
out.rail.anchorY = num(out.rail.anchorY, out.pose.y);
|
||||
}
|
||||
out.shape.model = "segments";
|
||||
out.shape.thickness = Math.max(4, Math.min(34, num(out.shape.thickness, base.shape.thickness)));
|
||||
out.shape.segments = cloneSegments(out.shape.segments, defaultSegments(type));
|
||||
out.shape.version = Number(out.shape.version || 0) || 0;
|
||||
out.collision.solid = out.collision.solid !== false;
|
||||
out.collision.hazard = type === "poison_block" ? out.collision.hazard !== false : false;
|
||||
if (out.hazard) {
|
||||
out.hazard.kind = out.hazard.kind || "poison";
|
||||
out.hazard.damage = Math.max(1, num(out.hazard.damage, 7));
|
||||
}
|
||||
out.sleep.awakeUntil = num(out.sleep.awakeUntil);
|
||||
// During the mechanical solver we intentionally mutate item.x/y/angle as
|
||||
// a scratch pose, then commit that pose back to physicsBody at the end of
|
||||
// the step. Do not let scalar()/setScalar() normalization overwrite the
|
||||
// scratch pose with the old body pose while the step is in progress.
|
||||
if (item._physicsStepScratch !== true) {
|
||||
item.x = out.pose.x;
|
||||
item.y = out.pose.y;
|
||||
item.angle = out.pose.angle;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function itemPoseShouldFeedBody(item, opts = {}) {
|
||||
return opts.syncFromLegacy === true || item?._physicsStepScratch === true || item?.playerHeld === true || item?._heldByPlayer === true || item?._physicsExternalPoseDirty === true;
|
||||
}
|
||||
|
||||
function ensureBody(item, worldRef = null, opts = {}) {
|
||||
if (!item || item.dead || !isBodyType(item)) return null;
|
||||
item.world = worldRef || item.world || null;
|
||||
if (!item.physicsBody || typeof item.physicsBody !== "object" || item.physicsBody.type !== item.type || opts.rebuild === true) {
|
||||
item.physicsBody = defaultBody(item);
|
||||
markBodyChanged(item, opts.rebuild === true ? "rebuild" : "init");
|
||||
} else if (itemPoseShouldFeedBody(item, opts)) {
|
||||
const sx = num(item.x);
|
||||
const sy = num(item.y);
|
||||
const sa = num(item.angle);
|
||||
item.physicsBody.pose = item.physicsBody.pose || { x: sx, y: sy, angle: sa };
|
||||
item.physicsBody.pose.x = sx;
|
||||
item.physicsBody.pose.y = sy;
|
||||
item.physicsBody.pose.angle = sa;
|
||||
item._physicsExternalPoseDirty = false;
|
||||
}
|
||||
const body = normalizeBody(item.physicsBody, item);
|
||||
item.physicsBody = body;
|
||||
return body;
|
||||
}
|
||||
|
||||
function markBodyChanged(item, reason = "physics-body") {
|
||||
if (!item) return;
|
||||
item._physicsBodyVersion = (Number(item._physicsBodyVersion || 0) || 0) + 1;
|
||||
item._physicsBodyDirtyReason = reason;
|
||||
}
|
||||
|
||||
function bodySignature(item) {
|
||||
const b = ensureBody(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!b) return "";
|
||||
const s = b.shape || {};
|
||||
const v = b.velocity || {};
|
||||
const m = b.motor || {};
|
||||
const r = b.rail || {};
|
||||
const c = b.collision || {};
|
||||
const sl = b.sleep || {};
|
||||
return [
|
||||
b.type, q(b.pose?.x, 10), q(b.pose?.y, 10), q(b.pose?.angle, 10000),
|
||||
q(s.thickness, 10), Number(s.version || 0) || 0, Array.isArray(s.segments) ? s.segments.length : 0,
|
||||
c.solid === false ? 0 : 1, c.hazard === false ? 0 : 1,
|
||||
m.powered === false ? 0 : 1, q(m.speed, 1000), q(m.direction, 10),
|
||||
q(v.x, 1000), q(v.y, 1000), q(v.angular, 10000), q(v.linear, 1000),
|
||||
q(r.axisAngle, 10000), q(r.travel, 10), q(r.phase, 10000), q(r.anchorX, 10), q(r.anchorY, 10),
|
||||
q(sl.awakeUntil, 1000), q(b.hazard?.damage, 10), q(b.mass, 10), q(b.inertia, 1),
|
||||
].join(":");
|
||||
}
|
||||
|
||||
function scalar(item, key, fallback = 0) {
|
||||
const b = ensureBody(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!b) return fallback;
|
||||
switch (key) {
|
||||
case "thickness": return Math.max(4, Math.min(34, num(b.shape?.thickness, fallback)));
|
||||
case "motorOn": return b.motor?.powered !== false;
|
||||
case "motorSpeed": return num(b.motor?.speed, fallback);
|
||||
case "spin": return num(b.velocity?.angular, fallback);
|
||||
case "xv": return num(b.velocity?.x, fallback);
|
||||
case "yv": return num(b.velocity?.y, fallback);
|
||||
case "slideSpeed": return num(b.velocity?.linear, fallback);
|
||||
case "solid": return b.collision?.solid !== false;
|
||||
case "damage": return Math.max(1, num(b.hazard?.damage, fallback));
|
||||
case "mass": return Math.max(0.2, num(b.mass, fallback));
|
||||
case "inertia": return Math.max(1, num(b.inertia, fallback));
|
||||
case "railOn": return b.motor?.powered !== false;
|
||||
case "railMotorSpeed": return Math.max(0, num(b.motor?.speed, fallback));
|
||||
case "railTravel": return Math.max(24, num(b.rail?.travel, fallback));
|
||||
case "railPhase": return clamp(b.rail?.phase, -1, 1);
|
||||
case "railDir": return Math.sign(num(b.motor?.direction, fallback || 1)) || 1;
|
||||
case "railAxis": return num(b.rail?.axisAngle, fallback);
|
||||
case "railAnchorX": return num(b.rail?.anchorX, fallback);
|
||||
case "railAnchorY": return num(b.rail?.anchorY, fallback);
|
||||
case "awakeUntil": return num(b.sleep?.awakeUntil, fallback);
|
||||
default: return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function setScalar(item, key, value, reason = "physics-set") {
|
||||
const b = ensureBody(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!b) return false;
|
||||
b.velocity = b.velocity || {};
|
||||
b.motor = b.motor || {};
|
||||
b.rail = b.rail || (item.type === "reciprocator" ? { axisAngle: num(item.angle), travel: 150, phase: 0, anchorX: num(item.x), anchorY: num(item.y) } : null);
|
||||
b.shape = b.shape || { model: "segments", thickness: item.type === "poison_block" ? 14 : 12, segments: defaultSegments(item.type), version: 0 };
|
||||
b.collision = b.collision || { solid: true, hazard: item.type === "poison_block" };
|
||||
b.sleep = b.sleep || { awakeUntil: 0 };
|
||||
if (item.type === "poison_block") b.hazard = b.hazard || { kind: "poison", damage: 7 };
|
||||
switch (key) {
|
||||
case "thickness": b.shape.thickness = Math.max(4, Math.min(34, num(value, item.type === "poison_block" ? 14 : 12))); b.shape.version = (Number(b.shape.version || 0) || 0) + 1; break;
|
||||
case "motorOn": b.motor.powered = item.type === "poison_block" ? false : !!value; break;
|
||||
case "motorSpeed": b.motor.speed = Math.max(0, num(value)); break;
|
||||
case "spin": b.velocity.angular = num(value); break;
|
||||
case "xv": b.velocity.x = num(value); break;
|
||||
case "yv": b.velocity.y = num(value); break;
|
||||
case "slideSpeed": b.velocity.linear = num(value); break;
|
||||
case "solid": b.collision.solid = !!value; break;
|
||||
case "damage": b.hazard = b.hazard || { kind: "poison", damage: 7 }; b.hazard.damage = Math.max(1, num(value, 7)); break;
|
||||
case "mass": b.mass = Math.max(0.2, num(value, 4.8)); break;
|
||||
case "inertia": b.inertia = Math.max(1, num(value, 36000)); break;
|
||||
case "railOn": b.motor.powered = !!value; break;
|
||||
case "railMotorSpeed": b.motor.speed = Math.max(0, num(value, 92)); break;
|
||||
case "railTravel": if (b.rail) b.rail.travel = Math.max(24, num(value, 150)); break;
|
||||
case "railPhase": if (b.rail) b.rail.phase = clamp(value, -1, 1); break;
|
||||
case "railDir": b.motor.direction = Math.sign(num(value, 1)) || 1; break;
|
||||
case "railAxis": if (b.rail) b.rail.axisAngle = num(value, b.pose?.angle); break;
|
||||
case "railAnchorX": if (b.rail) b.rail.anchorX = num(value, b.pose?.x); break;
|
||||
case "railAnchorY": if (b.rail) b.rail.anchorY = num(value, b.pose?.y); break;
|
||||
case "awakeUntil": b.sleep.awakeUntil = num(value); break;
|
||||
default: return false;
|
||||
}
|
||||
normalizeBody(b, item);
|
||||
markBodyChanged(item, reason);
|
||||
if (key === "thickness" || key === "solid") global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
function segments(item) { return cloneSegments(ensureBody(item, item?.world || null, { syncFromLegacy: false })?.shape?.segments, defaultSegments(item?.type)); }
|
||||
function setSegments(item, next, reason = "shape-edited") {
|
||||
const b = ensureBody(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!b) return false;
|
||||
b.shape = b.shape || {};
|
||||
b.shape.model = "segments";
|
||||
b.shape.segments = cloneSegments(next, defaultSegments(item.type));
|
||||
b.shape.version = (Number(b.shape.version || 0) || 0) + 1;
|
||||
markBodyChanged(item, reason);
|
||||
global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
|
||||
return true;
|
||||
}
|
||||
function pose(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.pose || null; }
|
||||
function velocity(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.velocity || null; }
|
||||
function motor(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.motor || null; }
|
||||
function rail(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.rail || null; }
|
||||
function shape(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.shape || null; }
|
||||
function collision(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.collision || null; }
|
||||
function hazard(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.hazard || null; }
|
||||
function sleep(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.sleep || null; }
|
||||
|
||||
function syncPoseFromItem(item) {
|
||||
if (!item) return null;
|
||||
// Capture the item pose before ensureBody()/normalizeBody() can write the
|
||||
// current body pose back to the item. This is required for scratch-solver
|
||||
// movement and direct player grabbing.
|
||||
const sx = num(item.x);
|
||||
const sy = num(item.y);
|
||||
const sa = num(item.angle);
|
||||
const b = ensureBody(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!b) return null;
|
||||
b.pose = b.pose || { x: sx, y: sy, angle: sa };
|
||||
b.pose.x = sx;
|
||||
b.pose.y = sy;
|
||||
b.pose.angle = sa;
|
||||
item.x = sx;
|
||||
item.y = sy;
|
||||
item.angle = sa;
|
||||
return b;
|
||||
}
|
||||
function applyPoseToItem(item) {
|
||||
const b = ensureBody(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!b) return false;
|
||||
const before = `${num(item.x).toFixed(3)}:${num(item.y).toFixed(3)}:${num(item.angle).toFixed(5)}`;
|
||||
item.x = num(b.pose?.x, item.x);
|
||||
item.y = num(b.pose?.y, item.y);
|
||||
item.angle = num(b.pose?.angle, item.angle);
|
||||
const after = `${num(item.x).toFixed(3)}:${num(item.y).toFixed(3)}:${num(item.angle).toFixed(5)}`;
|
||||
return before !== after;
|
||||
}
|
||||
|
||||
function endpointToPlain(endpoint) {
|
||||
if (!endpoint) return null;
|
||||
return {
|
||||
kind: endpoint.kind || "item",
|
||||
id: endpoint.id || "",
|
||||
type: endpoint.type || "",
|
||||
label: endpoint.label || "",
|
||||
liveToken: endpoint.liveToken ?? null,
|
||||
localX: num(endpoint.localX),
|
||||
localY: num(endpoint.localY),
|
||||
center: !!endpoint.center,
|
||||
x: num(endpoint.x),
|
||||
y: num(endpoint.y),
|
||||
attachT: Number.isFinite(Number(endpoint.attachT)) ? num(endpoint.attachT) : null,
|
||||
fuzzyResolve: endpoint.fuzzyResolve === true,
|
||||
supportLost: endpoint.supportLost === true,
|
||||
_ref: endpoint._ref || null,
|
||||
_supportItemId: endpoint._supportItemId || "",
|
||||
_supportCheckedVersion: Number(endpoint._supportCheckedVersion || -1) || -1,
|
||||
_supportAlive: endpoint._supportAlive !== false,
|
||||
};
|
||||
}
|
||||
function defaultConstraint(item) {
|
||||
const x = num(item?.x), y = num(item?.y);
|
||||
return {
|
||||
schema: CONSTRAINT_SCHEMA_VERSION,
|
||||
type: item?.type,
|
||||
kind: item?.type === "rope" ? "flexible-distance" : "rigid-distance",
|
||||
endpoints: [null, null],
|
||||
length: 80,
|
||||
mid: { x, y: y + 10, vx: 0, vy: 0 },
|
||||
sleep: { awakeUntil: 0 },
|
||||
particles: null,
|
||||
};
|
||||
}
|
||||
function normalizeConstraint(c, item) {
|
||||
const base = defaultConstraint(item);
|
||||
const out = c && typeof c === "object" ? c : base;
|
||||
out.schema = CONSTRAINT_SCHEMA_VERSION;
|
||||
out.type = item?.type;
|
||||
out.kind = out.kind || base.kind;
|
||||
out.endpoints = Array.isArray(out.endpoints) ? out.endpoints : [null, null];
|
||||
out.endpoints[0] = endpointToPlain(out.endpoints[0]);
|
||||
out.endpoints[1] = endpointToPlain(out.endpoints[1]);
|
||||
out.length = Math.max(24, num(out.length, 80));
|
||||
out.mid = out.mid && typeof out.mid === "object" ? out.mid : base.mid;
|
||||
out.mid.x = num(out.mid.x, item?.x);
|
||||
out.mid.y = num(out.mid.y, item?.y);
|
||||
out.mid.vx = num(out.mid.vx);
|
||||
out.mid.vy = num(out.mid.vy);
|
||||
out.sleep = out.sleep && typeof out.sleep === "object" ? out.sleep : base.sleep;
|
||||
out.sleep.awakeUntil = num(out.sleep.awakeUntil);
|
||||
out.particles = Array.isArray(out.particles) ? out.particles : null;
|
||||
item.r = Math.max(18, item.type === "rod" ? out.length * 0.5 : (num(item.r, out.length * 0.5) || out.length * 0.5));
|
||||
return out;
|
||||
}
|
||||
function ensureConstraint(item, worldRef = null, opts = {}) {
|
||||
if (!item || item.dead || !isConstraintType(item)) return null;
|
||||
item.world = worldRef || item.world || null;
|
||||
if (!item.physicsConstraint || typeof item.physicsConstraint !== "object" || item.physicsConstraint.type !== item.type || opts.rebuild === true) {
|
||||
item.physicsConstraint = defaultConstraint(item);
|
||||
}
|
||||
item.physicsConstraint = normalizeConstraint(item.physicsConstraint, item);
|
||||
return item.physicsConstraint;
|
||||
}
|
||||
function endpoint(item, index = 0) { return ensureConstraint(item, item?.world || null, { syncFromLegacy: false })?.endpoints?.[index] || null; }
|
||||
function setEndpoint(item, index, value) {
|
||||
const c = ensureConstraint(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!c) return false;
|
||||
c.endpoints[index] = endpointToPlain(value);
|
||||
return true;
|
||||
}
|
||||
function linkScalar(item, key, fallback = 0) {
|
||||
const c = ensureConstraint(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!c) return fallback;
|
||||
switch (key) {
|
||||
case "len": return Math.max(24, num(c.length, fallback));
|
||||
case "midX": return num(c.mid?.x, fallback);
|
||||
case "midY": return num(c.mid?.y, fallback);
|
||||
case "midVx": return num(c.mid?.vx, fallback);
|
||||
case "midVy": return num(c.mid?.vy, fallback);
|
||||
case "awakeUntil": return num(c.sleep?.awakeUntil, fallback);
|
||||
default: return fallback;
|
||||
}
|
||||
}
|
||||
function setLinkScalar(item, key, value) {
|
||||
const c = ensureConstraint(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!c) return false;
|
||||
c.mid = c.mid || { x: num(item.x), y: num(item.y), vx: 0, vy: 0 };
|
||||
c.sleep = c.sleep || { awakeUntil: 0 };
|
||||
switch (key) {
|
||||
case "len": c.length = Math.max(24, num(value, 80)); item.r = Math.max(18, c.length * 0.5); break;
|
||||
case "midX": c.mid.x = num(value, item.x); break;
|
||||
case "midY": c.mid.y = num(value, item.y); break;
|
||||
case "midVx": c.mid.vx = num(value); break;
|
||||
case "midVy": c.mid.vy = num(value); break;
|
||||
case "awakeUntil": c.sleep.awakeUntil = num(value); break;
|
||||
default: return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function particles(item) {
|
||||
const c = ensureConstraint(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!c) return null;
|
||||
return c.particles;
|
||||
}
|
||||
function setParticles(item, list) {
|
||||
const c = ensureConstraint(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!c) return false;
|
||||
c.particles = Array.isArray(list) ? list : null;
|
||||
return true;
|
||||
}
|
||||
function constraintSignature(item) {
|
||||
const c = ensureConstraint(item, item?.world || null, { syncFromLegacy: false });
|
||||
if (!c) return "";
|
||||
const epSig = ep => !ep ? "null" : [ep.kind || "item", ep.id || "", ep.type || "", q(ep.localX, 10), q(ep.localY, 10), ep.center ? 1 : 0, q(ep.x, 10), q(ep.y, 10), ep.attachT == null ? "" : q(ep.attachT, 1000)].join("/");
|
||||
return [item.type || "", q(c.length, 10), q(c.mid?.x, 10), q(c.mid?.y, 10), q(c.mid?.vx, 1000), q(c.mid?.vy, 1000), q(c.sleep?.awakeUntil, 1000), epSig(c.endpoints?.[0]), epSig(c.endpoints?.[1])].join(":");
|
||||
}
|
||||
|
||||
function collectPhysicsItems(worldRef) {
|
||||
const bodies = [];
|
||||
const constraints = [];
|
||||
if (!worldRef?.itemsOfType) return { bodies, constraints };
|
||||
worldRef.ensureItemBuckets?.("physics-world-system");
|
||||
for (const type of BODY_TYPES) for (const item of worldRef.itemsOfType(type) || []) if (item && !item.dead) bodies.push(item);
|
||||
for (const type of LINK_TYPES) for (const item of worldRef.itemsOfType(type) || []) if (item && !item.dead) constraints.push(item);
|
||||
return { bodies, constraints };
|
||||
}
|
||||
function prepareWorld(worldRef, opts = {}) {
|
||||
const { bodies, constraints } = collectPhysicsItems(worldRef);
|
||||
for (const item of bodies) ensureBody(item, worldRef, { syncFromLegacy: item?.playerHeld === true || item?._heldByPlayer === true || item?._physicsExternalPoseDirty === true });
|
||||
for (const item of constraints) ensureConstraint(item, worldRef, { syncFromLegacy: false });
|
||||
const frame = { id: (worldRef._physicsFrameSeq = (Number(worldRef._physicsFrameSeq || 0) || 0) + 1), time: num(worldRef?.time), bodies, constraints, bodyCount: bodies.length, constraintCount: constraints.length, sync: { bodyReads: 0, bodyWrites: 0, constraintReads: 0, constraintWrites: 0 } };
|
||||
worldRef._physicsWorldFrame = frame;
|
||||
return frame;
|
||||
}
|
||||
function commitWorld(worldRef, frame = worldRef?._physicsWorldFrame, opts = {}) {
|
||||
if (!frame) return { bodies: 0, constraints: 0, moved: 0 };
|
||||
let moved = 0;
|
||||
for (const item of frame.bodies || []) {
|
||||
const before = `${num(item.x).toFixed(3)}:${num(item.y).toFixed(3)}:${num(item.angle).toFixed(5)}`;
|
||||
ensureBody(item, worldRef, { syncFromLegacy: false });
|
||||
applyPoseToItem(item);
|
||||
const after = `${num(item.x).toFixed(3)}:${num(item.y).toFixed(3)}:${num(item.angle).toFixed(5)}`;
|
||||
if (before !== after) moved += 1;
|
||||
}
|
||||
for (const item of frame.constraints || []) ensureConstraint(item, worldRef, { syncFromLegacy: false });
|
||||
if (opts.markDirty && moved) worldRef?.markSpatialDirty?.("physics-world-commit");
|
||||
return { bodies: frame.bodies?.length || 0, constraints: frame.constraints?.length || 0, moved };
|
||||
}
|
||||
function updateWorld(worldRef, dt = 0.016, opts = {}) {
|
||||
if (!worldRef?.itemsOfType) return { ran: 0, bodies: 0, constraints: 0 };
|
||||
const profiler = global.TarinaiPerf;
|
||||
const end = profiler?.begin?.("update.physicsWorld.central") || null;
|
||||
try {
|
||||
const frame = prepareWorld(worldRef, opts);
|
||||
const mechanicalStats = global.TarinaiMechanicalSystem?.updateWorld?.(worldRef, dt, { maxPairs: opts.maxPairs || 320, maxLinks: 0, skipLinks: true, maxSubsteps: opts.maxSubsteps || 5, focusThreshold: opts.focusThreshold || 44 }) || { ran: 0, active: 0 };
|
||||
const constraintRan = global.TarinaiConstraintSystem?.updateWorld?.(worldRef, dt, { maxLinks: opts.maxLinks || 220 }) || 0;
|
||||
const postConstraintPairs = constraintRan ? (global.TarinaiMechanicalSystem?.resolveMechanicalPairs?.(worldRef, dt, { maxPairs: opts.postMaxPairs || 110 }) || 0) : 0;
|
||||
const committed = commitWorld(worldRef, frame);
|
||||
const stats = { ran: (mechanicalStats?.ran || 0) + constraintRan, bodies: frame.bodyCount, constraints: frame.constraintCount, mechanical: mechanicalStats, constraintRan, postConstraintPairs, committed, sync: frame.sync || null, schema: BODY_SCHEMA_VERSION };
|
||||
worldRef._physicsWorldStats = stats;
|
||||
return stats;
|
||||
} finally { if (end) end(); }
|
||||
}
|
||||
|
||||
function compactItemExtra(item, tarinaiIndex = new Map(), itemIndex = new Map()) {
|
||||
if (!item || !isPhysicsType(item)) return null;
|
||||
if (isBodyType(item)) {
|
||||
const b = ensureBody(item, item.world || null);
|
||||
return { pf: 2, body: {
|
||||
type: b.type,
|
||||
kind: b.kind,
|
||||
pose: [Math.round(num(b.pose.x)), Math.round(num(b.pose.y)), Math.round(num(b.pose.angle) * 1000)],
|
||||
vel: [Math.round(num(b.velocity.x) * 10), Math.round(num(b.velocity.y) * 10), Math.round(num(b.velocity.angular) * 1000), Math.round(num(b.velocity.linear) * 10)],
|
||||
motor: [b.motor.powered ? 1 : 0, Math.round(num(b.motor.speed) * 10), Math.round(num(b.motor.direction || 1))],
|
||||
rail: b.rail ? [Math.round(num(b.rail.axisAngle) * 1000), Math.round(num(b.rail.travel) * 10), Math.round(num(b.rail.phase) * 1000), Math.round(num(b.rail.anchorX)), Math.round(num(b.rail.anchorY))] : null,
|
||||
shape: [Math.round(num(b.shape.thickness) * 10), cloneSegments(b.shape.segments, defaultSegments(item.type)).map(seg => seg.map(v => Math.round(num(v))))],
|
||||
collision: [b.collision.solid ? 1 : 0, b.collision.hazard ? 1 : 0],
|
||||
hazard: b.hazard ? [b.hazard.kind || "", Math.round(num(b.hazard.damage, 7) * 10)] : null,
|
||||
mass: Math.round(num(b.mass, 1) * 10),
|
||||
inertia: Math.round(num(b.inertia, 1)),
|
||||
} };
|
||||
}
|
||||
const c = ensureConstraint(item, item.world || null);
|
||||
const epToSave = ep => {
|
||||
if (!ep) return null;
|
||||
const kind = ep.kind || "item";
|
||||
const refIdx = kind === "tarinai" ? (tarinaiIndex.get(ep.id) ?? -1) : (itemIndex.get(ep.id) ?? -1);
|
||||
return { kind, refIdx, id: ep.id || "", type: ep.type || "", label: ep.label || "", token: ep.liveToken ?? null, lx: Math.round(num(ep.localX)), ly: Math.round(num(ep.localY)), center: ep.center ? 1 : 0, x: Math.round(num(ep.x)), y: Math.round(num(ep.y)), t: Number.isFinite(Number(ep.attachT)) ? Math.round(num(ep.attachT) * 1000) : null };
|
||||
};
|
||||
return { pf: 2, constraint: { type: c.type, kind: c.kind, endpoints: [epToSave(c.endpoints?.[0]), epToSave(c.endpoints?.[1])], length: Math.round(num(c.length, 80)), mid: [Math.round(num(c.mid?.x)), Math.round(num(c.mid?.y)), Math.round(num(c.mid?.vx) * 10), Math.round(num(c.mid?.vy) * 10)] } };
|
||||
}
|
||||
function restoreBodyExtra(item, extra) {
|
||||
const p = extra?.body;
|
||||
if (!item || !p || !isBodyType(item)) return false;
|
||||
const poseArr = Array.isArray(p.pose) ? p.pose : [];
|
||||
const velArr = Array.isArray(p.vel) ? p.vel : [];
|
||||
const motorArr = Array.isArray(p.motor) ? p.motor : [];
|
||||
const railArr = Array.isArray(p.rail) ? p.rail : null;
|
||||
const shapeArr = Array.isArray(p.shape) ? p.shape : [];
|
||||
const collArr = Array.isArray(p.collision) ? p.collision : [];
|
||||
const hazardArr = Array.isArray(p.hazard) ? p.hazard : null;
|
||||
const body = defaultBody(item);
|
||||
body.kind = p.kind || body.kind;
|
||||
body.pose = { x: num(poseArr[0], item.x), y: num(poseArr[1], item.y), angle: num(poseArr[2]) / 1000 };
|
||||
body.velocity = { x: num(velArr[0]) / 10, y: num(velArr[1]) / 10, angular: num(velArr[2]) / 1000, linear: num(velArr[3]) / 10 };
|
||||
body.motor = { powered: bool(motorArr[0], true), speed: num(motorArr[1]) / 10, direction: Math.sign(num(motorArr[2], 1)) || 1 };
|
||||
body.rail = railArr ? { axisAngle: num(railArr[0]) / 1000, travel: Math.max(24, num(railArr[1], 1500) / 10), phase: clamp(num(railArr[2]) / 1000, -1, 1), anchorX: num(railArr[3], item.x), anchorY: num(railArr[4], item.y) } : null;
|
||||
body.shape = { model: "segments", thickness: Math.max(4, Math.min(34, num(shapeArr[0], item.type === "poison_block" ? 140 : 120) / 10)), segments: cloneSegments(shapeArr[1], defaultSegments(item.type)), version: 0 };
|
||||
body.collision = { solid: collArr[0] !== 0, hazard: collArr[1] !== 0 };
|
||||
body.hazard = hazardArr ? { kind: hazardArr[0] || "poison", damage: Math.max(1, num(hazardArr[1], 70) / 10) } : null;
|
||||
body.mass = Math.max(0.2, num(p.mass, Math.round(num(body.mass, 1) * 10)) / 10);
|
||||
body.inertia = Math.max(1, num(p.inertia, body.inertia));
|
||||
item.physicsBody = normalizeBody(body, item);
|
||||
applyPoseToItem(item);
|
||||
return true;
|
||||
}
|
||||
function restoreConstraintExtra(item, extra, tarinaiList = [], itemList = []) {
|
||||
const p = extra?.constraint;
|
||||
if (!item || !p || !isConstraintType(item)) return false;
|
||||
const epFromSave = ep => {
|
||||
if (!ep) return null;
|
||||
const kind = ep.kind || "item";
|
||||
const refIdx = Number(ep.refIdx);
|
||||
const indexedTarget = Number.isInteger(refIdx) && refIdx >= 0 ? (kind === "tarinai" ? tarinaiList[refIdx] : itemList[refIdx]) : null;
|
||||
return endpointToPlain({ kind, id: indexedTarget?.id || ep.id || "", type: ep.type || indexedTarget?.type || "", label: ep.label || indexedTarget?.name || "", liveToken: ep.token ?? indexedTarget?.liveToken ?? null, _ref: indexedTarget || null, localX: num(ep.lx), localY: num(ep.ly), center: !!ep.center, x: num(ep.x, item.x || 0), y: num(ep.y, item.y || 0), attachT: ep.t == null ? null : clamp(num(ep.t) / 1000, 0, 1), fuzzyResolve: !indexedTarget && !(Number.isInteger(refIdx) && refIdx >= 0) });
|
||||
};
|
||||
const midArr = Array.isArray(p.mid) ? p.mid : [];
|
||||
item.physicsConstraint = normalizeConstraint({ schema: CONSTRAINT_SCHEMA_VERSION, type: item.type, kind: p.kind || (item.type === "rope" ? "flexible-distance" : "rigid-distance"), endpoints: [epFromSave(p.endpoints?.[0]), epFromSave(p.endpoints?.[1])], length: Math.max(24, num(p.length, 80)), mid: { x: num(midArr[0], item.x || 0), y: num(midArr[1], item.y || 0), vx: num(midArr[2]) / 10, vy: num(midArr[3]) / 10 }, sleep: { awakeUntil: 0 }, particles: null }, item);
|
||||
return true;
|
||||
}
|
||||
function applyCompactExtra(item, extra, tarinaiList = [], itemList = []) {
|
||||
if (!item || !extra || typeof extra !== "object" || extra.pf !== 2) return false;
|
||||
if (isBodyType(item)) return restoreBodyExtra(item, extra);
|
||||
if (isConstraintType(item)) return restoreConstraintExtra(item, extra, tarinaiList, itemList);
|
||||
return false;
|
||||
}
|
||||
function invalidateItem(item, reason = "physics-edited") {
|
||||
if (!item) return false;
|
||||
if (isBodyType(item)) {
|
||||
ensureBody(item, item.world || null, { rebuild: false, syncFromLegacy: false });
|
||||
global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
|
||||
markBodyChanged(item, reason);
|
||||
return true;
|
||||
}
|
||||
if (isConstraintType(item)) {
|
||||
ensureConstraint(item, item.world || null, { rebuild: false, syncFromLegacy: false });
|
||||
item._linkLastStamp = "";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function purgeLegacyPhysicsStorage(item) {
|
||||
if (!item || typeof item !== "object") return false;
|
||||
// Physical state is schema-only now; remove any data fields left by older experiments.
|
||||
const bodyKeys = ["rotator" + "Thickness", "rotator" + "Segments", "rotator" + "Powered", "rotator" + "Speed", "rotator" + "AngularVelocity", "poison" + "CollisionEnabled", "poison" + "Damage", "poison" + "Mass", "poison" + "Inertia", "reciprocator" + "Powered", "reciprocator" + "Speed", "reciprocator" + "Travel", "reciprocator" + "Phase", "reciprocator" + "Direction", "reciprocator" + "AxisAngle", "reciprocator" + "Velocity", "reciprocator" + "AnchorX", "reciprocator" + "AnchorY", "_physics" + "AwakeUntil"];
|
||||
const linkKeys = ["link" + "A", "link" + "B", "link" + "Length", "link" + "MidX", "link" + "MidY", "link" + "MidVX", "link" + "MidVY", "_link" + "AwakeUntil", "rope" + "Particles"];
|
||||
for (const key of bodyKeys.concat(linkKeys)) { try { delete item[key]; } catch (_) {} }
|
||||
if (isBodyType(item)) ensureBody(item, item.world || null, { syncFromLegacy: false });
|
||||
if (isConstraintType(item)) ensureConstraint(item, item.world || null, { syncFromLegacy: false });
|
||||
return true;
|
||||
}
|
||||
|
||||
// Schema normalization entry points.
|
||||
const normalizeBodyState = (item, body = item?.physicsBody) => { if (item && body) { item.physicsBody = normalizeBody(body, item); return item.physicsBody; } return null; };
|
||||
const applyBodyState = (item, body = item?.physicsBody, opts = {}) => { if (item && body) { item.physicsBody = normalizeBody(body, item); if (opts.invalidateShape === true) global.TarinaiMechanicalSystem?.invalidateGeometry?.(item); return applyPoseToItem(item); } return false; };
|
||||
const normalizeConstraintState = (item, c = item?.physicsConstraint) => { if (item && c) { item.physicsConstraint = normalizeConstraint(c, item); return item.physicsConstraint; } return null; };
|
||||
const applyConstraintState = (item, c = item?.physicsConstraint) => { if (item && c) { item.physicsConstraint = normalizeConstraint(c, item); return true; } return false; };
|
||||
|
||||
const bodyApi = Object.freeze({
|
||||
BODY_SCHEMA_VERSION, CONSTRAINT_SCHEMA_VERSION, BODY_TYPES, LINK_TYPES,
|
||||
isBodyType, isConstraintType, isPhysicsType,
|
||||
bodySignature, constraintSignature,
|
||||
ensureBody, ensureConstraint,
|
||||
normalizeBodyState, applyBodyState, markBodyChanged, normalizeConstraintState, applyConstraintState,
|
||||
prepareWorld, commitWorld, compactItemExtra, applyCompactExtra, purgeLegacyPhysicsStorage,
|
||||
defaultSegments, cloneSegments, invalidateItem,
|
||||
pose, velocity, motor, rail, shape, collision, hazard, sleep,
|
||||
scalar, setScalar, segments, setSegments, syncPoseFromItem, applyPoseToItem,
|
||||
endpoint, setEndpoint, linkScalar, setLinkScalar, particles, setParticles, endpointToPlain,
|
||||
});
|
||||
global.TarinaiPhysicsBodySystem = bodyApi;
|
||||
global.TarinaiPhysicsWorldSystem = Object.freeze({ updateWorld, prepareWorld, commitWorld, collectPhysicsItems, bodyApi });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
201
js/render.js
201
js/render.js
|
|
@ -1,5 +1,125 @@
|
|||
"use strict";
|
||||
const TARINAI_RENDER_GLOBAL = typeof globalThis !== "undefined" ? globalThis : (typeof window !== "undefined" ? window : {});
|
||||
|
||||
const toolCursorIconCache = new Map();
|
||||
const toolCursorIconImages = new Map();
|
||||
|
||||
function toolCursorIconDefinition(world) {
|
||||
const tool = String(world?.tool || "");
|
||||
if (!tool || tool === "observe" || tool === "undo" || tool === "redo") return null;
|
||||
const def = typeof toolDefinition === "function" ? toolDefinition(tool) : null;
|
||||
if (!def) return null;
|
||||
return { tool, def, type: def.itemType || (def.placeable ? tool : "") };
|
||||
}
|
||||
|
||||
function staticVersionedAssetPath(path) {
|
||||
const raw = String(path || "");
|
||||
if (!raw) return "";
|
||||
const app = TARINAI_RENDER_GLOBAL.TARINAI_APP;
|
||||
if (app?.withVersion) return app.withVersion(raw);
|
||||
return raw;
|
||||
}
|
||||
|
||||
function cachedToolCursorIconImage(path) {
|
||||
const src = staticVersionedAssetPath(path);
|
||||
if (!src) return null;
|
||||
let img = toolCursorIconImages.get(src);
|
||||
if (!img && typeof Image !== "undefined") {
|
||||
img = new Image();
|
||||
img.decoding = "async";
|
||||
img.src = src;
|
||||
toolCursorIconImages.set(src, img);
|
||||
}
|
||||
return img || null;
|
||||
}
|
||||
|
||||
function drawToolCursorIconFallback(ctx, icon, size) {
|
||||
const value = String(icon || "");
|
||||
if (!value) return false;
|
||||
ctx.save();
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = `${Math.round(size * 0.58)}px ui-rounded, sans-serif`;
|
||||
ctx.fillStyle = "rgba(44, 36, 28, 0.78)";
|
||||
ctx.fillText(value, size / 2, size / 2 + size * 0.02);
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildToolCursorIconCanvas(tool, def, type, size) {
|
||||
const key = `${tool}:${type || "-"}:${size}`;
|
||||
const cached = toolCursorIconCache.get(key);
|
||||
if (cached) return cached;
|
||||
const ownerDocument = typeof document !== "undefined" ? document : null;
|
||||
const canvas = ownerDocument?.createElement?.("canvas") || null;
|
||||
if (!canvas) return null;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const c = canvas.getContext("2d");
|
||||
if (!c) return null;
|
||||
c.clearRect(0, 0, size, size);
|
||||
let drawn = false;
|
||||
if (type && typeof TARINAI_RENDER_GLOBAL.drawToolItemPreview === "function") {
|
||||
try { drawn = TARINAI_RENDER_GLOBAL.drawToolItemPreview(c, type, { width: size, height: size, compact: true, watermark: false }) !== false; }
|
||||
catch (_) { drawn = false; }
|
||||
}
|
||||
if (!drawn && def?.icon) {
|
||||
const img = cachedToolCursorIconImage(def.icon);
|
||||
if (img && img.complete && (img.naturalWidth || img.width)) {
|
||||
const pad = Math.max(4, size * 0.12);
|
||||
c.drawImage(img, pad, pad, size - pad * 2, size - pad * 2);
|
||||
drawn = true;
|
||||
} else {
|
||||
// Do not cache a label fallback while the real icon is still loading.
|
||||
// Otherwise tools such as "new" and "delete" can remain as kanji text
|
||||
// until a hard reload even after the image becomes available.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (!drawn) drawn = drawToolCursorIconFallback(c, def?.iconText || def?.label || tool, size);
|
||||
if (!drawn) return null;
|
||||
toolCursorIconCache.set(key, canvas);
|
||||
while (toolCursorIconCache.size > 64) toolCursorIconCache.delete(toolCursorIconCache.keys().next().value);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function drawSelectedToolCursorIcon(ctx, world) {
|
||||
const p = world?.pointer;
|
||||
if (!p?.inside) return;
|
||||
const info = toolCursorIconDefinition(world);
|
||||
if (!info) return;
|
||||
const activeUi = typeof uiCache !== "undefined" ? uiCache : null;
|
||||
if (activeUi?.panning || activeUi?.grabbing || activeUi?.hosing || activeUi?.areaDeleting) return;
|
||||
|
||||
const screen = world.worldToScreen ? world.worldToScreen(p.x, p.y) : { x: p.x, y: p.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(30, Math.min(46, Math.round(Math.min(canvasW, canvasH) * 0.055)));
|
||||
const offset = Math.max(6, Math.round(size * 0.16));
|
||||
let x = screen.x + offset;
|
||||
let y = screen.y + offset;
|
||||
x = clamp(x, 8, canvasW - size - 8);
|
||||
y = clamp(y, 8, canvasH - size - 8);
|
||||
|
||||
let icon = buildToolCursorIconCanvas(info.tool, info.def, info.type, 96);
|
||||
if (!icon && info.def?.icon) {
|
||||
const img = cachedToolCursorIconImage(info.def.icon);
|
||||
if (img && img.complete && (img.naturalWidth || img.width)) icon = img;
|
||||
}
|
||||
if (!icon) {
|
||||
// Icon image may still be loading; request one more frame once it arrives.
|
||||
cachedToolCursorIconImage(info.def?.icon || "");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.52;
|
||||
ctx.shadowColor = "rgba(255,255,255,0.36)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.drawImage(icon, x, y, size, 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);
|
||||
|
|
@ -150,31 +270,49 @@ function placementPreviewFor(world) {
|
|||
let tmp = { type, x, y, r, toolSize: sizeName, foodServingScale: sizeScale, dead: false };
|
||||
if (typeof isRotatableItemType === "function" && isRotatableItemType(type)) tmp.angle = world?.toolAngleFor ? world.toolAngleFor(type) : (typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
|
||||
if (type === "reciprocator") {
|
||||
tmp.reciprocatorAxisAngle = tmp.angle;
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(tmp, world, { syncFromLegacy: false });
|
||||
pb?.setScalar?.(tmp, "railAxis", tmp.angle, "preview-axis");
|
||||
tmp.angle = typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0;
|
||||
pb?.syncPoseFromItem?.(tmp);
|
||||
}
|
||||
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) };
|
||||
}
|
||||
if (type === "rotator" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) {
|
||||
tmp.rotatorSpeed = Math.PI * 0.65;
|
||||
tmp.rotatorPowered = true;
|
||||
tmp.rotatorThickness = 12;
|
||||
tmp.rotatorSegments = [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(tmp, world, { syncFromLegacy: false });
|
||||
pb?.setScalar?.(tmp, "motorSpeed", Math.PI * 0.65, "preview");
|
||||
pb?.setScalar?.(tmp, "motorOn", true, "preview");
|
||||
pb?.setScalar?.(tmp, "thickness", 12, "preview");
|
||||
pb?.setSegments?.(tmp, [[-78, 0, 78, 0], [0, -52, 0, 52]], "preview");
|
||||
const rects = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.obstacleRects(tmp) || [];
|
||||
const bounds = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.boundsAabb(tmp) || null;
|
||||
return { type, x, y, r, rects, blocked: (bounds ? rectOutside(bounds) : circleOutside(x, y, r)) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "poison_block" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) {
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
// Use the actual default poison-block physics footprint for placement
|
||||
// feedback. Previously it fell through to the generic oval preview, which
|
||||
// did not match the placed shape.
|
||||
pb?.ensureBody?.(tmp, null, { syncFromLegacy: false });
|
||||
tmp.world = null;
|
||||
const rects = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.obstacleRects(tmp) || [];
|
||||
const bounds = TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem.boundsAabb(tmp) || null;
|
||||
return { type, x, y, r, rects, blocked: (bounds ? rectOutside(bounds) : circleOutside(x, y, r)) || world.placementBlocked?.(tmp) };
|
||||
}
|
||||
if (type === "reciprocator" && TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem) {
|
||||
tmp.reciprocatorPowered = true;
|
||||
tmp.reciprocatorSpeed = 92;
|
||||
tmp.reciprocatorTravel = 150;
|
||||
tmp.reciprocatorPhase = 0;
|
||||
tmp.reciprocatorAnchorX = x;
|
||||
tmp.reciprocatorAnchorY = y;
|
||||
tmp.rotatorThickness = 12;
|
||||
tmp.rotatorSegments = [[-78, 0, 78, 0]];
|
||||
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureBody?.(tmp, world, { syncFromLegacy: false });
|
||||
pb?.setScalar?.(tmp, "railOn", true, "preview");
|
||||
pb?.setScalar?.(tmp, "railMotorSpeed", 92, "preview");
|
||||
pb?.setScalar?.(tmp, "railTravel", 150, "preview");
|
||||
pb?.setScalar?.(tmp, "railPhase", 0, "preview");
|
||||
pb?.setScalar?.(tmp, "railAnchorX", x, "preview");
|
||||
pb?.setScalar?.(tmp, "railAnchorY", y, "preview");
|
||||
pb?.setScalar?.(tmp, "thickness", 12, "preview");
|
||||
pb?.setSegments?.(tmp, [[-78, 0, 78, 0]], "preview");
|
||||
const rects = world.reciprocatorObstacleRects(tmp) || [];
|
||||
const bounds = rects.length ? { left: Math.min(...rects.map(rr => rr.left)), right: Math.max(...rects.map(rr => rr.right)), top: Math.min(...rects.map(rr => rr.top)), bottom: Math.max(...rects.map(rr => rr.bottom)) } : null;
|
||||
return { type, x, y, r, rects, blocked: (bounds ? rectOutside(bounds) : circleOutside(x, y, r)) || world.placementBlocked?.(tmp) };
|
||||
|
|
@ -603,7 +741,7 @@ function renderRadiusForEntity(entity) {
|
|||
if (!entity) return 40;
|
||||
if (entity.type === "genkotsu") return Math.max(420, (entity.r || 88) * 6.2);
|
||||
if (entity.type === "fence_v" || entity.type === "fence_h" || entity.type === "bounce_fence" || entity.type === "bounce_fence_v" || entity.type === "gate_fence") return Math.max(96, (entity.r || 24) * 4.2);
|
||||
if (entity.type === "reciprocator") return Math.max(140, TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem?.reach?.(entity) || ((entity.r || 64) * 2.8 + (entity.reciprocatorTravel || 150)));
|
||||
if (entity.type === "reciprocator") return Math.max(140, TARINAI_RENDER_GLOBAL.TarinaiMechanicalSystem?.reach?.(entity) || ((entity.r || 64) * 2.8 + (TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem?.scalar?.(entity, "railTravel", 150) || 150)));
|
||||
if (entity.type === "nest_box") return Math.max(86, (entity.r || 34) * 3.2);
|
||||
if (entity.type === "ant_nest") return Math.max(72, (entity.r || 28) * 3.0);
|
||||
if (entity.kind === "queen") return 42;
|
||||
|
|
@ -1175,6 +1313,14 @@ function weatherLabel(weather) {
|
|||
return { sunny: "\u6674\u308c", cloudy: "\u66c7\u308a", light_rain: "\u5c0f\u96e8" }[weather] || "\u6674\u308c";
|
||||
}
|
||||
|
||||
function renderPerfBegin(label) {
|
||||
return window.TarinaiPerf?.begin?.(label) || null;
|
||||
}
|
||||
|
||||
function renderPerfEnd(end) {
|
||||
if (end) end();
|
||||
}
|
||||
|
||||
function fillScreenFallback(ctx, w, h, lighting) {
|
||||
const light = lighting?.light ?? 0.7;
|
||||
ctx.save();
|
||||
|
|
@ -1203,10 +1349,12 @@ function render() {
|
|||
const cameraY = world.cameraY || 0;
|
||||
const viewScale = world.viewScale ? world.viewScale() : 1;
|
||||
|
||||
const endSetup = renderPerfBegin("render.setup");
|
||||
const lighting = getLightingState(world);
|
||||
fillScreenFallback(ctx, screenW, screenH, lighting);
|
||||
const light = lighting.light;
|
||||
const cachedBackground = ensureBackgroundCache(sceneW, sceneH, lighting);
|
||||
renderPerfEnd(endSetup);
|
||||
|
||||
const beginFieldTransform = () => {
|
||||
ctx.save();
|
||||
|
|
@ -1215,6 +1363,7 @@ function render() {
|
|||
ctx.translate(-cameraX, -cameraY);
|
||||
};
|
||||
|
||||
const endBackground = renderPerfBegin("render.background");
|
||||
beginFieldTransform();
|
||||
if (cachedBackground) {
|
||||
ctx.drawImage(cachedBackground, 0, 0, sceneW, sceneH);
|
||||
|
|
@ -1223,28 +1372,39 @@ function render() {
|
|||
drawGardenBed(sceneW, sceneH, lighting);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
drawLightRays(ctx, screenW, screenH, lighting);
|
||||
renderPerfEnd(endBackground);
|
||||
|
||||
const visibleRect = visibleWorldRect(world, 180);
|
||||
|
||||
beginFieldTransform();
|
||||
const endPreviews = renderPerfBegin("render.previews");
|
||||
drawPlacementPreview(ctx, world);
|
||||
drawOperationToolPreview(ctx, world);
|
||||
renderPerfEnd(endPreviews);
|
||||
drawTerrainLayer(ctx, world, lighting, visibleRect);
|
||||
|
||||
// Render only entities that live in visible spatial cells; this keeps large offscreen colonies cheap.
|
||||
const renderStack = collectVisibleRenderStack(world, visibleRect);
|
||||
window.TarinaiPerf?.setMetric?.("render.visibleBackItems", renderStack.backItems.length);
|
||||
window.TarinaiPerf?.setMetric?.("render.visibleLayered", renderStack.layered.length);
|
||||
window.TarinaiPerf?.setMetric?.("render.visibleEffects", (world.effects || []).length);
|
||||
|
||||
const endBackItems = renderPerfBegin("render.draw.backItems");
|
||||
for (const it of renderStack.backItems) {
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
renderPerfEnd(endBackItems);
|
||||
|
||||
// Parent-follow guide lines intentionally disabled; they looked like stray lines between Tarinai.
|
||||
|
||||
const endLayered = renderPerfBegin("render.draw.layered");
|
||||
for (const entry of renderStack.layered) {
|
||||
if (isEntityVisibleInRect(entry.entity, visibleRect)) entry.entity.draw(ctx, world.time, lighting);
|
||||
}
|
||||
renderPerfEnd(endLayered);
|
||||
|
||||
const endAttachments = renderPerfBegin("render.draw.attachments");
|
||||
for (const it of renderStack.carriedPlushies) {
|
||||
syncCarriedPlushieToOwner(it, world);
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
|
|
@ -1252,14 +1412,20 @@ function render() {
|
|||
for (const it of renderStack.lodgedPins) {
|
||||
if (isEntityVisibleInRect(it, visibleRect)) it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
renderPerfEnd(endAttachments);
|
||||
|
||||
const endEffects = renderPerfBegin("render.draw.effects");
|
||||
for (const ef of world.effects) {
|
||||
if (isEntityVisibleInRect(ef, visibleRect)) ef.draw(ctx);
|
||||
}
|
||||
renderPerfEnd(endEffects);
|
||||
ctx.restore();
|
||||
|
||||
// Light affecting the whole visible scene, including characters and ground.
|
||||
const endLighting = renderPerfBegin("render.lightingWeather");
|
||||
drawAtmosphericLighting(ctx, screenW, screenH, lighting);
|
||||
if (world.weather === "light_rain") drawRain(ctx, screenW, screenH, world.time);
|
||||
renderPerfEnd(endLighting);
|
||||
|
||||
const drawAtScreenPosition = (entity, drawFn) => {
|
||||
if (!entity) return;
|
||||
|
|
@ -1275,9 +1441,13 @@ function render() {
|
|||
entity.y = oy;
|
||||
}
|
||||
};
|
||||
const endUiOverlay = renderPerfBegin("render.uiOverlay");
|
||||
drawAtScreenPosition(world.selected, () => drawSelectedCard(ctx, world, lighting));
|
||||
drawPointerItemTooltip(ctx, world);
|
||||
drawSelectedToolCursorIcon(ctx, world);
|
||||
renderPerfEnd(endUiOverlay);
|
||||
|
||||
const endHud = renderPerfBegin("render.hud");
|
||||
// Time HUD
|
||||
ctx.save();
|
||||
const hudW = 150;
|
||||
|
|
@ -1305,6 +1475,7 @@ function render() {
|
|||
ctx.fillStyle = light > 0.42 ? "rgba(84,72,55,0.42)" : "rgba(230,235,255,0.48)";
|
||||
ctx.fillText(`${window.__tarinaiFps || 0} fps`, w - 14, h - 12);
|
||||
ctx.restore();
|
||||
renderPerfEnd(endHud);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
}
|
||||
|
||||
function syncRestoreDependents(worldRef = global.world, context = {}) {
|
||||
global.TarinaiFreezeSystem?.afterWorldRestored?.(worldRef, context);
|
||||
worldRef?.emit?.("ui:restore-sync", { source: context.source || "restore" });
|
||||
renderWorldAfterRestore(worldRef);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@
|
|||
if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
|
||||
try {
|
||||
const parsed = JSON.parse(reader.str() || "[]");
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
return (Array.isArray(parsed) || (parsed && typeof parsed === "object")) ? parsed : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
// Owns stable save identifiers shared by snapshot_system.js and save_codec.js.
|
||||
// Current-format save identifiers only.
|
||||
(function (global) {
|
||||
const SNAPSHOT_VERSION = 7;
|
||||
const BINARY_SCHEMA_VERSION = 10;
|
||||
const SNAPSHOT_VERSION = 11;
|
||||
const BINARY_SCHEMA_VERSION = 14;
|
||||
|
||||
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
|
||||
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi",
|
||||
"sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
|
||||
"stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h",
|
||||
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod"
|
||||
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block"
|
||||
]);
|
||||
|
||||
function typeIds(names) {
|
||||
|
|
@ -32,10 +32,10 @@
|
|||
const SERVING_FOOD_TYPE_IDS = typeIds(["sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "food"]);
|
||||
const PIN_TYPE_IDS = typeIds(["pushpin", "oshibyo"]);
|
||||
const FENCE_ITEM_TYPE_IDS = typeIds(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence"]);
|
||||
const MECHANICAL_ITEM_TYPE_IDS = typeIds(["rotator", "reciprocator"]);
|
||||
const MECHANICAL_ITEM_TYPE_IDS = typeIds(["rotator", "poison_block", "reciprocator"]);
|
||||
const LINK_ITEM_TYPE_IDS = typeIds(["rope", "rod"]);
|
||||
const JSON_EXTRA_ITEM_TYPE_IDS = typeIds(["rotator", "reciprocator", "rope", "rod"]);
|
||||
const ROTATABLE_ITEM_TYPE_IDS = typeIds(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator"]);
|
||||
const JSON_EXTRA_ITEM_TYPE_IDS = typeIds(["rotator", "poison_block", "reciprocator", "rope", "rod"]);
|
||||
const ROTATABLE_ITEM_TYPE_IDS = typeIds(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator"]);
|
||||
|
||||
function enumIndex(list, value, fallback = 0) {
|
||||
const i = list.indexOf(String(value ?? ""));
|
||||
|
|
|
|||
149
js/sim_core.js
149
js/sim_core.js
|
|
@ -64,6 +64,58 @@ function stableUnit(seed, salt = "") {
|
|||
return ((h >>> 0) % 100000) / 100000;
|
||||
}
|
||||
|
||||
function stableKeyPart(value) {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function deterministicFrame(worldRef = null, hz = 60) {
|
||||
const t = Number(worldRef?.time || 0) || 0;
|
||||
return Math.floor(t * Math.max(1, Number(hz) || 60) + 1e-6);
|
||||
}
|
||||
|
||||
function deterministicUnit(worldRef = null, salt = "", ...parts) {
|
||||
const seed = String(worldRef?.worldSeed || "tarinai-world");
|
||||
const frame = deterministicFrame(worldRef, 60);
|
||||
const serial = Number(worldRef?._deterministicEpoch || 0) || 0;
|
||||
const key = [salt, frame, serial, ...parts.map(stableKeyPart)].join("|");
|
||||
return stableUnit(seed, key);
|
||||
}
|
||||
|
||||
function deterministicRange(worldRef = null, salt = "", min = 0, max = 1, ...parts) {
|
||||
const a = Number(min) || 0;
|
||||
const b = Number(max) || 0;
|
||||
return a + deterministicUnit(worldRef, salt, ...parts) * (b - a);
|
||||
}
|
||||
|
||||
function deterministicSigned(worldRef = null, salt = "", ...parts) {
|
||||
return deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1;
|
||||
}
|
||||
|
||||
function deterministicChance(worldRef = null, salt = "", chance = 0, ...parts) {
|
||||
const p = clamp(Number(chance) || 0, 0, 1);
|
||||
if (p <= 0) return false;
|
||||
if (p >= 1) return true;
|
||||
return deterministicUnit(worldRef, salt, ...parts) < p;
|
||||
}
|
||||
|
||||
function deterministicAngle(worldRef = null, salt = "", ...parts) {
|
||||
return deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.TarinaiDeterminism = { stableKeyPart, deterministicFrame, deterministicUnit, deterministicRange, deterministicSigned, deterministicChance, deterministicAngle };
|
||||
}
|
||||
|
||||
const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"];
|
||||
const GENETIC_LIMITS = Object.freeze({
|
||||
lifeSpanMul: [0.82, 1.24],
|
||||
|
|
@ -653,10 +705,79 @@ class SpatialGrid {
|
|||
bucket.push(entity);
|
||||
}
|
||||
|
||||
addAabb(map, entity, bounds) {
|
||||
if (!entity || !bounds) return this.add(map, entity);
|
||||
const left = Number(bounds.left);
|
||||
const right = Number(bounds.right);
|
||||
const top = Number(bounds.top);
|
||||
const bottom = Number(bounds.bottom);
|
||||
if (!Number.isFinite(left) || !Number.isFinite(right) || !Number.isFinite(top) || !Number.isFinite(bottom)) return this.add(map, entity);
|
||||
const minX = Math.floor(Math.min(left, right) / this.cellSize);
|
||||
const maxX = Math.floor(Math.max(left, right) / this.cellSize);
|
||||
const minY = Math.floor(Math.min(top, bottom) / this.cellSize);
|
||||
const maxY = Math.floor(Math.max(top, bottom) / this.cellSize);
|
||||
const cellCount = Math.max(1, (maxX - minX + 1) * (maxY - minY + 1));
|
||||
if (cellCount > 144) return this.add(map, entity);
|
||||
for (let cy = minY; cy <= maxY; cy += 1) {
|
||||
for (let cx = minX; cx <= maxX; cx += 1) {
|
||||
const key = cx + cy * 100000;
|
||||
let bucket = map.get(key);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
map.set(key, bucket);
|
||||
}
|
||||
bucket.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boundsForTrait(it, trait) {
|
||||
const mech = (typeof window !== "undefined" ? window : globalThis).TarinaiMechanicalSystem;
|
||||
if (mech?.isMechanicalType?.(it?.type)) {
|
||||
if (trait === "hazard" && it.type === "poison_block") return mech.hazardBoundsAabb?.(it) || mech.boundsAabb?.(it);
|
||||
return mech.boundsAabb?.(it);
|
||||
}
|
||||
const ropePath = it?.type === "rope" ? ((typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem?.particles?.(it) || null) : null;
|
||||
if (ropePath && ropePath.length) {
|
||||
let left = Infinity, right = -Infinity, top = Infinity, bottom = -Infinity;
|
||||
for (const p of ropePath) {
|
||||
left = Math.min(left, (Number(p.x) || 0) - 8);
|
||||
right = Math.max(right, (Number(p.x) || 0) + 8);
|
||||
top = Math.min(top, (Number(p.y) || 0) - 8);
|
||||
bottom = Math.max(bottom, (Number(p.y) || 0) + 8);
|
||||
}
|
||||
if (Number.isFinite(left)) return { left, right, top, bottom, item: it, type: it.type };
|
||||
}
|
||||
const r = Math.max(Number(it?.r || it?.radius || 0) || 0, 12);
|
||||
if (r > this.cellSize * 0.72) return { left: (it.x || 0) - r, right: (it.x || 0) + r, top: (it.y || 0) - r, bottom: (it.y || 0) + r, item: it, type: it?.type };
|
||||
return null;
|
||||
}
|
||||
|
||||
addTrait(map, it, trait) {
|
||||
const bounds = this.boundsForTrait(it, trait);
|
||||
if (bounds) this.addAabb(map, it, bounds);
|
||||
else this.add(map, it);
|
||||
}
|
||||
|
||||
isDynamicItem(it) {
|
||||
if (!it || it.dead) return false;
|
||||
const type = it.type || "";
|
||||
if (type === "ball" || type === "genkotsu" || type === "firecracker" || type === "pushpin" || type === "oshibyo" || (type === "rotator" && Math.abs(Number(it.rotatorSpeed || 0)) > 0.001)) return true;
|
||||
if (type === "ball" || type === "genkotsu" || type === "firecracker" || type === "pushpin" || type === "oshibyo") return true;
|
||||
if (type === "rope" || type === "rod") return true;
|
||||
const globalRef = (typeof window !== "undefined" ? window : globalThis);
|
||||
const physics = globalRef.TarinaiPhysicsBodySystem;
|
||||
const body = physics?.isBodyType?.(it) ? physics.ensureBody?.(it, null, { syncFromLegacy: false }) : null;
|
||||
if (type === "rotator") {
|
||||
const motor = body?.motor || {};
|
||||
const velocity = body?.velocity || {};
|
||||
return Math.abs(Number(motor.speed || 0)) > 0.001 || Math.abs(Number(velocity.angular || 0)) > 0.001 || motor.powered !== false;
|
||||
}
|
||||
if (type === "reciprocator") {
|
||||
const motor = body?.motor || {};
|
||||
const velocity = body?.velocity || {};
|
||||
return motor.powered !== false || Math.abs(Number(velocity.linear || 0)) > 0.05;
|
||||
}
|
||||
if (type === "poison_block") return Boolean(globalRef.TarinaiMechanicalSystem?.passiveItemAwake?.(it));
|
||||
if ((it.dropTimer || 0) > 0) return true;
|
||||
if (Math.hypot(it.vx || 0, it.vy || 0) > 0.05) return true;
|
||||
if (it.isStructure && it.type === "plushie" && it.carriedById) return true;
|
||||
|
|
@ -668,9 +789,19 @@ class SpatialGrid {
|
|||
const obstacleMap = dynamic ? this.dynamicObstacleCells : this.staticObstacleCells;
|
||||
const foodMap = dynamic ? this.dynamicFoodCells : this.staticFoodCells;
|
||||
const hazardMap = dynamic ? this.dynamicHazardCells : this.staticHazardCells;
|
||||
if (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")) this.add(obstacleMap, it);
|
||||
if (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)))) this.add(foodMap, it);
|
||||
if (typeof itemHasTrait === "function" && itemHasTrait(type, "hazard")) this.add(hazardMap, it);
|
||||
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 hasHazard = typeof itemHasTrait === "function" && itemHasTrait(type, "hazard");
|
||||
if (type === "poison_block") {
|
||||
const body = (typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem?.ensureBody?.(it, null, { syncFromLegacy: false });
|
||||
const solid = body?.collision ? body.collision.solid !== false : true;
|
||||
if (solid && hasObstacle) this.addTrait(obstacleMap, it, "obstacle");
|
||||
if (hasHazard) this.addTrait(hazardMap, it, "hazard");
|
||||
return;
|
||||
}
|
||||
if (hasObstacle) this.addTrait(obstacleMap, it, "obstacle");
|
||||
if (hasFood) this.addTrait(foodMap, it, "food_interest");
|
||||
if (hasHazard) this.addTrait(hazardMap, it, "hazard");
|
||||
}
|
||||
|
||||
classifyItem(it) {
|
||||
|
|
@ -763,6 +894,7 @@ class SpatialGrid {
|
|||
|
||||
nearbyInto(map, x, y, radius, out = [], filterDistance = false) {
|
||||
const r = Number.isFinite(radius) ? radius : Math.max(1200, this.cellSize * 12);
|
||||
const stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
||||
const minX = Math.floor((x - r) / this.cellSize);
|
||||
const maxX = Math.floor((x + r) / this.cellSize);
|
||||
const minY = Math.floor((y - r) / this.cellSize);
|
||||
|
|
@ -772,6 +904,8 @@ class SpatialGrid {
|
|||
const bucket = map.get(cx + cy * 100000);
|
||||
if (!bucket) continue;
|
||||
for (const entity of bucket) {
|
||||
if (!entity || entity._spatialQueryStamp === stamp) continue;
|
||||
entity._spatialQueryStamp = stamp;
|
||||
if (filterDistance) {
|
||||
const dx = (entity.x || 0) - x;
|
||||
const dy = (entity.y || 0) - y;
|
||||
|
|
@ -794,6 +928,7 @@ class SpatialGrid {
|
|||
|
||||
nearbyRectInto(map, rect, out = []) {
|
||||
if (!rect) return out;
|
||||
const stamp = (this._spatialQueryStamp = (this._spatialQueryStamp || 0) + 1);
|
||||
const minX = Math.floor((rect.left || 0) / this.cellSize);
|
||||
const maxX = Math.floor((rect.right || 0) / this.cellSize);
|
||||
const minY = Math.floor((rect.top || 0) / this.cellSize);
|
||||
|
|
@ -802,7 +937,11 @@ class SpatialGrid {
|
|||
for (let cx = minX; cx <= maxX; cx++) {
|
||||
const bucket = map.get(cx + cy * 100000);
|
||||
if (!bucket) continue;
|
||||
for (const entity of bucket) out.push(entity);
|
||||
for (const entity of bucket) {
|
||||
if (!entity || entity._spatialQueryStamp === stamp) continue;
|
||||
entity._spatialQueryStamp = stamp;
|
||||
out.push(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
|
|
|||
|
|
@ -7,36 +7,99 @@
|
|||
const helpers = () => global.TarinaiSimulationRuntime?.helpers || {};
|
||||
const updatePolicy = () => global.TarinaiCreatureUpdatePolicy || {};
|
||||
|
||||
function inc(map, key, n = 1) {
|
||||
const k = String(key || "none");
|
||||
map[k] = (map[k] || 0) + n;
|
||||
}
|
||||
|
||||
function updateCreatures(worldRef, dt) {
|
||||
const h = helpers();
|
||||
const end = global.TarinaiPerf?.begin?.("update.tarinai") || null;
|
||||
const end = global.TarinaiPerf?.begin?.("update.tarinai.individual") || null;
|
||||
const visibleRect = h.updateVisibleWorldRect?.(worldRef, 160) || { left: -Infinity, top: -Infinity, right: Infinity, bottom: Infinity };
|
||||
const now = worldRef.time || 0;
|
||||
const stats = {
|
||||
total: 0,
|
||||
full: 0,
|
||||
realtime: 0,
|
||||
smooth: 0,
|
||||
skipped: 0,
|
||||
selected: 0,
|
||||
urgent: 0,
|
||||
visible: 0,
|
||||
near: 0,
|
||||
far: 0,
|
||||
lanes: {},
|
||||
states: {},
|
||||
smoothStates: {},
|
||||
skippedStates: {},
|
||||
sleepPhysical: 0,
|
||||
passivePhysical: 0,
|
||||
};
|
||||
const prevSpatialDeferMode = worldRef.deferSpatialRebuildMode || "";
|
||||
worldRef.deferSpatialRebuildDepth = (worldRef.deferSpatialRebuildDepth || 0) + 1;
|
||||
// Smooth-motion Tarinai pass may read obstacle/item grids after Tarinai and
|
||||
// constraint movement have dirtied mobile buckets. Use the last coherent
|
||||
// grid during this phase and collapse mobile invalidations into one rebuild
|
||||
// after all visible bodies have been advanced.
|
||||
worldRef.deferSpatialRebuildMode = "mobile";
|
||||
try {
|
||||
for (const t of worldRef.tarinai) {
|
||||
if (!t || t.dead) continue;
|
||||
const interval = updatePolicy().updateInterval?.(worldRef, t, visibleRect) ?? 0;
|
||||
stats.total += 1;
|
||||
const behaviorId = updatePolicy().behaviorId?.(t) || t.state || "none";
|
||||
inc(stats.states, behaviorId);
|
||||
const cadence = updatePolicy().updateCadence?.(worldRef, t, visibleRect) || { interval: 0, lane: "legacy", smoothMotion: false, visible: true, urgent: false };
|
||||
const interval = Number.isFinite(cadence.interval) ? cadence.interval : Infinity;
|
||||
inc(stats.lanes, cadence.lane || "unknown");
|
||||
if (worldRef.selected === t) stats.selected += 1;
|
||||
if (cadence.urgent) stats.urgent += 1;
|
||||
if (cadence.sleepPhysical) stats.sleepPhysical += 1;
|
||||
if (cadence.passivePhysicsNeed) stats.passivePhysical += 1;
|
||||
if (cadence.visible) stats.visible += 1;
|
||||
else if (interval < 0.75) stats.near += 1;
|
||||
else stats.far += 1;
|
||||
if (!Number.isFinite(interval)) continue;
|
||||
|
||||
const context = { cadence, motionDt: dt };
|
||||
if (interval <= 0) {
|
||||
const runDt = Math.min(1.4, (t._updateAccum || 0) + dt);
|
||||
t._updateAccum = 0;
|
||||
if (global.TarinaiCreatureRuntime?.updateOne) global.TarinaiCreatureRuntime.updateOne(t, runDt);
|
||||
stats.full += 1;
|
||||
stats.realtime += 1;
|
||||
if (global.TarinaiCreatureRuntime?.updateOne) global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
|
||||
else t.update(runDt);
|
||||
continue;
|
||||
}
|
||||
|
||||
t._updateAccum = Math.min(2.4, (t._updateAccum || 0) + dt);
|
||||
t._nextLowFreqUpdateAt = Number.isFinite(t._nextLowFreqUpdateAt) ? t._nextLowFreqUpdateAt : (worldRef.time || 0) + interval * (0.65 + Math.random() * 0.7);
|
||||
if ((worldRef.time || 0) >= t._nextLowFreqUpdateAt) {
|
||||
if (!Number.isFinite(t._nextLowFreqUpdateAt)) {
|
||||
const jitter = typeof stableUnit === "function" ? stableUnit(t.id || t.familyKey || Math.random(), "creature-cadence") : Math.random();
|
||||
t._nextLowFreqUpdateAt = now + interval * (0.65 + jitter * 0.70);
|
||||
}
|
||||
if (now >= t._nextLowFreqUpdateAt) {
|
||||
const runDt = Math.max(dt, t._updateAccum || dt);
|
||||
const jitter = typeof stableUnit === "function" ? stableUnit(t.id || t.familyKey || Math.random(), `creature-cadence:${Math.floor(now * 2)}`) : Math.random();
|
||||
t._updateAccum = 0;
|
||||
t._nextLowFreqUpdateAt = (worldRef.time || 0) + interval;
|
||||
if (global.TarinaiCreatureRuntime?.updateOne) global.TarinaiCreatureRuntime.updateOne(t, runDt);
|
||||
t._nextLowFreqUpdateAt = now + interval * (0.92 + jitter * 0.22);
|
||||
stats.full += 1;
|
||||
if (global.TarinaiCreatureRuntime?.updateOne) global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
|
||||
else t.update(runDt);
|
||||
} else if (cadence.smoothMotion && global.TarinaiCreatureRuntime?.updateMotionOnly) {
|
||||
if (global.TarinaiCreatureRuntime.updateMotionOnly(t, dt, { context })) {
|
||||
stats.smooth += 1;
|
||||
inc(stats.smoothStates, behaviorId);
|
||||
}
|
||||
} else {
|
||||
stats.skipped += 1;
|
||||
inc(stats.skippedStates, behaviorId);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
worldRef.deferSpatialRebuildDepth = Math.max(0, (worldRef.deferSpatialRebuildDepth || 1) - 1);
|
||||
worldRef.deferSpatialRebuildMode = prevSpatialDeferMode;
|
||||
worldRef._creatureUpdateStats = stats;
|
||||
if (end) end();
|
||||
}
|
||||
const now = worldRef.time || 0;
|
||||
const bedConflictInterval = 3.2;
|
||||
if (now >= (worldRef.nextBedConflictCheckAt || 0)) {
|
||||
worldRef.nextBedConflictCheckAt = now + bedConflictInterval;
|
||||
|
|
@ -45,15 +108,29 @@
|
|||
} else {
|
||||
worldRef.workStats && (worldRef.workStats.bedConflictSkips = (worldRef.workStats.bedConflictSkips || 0) + 1);
|
||||
}
|
||||
h.markSpatialDirtyIfMoved?.(worldRef, worldRef.tarinai, "tarinai-moved", 0.25);
|
||||
worldRef.ensureSpatial?.("post-tarinai-update");
|
||||
const endSpatial = global.TarinaiPerf?.begin?.("update.tarinai.spatial") || null;
|
||||
const tarinaiMoved = h.markSpatialDirtyIfMoved?.(worldRef, worldRef.tarinai, "tarinai-moved", 0.25) || false;
|
||||
if (tarinaiMoved || worldRef.spatialDirty) {
|
||||
if (worldRef.rebuildSpatial) worldRef.rebuildSpatial(true, worldRef.deferredSpatialDirtyReason || "post-tarinai-update");
|
||||
else worldRef.ensureSpatial?.("post-tarinai-update");
|
||||
worldRef.deferredSpatialDirtyReason = "";
|
||||
}
|
||||
if (endSpatial) endSpatial();
|
||||
const endBallInteractions = global.TarinaiPerf?.begin?.("update.ballInteractions") || null;
|
||||
worldRef.limitBallChasers();
|
||||
worldRef.resolveBallInteractions(dt);
|
||||
const collisionInterval = 1.8;
|
||||
if (now >= (worldRef.nextTarinaiCollisionCheckAt || 0)) {
|
||||
if (endBallInteractions) endBallInteractions();
|
||||
const hasFastTarinai = (worldRef.tarinai || []).some(t => {
|
||||
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) return false;
|
||||
return Math.hypot(t.vx || 0, t.vy || 0) >= 150;
|
||||
});
|
||||
const collisionInterval = hasFastTarinai ? 0 : 0.55;
|
||||
if (hasFastTarinai || now >= (worldRef.nextTarinaiCollisionCheckAt || 0)) {
|
||||
worldRef.nextTarinaiCollisionCheckAt = now + collisionInterval;
|
||||
worldRef.workStats && (worldRef.workStats.collisionRuns = (worldRef.workStats.collisionRuns || 0) + 1);
|
||||
worldRef.resolveTarinaiHighSpeedCollisions(dt);
|
||||
const endCollision = global.TarinaiPerf?.begin?.("update.tarinai.collisions") || null;
|
||||
try { worldRef.resolveTarinaiHighSpeedCollisions(dt); }
|
||||
finally { if (endCollision) endCollision(); }
|
||||
} else {
|
||||
worldRef.workStats && (worldRef.workStats.collisionSkips = (worldRef.workStats.collisionSkips || 0) + 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,17 +42,34 @@
|
|||
function updateItemsAndAnts(worldRef, dt) {
|
||||
const h = helpers();
|
||||
const itemCountBefore = worldRef.items.length;
|
||||
h.updateItemsScheduled?.(worldRef, dt);
|
||||
updateRandomGrassGrowth(worldRef, dt);
|
||||
const itemsMoved = h.markScheduledItemsMoved?.(worldRef, "items-moved", 0.25) || false;
|
||||
if (worldRef.itemCounts?.ball) {
|
||||
worldRef.ensureSpatial?.("ball-ball-collisions");
|
||||
worldRef.resolveBallBallCollisions(dt);
|
||||
worldRef.markSpatialDirty?.("ball-ball-collisions");
|
||||
const prevMode = worldRef.deferSpatialRebuildMode || "";
|
||||
worldRef.deferSpatialRebuildDepth = (worldRef.deferSpatialRebuildDepth || 0) + 1;
|
||||
worldRef.deferSpatialRebuildMode = "mobile";
|
||||
let itemsMoved = false;
|
||||
let antsMoved = false;
|
||||
try {
|
||||
h.updateItemsScheduled?.(worldRef, dt);
|
||||
updateRandomGrassGrowth(worldRef, dt);
|
||||
itemsMoved = h.markScheduledItemsMoved?.(worldRef, "items-moved", 0.25) || false;
|
||||
if (worldRef.itemCounts?.ball) {
|
||||
worldRef.ensureSpatial?.("ball-ball-collisions");
|
||||
worldRef.resolveBallBallCollisions(dt);
|
||||
worldRef.markSpatialDirty?.("ball-ball-collisions");
|
||||
}
|
||||
worldRef.updateAnts?.(dt);
|
||||
antsMoved = h.markSpatialDirtyIfMoved?.(worldRef, worldRef.ants || [], "ants-moved", 0.25) || false;
|
||||
} finally {
|
||||
worldRef.deferSpatialRebuildDepth = Math.max(0, (worldRef.deferSpatialRebuildDepth || 1) - 1);
|
||||
worldRef.deferSpatialRebuildMode = prevMode;
|
||||
}
|
||||
if (itemsMoved || antsMoved || worldRef.spatialDirty) {
|
||||
// Mobile items/ants are obstacle and contact sources for the next phase.
|
||||
// Force one fresh grid after the mobile phase; reads inside the phase are
|
||||
// allowed to use the frame-start grid to avoid rebuild ping-pong.
|
||||
if (worldRef.rebuildSpatial) worldRef.rebuildSpatial(true, worldRef.deferredSpatialDirtyReason || "post-mobile-item-update");
|
||||
else worldRef.ensureSpatial?.("post-mobile-item-update");
|
||||
worldRef.deferredSpatialDirtyReason = "";
|
||||
}
|
||||
worldRef.updateAnts?.(dt);
|
||||
const antsMoved = h.markSpatialDirtyIfMoved?.(worldRef, worldRef.ants || [], "ants-moved", 0.25) || false;
|
||||
if (itemsMoved || antsMoved || worldRef.spatialDirty) worldRef.ensureSpatial?.("post-mobile-item-update");
|
||||
return { itemCountBefore, itemsMoved, antsMoved };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -322,50 +322,11 @@
|
|||
if (type === "grass") return [q(item.growth, 100), q(item.health, 100), q(item.fertilityBoost, 100), item.manualGrass || item.placedByPlayer ? 1 : 0];
|
||||
if (type === "zunchi") return [enumIndex(STAGE_IDS, item.stage || "fresh"), q(item.freshness, 100), q(item.fertility, 100)];
|
||||
if (type === "signboard") return [item.text || ""];
|
||||
if (type === "rotator") return [
|
||||
q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)),
|
||||
q(item.rotatorSpeed, 1000),
|
||||
q(item.rotatorThickness || 12, 10),
|
||||
(Array.isArray(item.rotatorSegments) ? item.rotatorSegments : [[-78,0,78,0],[0,-52,0,52]]).map(seg => [q(seg[0], 1), q(seg[1], 1), q(seg[2], 1), q(seg[3], 1)]),
|
||||
item.rotatorPowered === false ? 0 : 1,
|
||||
q(item.rotatorAngularVelocity || 0, 1000)
|
||||
];
|
||||
if (type === "reciprocator") return [
|
||||
q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)),
|
||||
q(Number.isFinite(Number(item.reciprocatorAxisAngle)) ? item.reciprocatorAxisAngle : item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)),
|
||||
item.reciprocatorPowered === false ? 0 : 1,
|
||||
q(item.reciprocatorSpeed || 92, 10),
|
||||
q(item.reciprocatorTravel || 150, 10),
|
||||
q(item.reciprocatorPhase || 0, 1000),
|
||||
q(item.reciprocatorVelocity || 0, 10),
|
||||
q(item.reciprocatorAnchorX || item.x || 0, 1),
|
||||
q(item.reciprocatorAnchorY || item.y || 0, 1),
|
||||
q(item.reciprocatorDirection || 1, 1),
|
||||
q(item.rotatorThickness || 12, 10),
|
||||
(Array.isArray(item.rotatorSegments) ? item.rotatorSegments : [[-78,0,78,0]]).map(seg => [q(seg[0], 1), q(seg[1], 1), q(seg[2], 1), q(seg[3], 1)])
|
||||
];
|
||||
|
||||
if (type === "rope" || type === "rod") {
|
||||
const cloneEndpoint = ep => {
|
||||
if (!ep) return null;
|
||||
const kind = ep.kind || "item";
|
||||
const refIdx = kind === "tarinai" ? (tarinaiIndex.get(ep.id) ?? -1) : (itemIndex.get(ep.id) ?? -1);
|
||||
return {
|
||||
kind,
|
||||
id: ep.id || "",
|
||||
refIdx,
|
||||
type: ep.type || "",
|
||||
label: ep.label || "",
|
||||
localX: q(ep.localX || 0, 1),
|
||||
localY: q(ep.localY || 0, 1),
|
||||
center: ep.center ? 1 : 0,
|
||||
x: q(ep.x || 0, 1),
|
||||
y: q(ep.y || 0, 1),
|
||||
attachT: Number.isFinite(Number(ep.attachT)) ? q(ep.attachT, 1000) : null,
|
||||
};
|
||||
};
|
||||
return [cloneEndpoint(item.linkA), cloneEndpoint(item.linkB), q(item.linkLength || item.r * 2 || 80, 1), q(item.linkMidX || item.x || 0, 1), q(item.linkMidY || item.y || 0, 1), q(item.linkMidVX || 0, 10), q(item.linkMidVY || 0, 10)];
|
||||
if (global.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) {
|
||||
const packed = global.TarinaiPhysicsBodySystem.compactItemExtra?.(item, tarinaiIndex, itemIndex);
|
||||
if (packed) return packed;
|
||||
}
|
||||
if (global.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) return { pf: 2, missing: true };
|
||||
if (type === "duplicator") return [enumIndex(ITEM_TYPE_IDS, item.storedFoodType || "", -1)];
|
||||
if (type === "ball") return [q(item.vx, 10), q(item.vy, 10)];
|
||||
if (type === "ant_nest") return [q(item.antCount, 1), q(item.queenSpawnAt, 10)];
|
||||
|
|
@ -411,67 +372,13 @@
|
|||
item.stage = enumValue(STAGE_IDS, extra[0], "fresh"); item.freshness = u(extra[1], 100, item.freshness || 1); item.fertility = u(extra[2], 100, item.fertility || 1);
|
||||
} else if (type === "signboard") {
|
||||
item.text = String(extra[0] || "");
|
||||
} else if (type === "rotator") {
|
||||
const e = Array.isArray(extra) ? extra : [];
|
||||
const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
|
||||
item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(e[0], 1000, fallback), fallback) : u(e[0], 1000, fallback);
|
||||
item.rotatorSpeed = u(e[1], 1000, item.rotatorSpeed || 0);
|
||||
item.rotatorThickness = Math.max(4, Math.min(34, u(e[2], 10, item.rotatorThickness || 12)));
|
||||
item.rotatorSegments = (Array.isArray(e[3]) ? e[3] : [[-78,0,78,0],[0,-52,0,52]]).map(seg => [u(seg[0],1), u(seg[1],1), u(seg[2],1), u(seg[3],1)]).filter(seg => Math.hypot(seg[2]-seg[0], seg[3]-seg[1]) >= 4);
|
||||
item.rotatorPowered = e.length >= 5 ? !!e[4] : item.rotatorPowered !== false;
|
||||
item.rotatorAngularVelocity = u(e[5], 1000, item.rotatorAngularVelocity || 0);
|
||||
if (!item.rotatorSegments.length) item.rotatorSegments = [[-78,0,78,0]];
|
||||
const extent = Math.max(...item.rotatorSegments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + item.rotatorThickness + 8;
|
||||
item.r = Math.max(item.r || 64, Math.min(460, extent));
|
||||
} else if (type === "reciprocator") {
|
||||
const e = Array.isArray(extra) ? extra : [];
|
||||
const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
|
||||
item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(e[0], 1000, fallback), fallback) : u(e[0], 1000, fallback);
|
||||
item.reciprocatorAxisAngle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(e[1], 1000, item.angle), item.angle) : u(e[1], 1000, item.angle);
|
||||
item.reciprocatorPowered = e.length >= 3 ? !!e[2] : item.reciprocatorPowered !== false;
|
||||
item.reciprocatorSpeed = Math.max(0, u(e[3], 10, item.reciprocatorSpeed || 92));
|
||||
item.reciprocatorTravel = Math.max(24, u(e[4], 10, item.reciprocatorTravel || 150));
|
||||
item.reciprocatorPhase = clamp(u(e[5], 1000, item.reciprocatorPhase || 0), -1, 1);
|
||||
item.reciprocatorVelocity = u(e[6], 10, item.reciprocatorVelocity || 0);
|
||||
item.reciprocatorAnchorX = u(e[7], 1, item.reciprocatorAnchorX || item.x || 0);
|
||||
item.reciprocatorAnchorY = u(e[8], 1, item.reciprocatorAnchorY || item.y || 0);
|
||||
item.reciprocatorDirection = u(e[9], 1, item.reciprocatorDirection || 1) || 1;
|
||||
item.rotatorThickness = Math.max(4, Math.min(34, u(e[10], 10, item.rotatorThickness || 12)));
|
||||
item.rotatorSegments = (Array.isArray(e[11]) ? e[11] : (Array.isArray(item.rotatorSegments) ? item.rotatorSegments : [[-78,0,78,0]])).map(seg => [u(seg[0],1), u(seg[1],1), u(seg[2],1), u(seg[3],1)]).filter(seg => Math.hypot(seg[2]-seg[0], seg[3]-seg[1]) >= 4);
|
||||
if (!item.rotatorSegments.length) item.rotatorSegments = [[-78,0,78,0]];
|
||||
const extent = Math.max(...item.rotatorSegments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + item.rotatorThickness + 8;
|
||||
item.r = Math.max(item.r || 64, Math.min(460, extent));
|
||||
} else if (type === "rope" || type === "rod") {
|
||||
const e = Array.isArray(extra) ? extra : [];
|
||||
const normalizeEndpoint = ep => {
|
||||
if (!ep) return null;
|
||||
const kind = ep.kind || "item";
|
||||
const refIdx = Number(ep.refIdx);
|
||||
const indexedTarget = Number.isInteger(refIdx) && refIdx >= 0
|
||||
? (kind === "tarinai" ? tarinaiList[refIdx] : itemList[refIdx])
|
||||
: null;
|
||||
return {
|
||||
kind,
|
||||
id: indexedTarget?.id || ep.id || "",
|
||||
type: ep.type || indexedTarget?.type || "",
|
||||
label: ep.label || indexedTarget?.name || "",
|
||||
localX: u(ep.localX, 1, 0),
|
||||
localY: u(ep.localY, 1, 0),
|
||||
center: !!ep.center,
|
||||
x: u(ep.x, 1, item.x || 0),
|
||||
y: u(ep.y, 1, item.y || 0),
|
||||
attachT: ep.attachT == null ? null : clamp(u(ep.attachT, 1000, 0.5), 0, 1),
|
||||
fuzzyResolve: !indexedTarget && !Number.isInteger(refIdx),
|
||||
};
|
||||
};
|
||||
item.linkA = normalizeEndpoint(e[0]);
|
||||
item.linkB = normalizeEndpoint(e[1]);
|
||||
item.linkLength = Math.max(24, u(e[2], 1, item.linkLength || 80));
|
||||
item.linkMidX = u(e[3], 1, item.linkMidX || item.x || 0);
|
||||
item.linkMidY = u(e[4], 1, item.linkMidY || item.y || 0);
|
||||
item.linkMidVX = u(e[5], 10, item.linkMidVX || 0);
|
||||
item.linkMidVY = u(e[6], 10, item.linkMidVY || 0);
|
||||
item.r = Math.max(18, item.linkLength * 0.5);
|
||||
} else if (global.TarinaiPhysicsBodySystem?.applyCompactExtra?.(item, extra, tarinaiList, itemList)) {
|
||||
return;
|
||||
} else if (global.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) {
|
||||
// Physics items only accept the normalized pf:2 payload from this schema.
|
||||
// Old array payloads are intentionally unsupported.
|
||||
global.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "physics-extra-missing");
|
||||
return;
|
||||
} else if (type === "duplicator") {
|
||||
item.storedFoodType = enumValue(ITEM_TYPE_IDS, extra[0], ""); item.storedFoodLabel = global.foodLabel ? global.foodLabel(item.storedFoodType) : item.storedFoodType; if (item.roles) item.roles.food = Boolean(item.storedFoodType);
|
||||
} else if (type === "ball") {
|
||||
|
|
@ -564,7 +471,7 @@
|
|||
const row = itemRows[idx] || [];
|
||||
const type = enumValue(ITEM_TYPE_IDS, row[0], "");
|
||||
if (!type) continue;
|
||||
const extra = Array.isArray(row[4]) ? row[4] : [];
|
||||
const extra = (Array.isArray(row[4]) || (row[4] && typeof row[4] === "object")) ? row[4] : [];
|
||||
const owner = global.StructureRegistry?.get?.(type) ? worldRef.tarinai[extra[2]] || null : null;
|
||||
const item = global.StructureRegistry?.get?.(type)
|
||||
? global.StructureRegistry.create(type, owner, u(row[1], 1), u(row[2], 1), worldRef)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,15 @@
|
|||
const frameDt = normalize(worldRef, dt);
|
||||
if (frameDt === null) return;
|
||||
const ctx = { dt: frameDt, mobile: null };
|
||||
for (const system of SYSTEM_ORDER) system.run(worldRef, ctx);
|
||||
const profiler = global.TarinaiPerf;
|
||||
for (const system of SYSTEM_ORDER) {
|
||||
const end = profiler?.begin?.(`update.phase.${system.id}`) || null;
|
||||
try {
|
||||
system.run(worldRef, ctx);
|
||||
} finally {
|
||||
if (end) end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
global.TarinaiSystemOrder = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
|
||||
const rr = Math.max(0, Math.min(r || 0, Math.abs(w) / 2, Math.abs(h) / 2));
|
||||
|
|
@ -182,7 +212,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.children = opts.children || [];
|
||||
this.nextEatSound = opts.nextEatSound ?? 0;
|
||||
this.fightTimer = opts.fightTimer ?? 0;
|
||||
this.fightCooldown = opts.fightCooldown ?? rand(0, CONFIG.fightCooldown);
|
||||
this.fightCooldown = opts.fightCooldown ?? deterministicRange(world, "initial-fight-cooldown", 0, CONFIG.fightCooldown, this.familyKey || this.id || opts.birthSeed);
|
||||
this.fightTargetId = opts.fightTargetId || null;
|
||||
this.fightTargetIds = Array.isArray(opts.fightTargetIds) ? [...opts.fightTargetIds] : (this.fightTargetId ? [this.fightTargetId] : []);
|
||||
this.nextHeadbutt = opts.nextHeadbutt ?? 0;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,31 @@ function stopFailedBuildPlan(t, world, type = "", reason = "") {
|
|||
return false;
|
||||
}
|
||||
|
||||
|
||||
function forceBuiltStructureVisualRefresh(world, structure, reason = "structure-built") {
|
||||
if (!world) return;
|
||||
world.drawListDirty = true;
|
||||
if (world._renderStack) world._renderStack.signature = "";
|
||||
if (world._visibleRenderStack) {
|
||||
world._visibleRenderStack.backItems = [];
|
||||
world._visibleRenderStack.layered = [];
|
||||
world._visibleRenderStack.carriedPlushies = [];
|
||||
world._visibleRenderStack.lodgedPins = [];
|
||||
}
|
||||
world.markItemBucketsDirty?.(reason);
|
||||
world.markSpatialDirty?.(reason);
|
||||
world.ensureItemBuckets?.(reason);
|
||||
world.ensureSpatial?.(reason);
|
||||
if (structure) {
|
||||
structure.world = world;
|
||||
structure._builtVisibleAt = Number(world.time || 0) || 0;
|
||||
}
|
||||
if (typeof globalThis.render === "function") {
|
||||
globalThis.render();
|
||||
globalThis.requestAnimationFrame?.(() => globalThis.render?.());
|
||||
}
|
||||
}
|
||||
|
||||
function findStructureBuildSpot(t, world, type) {
|
||||
const baseX = Number.isFinite(t?.x) ? t.x : 0;
|
||||
const baseY = Number.isFinite(t?.y) ? t.y : 0;
|
||||
|
|
@ -114,9 +139,12 @@ function continueBuildPlan(t, world, dt) {
|
|||
structure.x = t.x;
|
||||
structure.y = t.y - Math.max(28, (t.radius || 24) * 1.28);
|
||||
}
|
||||
world.addItem?.(structure, `build:${plan.type}`) || world.items.push(structure);
|
||||
world.drawListDirty = true;
|
||||
const addedStructure = world.addItem?.(structure, `build:${plan.type}`) || (world.items.push(structure), structure);
|
||||
if (!addedStructure) {
|
||||
return stopFailedBuildPlan(t, world, plan.type, "作る場所が見つからない");
|
||||
}
|
||||
structure.use?.(t, world);
|
||||
forceBuiltStructureVisualRefresh(world, structure, `build:${plan.type}:visual-refresh`);
|
||||
t.setActionState?.("idle", { target: structure, reason: `${label}\u3092\u4f5c\u3063\u305f`, sleeping: false });
|
||||
applyNeedSatisfaction(t, { fulfill: plan.type === "plushie" ? 50 : 36, safety: plan.type === "plushie" ? 4 : 8 }, plan.type);
|
||||
t.buildPlan = null;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
const Tarinai = global.Tarinai;
|
||||
|
|
@ -37,12 +67,12 @@
|
|||
|
||||
emitZunchiDiseaseEffect(dt) {
|
||||
if (!this.zunchiDisease || !this.world?.effects) return;
|
||||
if (Math.random() < dt * (1.2 + (this.zunchiDiseaseSeverity || 0) * 1.4)) {
|
||||
this.world.effects.push(new Effect("zunchi_miasma", this.x + rand(-this.radius * 0.45, this.radius * 0.45), this.y - this.radius * rand(0.05, 0.72), {
|
||||
vx: rand(-5, 5),
|
||||
vy: rand(-10, -3),
|
||||
life: rand(1.4, 2.4),
|
||||
size: rand(8, 18) * (0.8 + (this.zunchiDiseaseSeverity || 0) * 0.45),
|
||||
if (deterministicChance(this.world, "zunchi-disease-miasma", dt * (1.2 + (this.zunchiDiseaseSeverity || 0) * 1.4), this)) {
|
||||
this.world.effects.push(new Effect("zunchi_miasma", this.x + deterministicRange(this.world, "zunchi-miasma-x", -this.radius * 0.45, this.radius * 0.45, this), this.y - this.radius * deterministicRange(this.world, "zunchi-miasma-y", 0.05, 0.72, this), {
|
||||
vx: deterministicRange(this.world, "zunchi-miasma-vx", -5, 5, this),
|
||||
vy: deterministicRange(this.world, "zunchi-miasma-vy", -10, -3, this),
|
||||
life: deterministicRange(this.world, "zunchi-miasma-life", 1.4, 2.4, this),
|
||||
size: deterministicRange(this.world, "zunchi-miasma-size", 8, 18, this) * (0.8 + (this.zunchiDiseaseSeverity || 0) * 0.45),
|
||||
color: "rgba(16,78,28,0.70)",
|
||||
}));
|
||||
}
|
||||
|
|
@ -55,7 +85,8 @@
|
|||
this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100);
|
||||
if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006);
|
||||
}
|
||||
if (!this.zunchiDisease && this.zunchiStain > 82 && Math.random() < (this.diseaseChance ? this.diseaseChance(dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) : dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045)) {
|
||||
const zunchiSelfInfectChance = this.diseaseChance ? this.diseaseChance(dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) : dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045;
|
||||
if (!this.zunchiDisease && this.zunchiStain > 82 && deterministicChance(this.world, "zunchi-self-infect", zunchiSelfInfectChance, this)) {
|
||||
this.infectZunchiDisease(null, true);
|
||||
this.world?.log?.(`${this.name}\u306f\u305a\u3093\u3061\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] });
|
||||
}
|
||||
|
|
@ -238,8 +269,8 @@
|
|||
const entry = this.world.nestBoxExitPoint ? this.world.nestBoxExitPoint(b, this) : this.world.nestBoxEntryPoint(b);
|
||||
this.x = clamp(entry.x, CONFIG.worldPadding, this.world.w - CONFIG.worldPadding);
|
||||
this.y = clamp(entry.y, CONFIG.worldPadding, this.world.h - CONFIG.worldPadding);
|
||||
this.vx = rand(-8, 8);
|
||||
this.vy = rand(6, 18);
|
||||
this.vx = deterministicRange(this.world, "sleep-disease-shiver-vx", -8, 8, this);
|
||||
this.vy = deterministicRange(this.world, "sleep-disease-shiver-vy", 6, 18, this);
|
||||
}
|
||||
this.insideNestBoxId = null;
|
||||
this.nestFade = 0;
|
||||
|
|
@ -316,32 +347,32 @@
|
|||
if (this.explosionDisease) {
|
||||
this.explosionDiseaseTimer = Math.max(0, (this.explosionDiseaseTimer || 0) - dt);
|
||||
if (this.addStress) this.addStress(dt * 0.22, { threshold: 9, duration: 3.6 });
|
||||
if (Math.random() < dt * 2.1) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
this.world?.effects?.push(new Effect("explosion", this.x + Math.cos(a) * this.radius * rand(0.2, 0.9), this.y + Math.sin(a) * this.radius * rand(0.1, 0.8), {
|
||||
vx: Math.cos(a) * rand(10, 38), vy: Math.sin(a) * rand(10, 38) - rand(8, 22), size: rand(5, 12), life: rand(0.18, 0.42), color: "rgba(255,64,48,0.86)"
|
||||
if (deterministicChance(this.world, "explosion-disease-spark", dt * 2.1, this)) {
|
||||
const a = deterministicAngle(this.world, "explosion-disease-spark-angle", this);
|
||||
this.world?.effects?.push(new Effect("explosion", this.x + Math.cos(a) * this.radius * deterministicRange(this.world, "explosion-disease-spark-rx", 0.2, 0.9, this), this.y + Math.sin(a) * this.radius * deterministicRange(this.world, "explosion-disease-spark-ry", 0.1, 0.8, this), {
|
||||
vx: Math.cos(a) * deterministicRange(this.world, "explosion-disease-spark-vx", 10, 38, this), vy: Math.sin(a) * deterministicRange(this.world, "explosion-disease-spark-vy", 10, 38, this) - deterministicRange(this.world, "explosion-disease-spark-up", 8, 22, this), size: deterministicRange(this.world, "explosion-disease-spark-size", 5, 12, this), life: deterministicRange(this.world, "explosion-disease-spark-life", 0.18, 0.42, this), color: "rgba(255,64,48,0.86)"
|
||||
}));
|
||||
}
|
||||
if (this.explosionDiseaseTimer <= 0.01) {
|
||||
this.world?.explodeDiseaseTarinai?.(this);
|
||||
return;
|
||||
}
|
||||
if (this.explosionDiseaseTimer < 8 && Math.random() < dt * 0.45) this.bubble("!", 1.0, "rgba(178,78,42,0.82)");
|
||||
if (this.explosionDiseaseTimer < 8 && deterministicChance(this.world, "explosion-disease-bubble", dt * 0.45, this)) this.bubble("!", 1.0, "rgba(178,78,42,0.82)");
|
||||
}
|
||||
const bleedAge = (this.world?.time || 0) - (this.lastBleedAt || -999);
|
||||
if (!this.fightDisease && this.fightDiseaseCooldown <= 0 && bleedAge >= (CONFIG.dayLength || 120) * 5 / 24 && bleedAge < (CONFIG.dayLength || 120) * 0.85) {
|
||||
if (Math.random() < (this.diseaseChance ? this.diseaseChance(dt * 0.000375) : dt * 0.000375)) this.infectFightDisease();
|
||||
if (deterministicChance(this.world, "fight-disease-from-bleed", this.diseaseChance ? this.diseaseChance(dt * 0.000375) : dt * 0.000375, this)) this.infectFightDisease();
|
||||
}
|
||||
if (this.zunchiDisease && Math.random() < dt * 0.00055 * (this.energy > 66 ? 1.8 : 1.0)) {
|
||||
if (this.zunchiDisease && deterministicChance(this.world, "zunchi-natural-recover", dt * 0.00055 * (this.energy > 66 ? 1.8 : 1.0), this)) {
|
||||
if (this.recoverZunchiDisease("\u81ea\u7136\u306b\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f")) this.world?.log?.(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c\u81ea\u7136\u306b\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
}
|
||||
if (this.fightDisease && Math.random() < dt * 0.00070 * (this.energy > 58 ? 1.4 : 1.0)) {
|
||||
if (this.fightDisease && deterministicChance(this.world, "fight-disease-natural-recover", dt * 0.00070 * (this.energy > 58 ? 1.4 : 1.0), this)) {
|
||||
this.recoverFightDisease("\u81ea\u7136\u306b\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
}
|
||||
if (this.fightDisease) {
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 3, safety: dt * 2 });
|
||||
if (Math.random() < dt * 0.30) this.wanderAngle += rand(-2.8, 2.8);
|
||||
if (Math.random() < dt * 0.08) this.bubble("?", 2.6, "rgba(150,78,44,0.80)");
|
||||
if (deterministicChance(this.world, "fight-disease-wander", dt * 0.30, this)) this.wanderAngle += deterministicRange(this.world, "fight-disease-wander-angle", -2.8, 2.8, this);
|
||||
if (deterministicChance(this.world, "fight-disease-bubble", dt * 0.08, this)) this.bubble("?", 2.6, "rgba(150,78,44,0.80)");
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Need planning runtime and Tarinai prototype extensions.
|
||||
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
|
||||
|
|
@ -600,7 +630,7 @@ function resolveNeedsCore(dt) {
|
|||
this.fightTargetIds = [];
|
||||
this.fightTargetId = null;
|
||||
this.counterAttackFromId = null;
|
||||
this.fightCooldown = Math.max(this.fightCooldown || 0, CONFIG.fightCooldown + rand(4, 8));
|
||||
this.fightCooldown = Math.max(this.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this.world, "fight-lost-target-cooldown", 4, 8, this, rival));
|
||||
if (rival) this.world?.markFightPairCooldown?.(this, rival, CONFIG.fightCooldown + 6);
|
||||
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(this, e => e && e.id === "fight_rival");
|
||||
this.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
|
||||
|
|
|
|||
|
|
@ -18,9 +18,21 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function updateOne(tarinai, dt) {
|
||||
function updateOne(tarinai, dt, options = {}) {
|
||||
if (!tarinai || tarinai.dead || !global.TarinaiUpdatePipeline?.updateOne) return false;
|
||||
return global.TarinaiUpdatePipeline.updateOne(tarinai, dt) !== false;
|
||||
return global.TarinaiUpdatePipeline.updateOne(tarinai, dt, options) !== false;
|
||||
}
|
||||
|
||||
function updateMotionOnly(tarinai, dt, options = {}) {
|
||||
if (!tarinai || tarinai.dead || !global.TarinaiMovementUpdateStep?.update) return false;
|
||||
tarinai.prevX = tarinai.x;
|
||||
tarinai.prevY = tarinai.y;
|
||||
const motionDt = Math.max(0.001, Math.min(0.05, Number(options.context?.motionDt ?? dt) || 0.016));
|
||||
return global.TarinaiMovementUpdateStep.update(tarinai, motionDt, {
|
||||
...(options.context || {}),
|
||||
motionOnly: true,
|
||||
motionDt,
|
||||
})?.done !== true || !tarinai.dead;
|
||||
}
|
||||
|
||||
function updateBatch(tarinaiList, dt) {
|
||||
|
|
@ -30,6 +42,6 @@
|
|||
return ran;
|
||||
}
|
||||
|
||||
global.TarinaiCreatureRuntime = Object.freeze({ installPrototypeBoundary, updateOne, updateBatch });
|
||||
global.TarinaiCreatureRuntime = Object.freeze({ installPrototypeBoundary, updateOne, updateMotionOnly, updateBatch });
|
||||
installPrototypeBoundary();
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Social, panic, fight, mate behavior runtime ticks.
|
||||
|
||||
|
|
@ -77,7 +107,7 @@ function updateFightBehavior(t, world, dt) {
|
|||
t.fightTargetIds = [];
|
||||
t.fightTargetId = null;
|
||||
t.counterAttackFromId = null;
|
||||
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + rand(4, 8));
|
||||
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(world, "fight-target-lost-cooldown", 4, 8, t, rival));
|
||||
if (isLiveTarinaiEntity(rival)) world.markFightPairCooldown?.(t, rival, CONFIG.fightCooldown + 6);
|
||||
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
|
||||
t.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
const Tarinai = global.Tarinai;
|
||||
|
|
@ -35,10 +65,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < (o.diseaseChance ? o.diseaseChance(dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) : dt * 0.065 * clamp(1 - d / 58, 0.12, 1))) {
|
||||
const zunchiSpreadChanceA = o.diseaseChance ? o.diseaseChance(dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) : dt * 0.065 * clamp(1 - d / 58, 0.12, 1);
|
||||
if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && deterministicChance(this.world, "social-zunchi-spread-a", zunchiSpreadChanceA, this, o)) {
|
||||
if (o.infectZunchiDisease(this)) this.world.log(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c${o.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
|
||||
}
|
||||
if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && Math.random() < (this.diseaseChance ? this.diseaseChance(dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) : dt * 0.065 * clamp(1 - d / 58, 0.12, 1))) {
|
||||
const zunchiSpreadChanceB = this.diseaseChance ? this.diseaseChance(dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) : dt * 0.065 * clamp(1 - d / 58, 0.12, 1);
|
||||
if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && deterministicChance(this.world, "social-zunchi-spread-b", zunchiSpreadChanceB, this, o)) {
|
||||
if (this.infectZunchiDisease(o)) this.world.log(`${o.name}\u306e\u305a\u3093\u3061\u75c5\u304c${this.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +90,7 @@
|
|||
nonSlave.thought = "\u305a\u3093\u3061\u3069\u308c\u3044\u304c\u8fd1\u3044";
|
||||
nonSlave.adjustRelation?.(slave, -dt * 0.08, dt * 0.24, "zunchi_slave_avoid");
|
||||
slave.adjustRelation?.(nonSlave, -dt * 0.02, 0, "zunchi_slave_avoid");
|
||||
if (Math.random() < dt * 0.35) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18);
|
||||
if (deterministicChance(this.world, "zunchi-slave-surprise", dt * 0.35, nonSlave, slave)) nonSlave.surpriseTimer = Math.max(nonSlave.surpriseTimer || 0, 0.18);
|
||||
continue;
|
||||
}
|
||||
const overlap = this.radius + o.radius - d;
|
||||
|
|
@ -66,7 +98,7 @@
|
|||
const push = (overlap + 6) * (pairBond ? 0.08 : 0.17);
|
||||
this.vx -= (dx / d) * push;
|
||||
this.vy -= (dy / d) * push;
|
||||
if (!pairBond && Math.random() < dt * 1.2) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
|
||||
if (!pairBond && deterministicChance(this.world, "body-overlap-surprise", dt * 1.2, this, o)) this.surpriseTimer = Math.max(this.surpriseTimer, 0.12);
|
||||
}
|
||||
if (pairBond && d > 26 && d < 120) {
|
||||
this.vx += (dx / d) * dt * 4.8;
|
||||
|
|
@ -96,10 +128,10 @@
|
|||
const pulse = Math.sin(this.world.time * 18 + this.age * 0.37) > 0.35;
|
||||
if (pulse && this.world.time > this.nextHeadbutt) {
|
||||
const nx = dx / d, ny = dy / d;
|
||||
this.vx -= nx * rand(24, 44);
|
||||
this.vy -= ny * rand(24, 44);
|
||||
o.vx += nx * rand(34, 64);
|
||||
o.vy += ny * rand(34, 64);
|
||||
this.vx -= nx * deterministicRange(this.world, "headbutt-self-x", 24, 44, this, o);
|
||||
this.vy -= ny * deterministicRange(this.world, "headbutt-self-y", 24, 44, this, o);
|
||||
o.vx += nx * deterministicRange(this.world, "headbutt-other-x", 34, 64, this, o);
|
||||
o.vy += ny * deterministicRange(this.world, "headbutt-other-y", 34, 64, this, o);
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { safety: 8, health: 4 });
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(o, { safety: 12, health: 6 });
|
||||
const damageToThis = o.outgoingDamage ? o.outgoingDamage(1.2) : 1.2;
|
||||
|
|
@ -114,7 +146,7 @@
|
|||
o.hurtTimer = Math.max(o.hurtTimer, 1.8);
|
||||
this.adjustRelation(o, -0.16, 0.12, "fight");
|
||||
o.adjustRelation(this, -0.22, 0.28 * o.personalityProfile().fear, "fight");
|
||||
if (Math.random() < 0.22 || o.energy < 24) {
|
||||
if (deterministicChance(this.world, "headbutt-fall", 0.22, this, o) || o.energy < 24) {
|
||||
o.fallTimer = Math.max(o.fallTimer, 0.58);
|
||||
o.fallMax = Math.max(o.fallMax || 0.58, o.fallTimer);
|
||||
o.fallDir = nx > 0 ? 1 : -1;
|
||||
|
|
@ -124,7 +156,7 @@
|
|||
this.world.maybeDefeatFromFightDamage?.(o, this, Math.max(damageToOther, 2.4));
|
||||
}
|
||||
this.world.spawnHeadbuttEffect((this.x + o.x) / 2, (this.y + o.y) / 2 - 5);
|
||||
this.nextHeadbutt = this.world.time + rand(0.34, 0.55);
|
||||
this.nextHeadbutt = this.world.time + deterministicRange(this.world, "next-headbutt", 0.34, 0.55, this, o);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -144,7 +176,8 @@
|
|||
const birthPeace = (this.postBirthPeaceTimer || 0) > 0 || (o.postBirthPeaceTimer || 0) > 0;
|
||||
const familyFightBlocked = this.world.areParentChild ? this.world.areParentChild(this, o) : false;
|
||||
const pairFightBlocked = this.world.areCoParents ? this.world.areCoParents(this, o) : false;
|
||||
if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && Math.random() < dt * (battleDrug ? 0.23 : 0.085) * personalityRisk * fearBrake * friendBrake * conflictBias) {
|
||||
const fightStartChance = dt * (battleDrug ? 0.23 : 0.085) * personalityRisk * fearBrake * friendBrake * conflictBias;
|
||||
if (this.world.canFightPair?.(this, o) && d < 56 && !familyFightBlocked && !pairFightBlocked && !birthPeace && fightRisk && this.fightCooldown <= 0 && o.fightCooldown <= 0 && deterministicChance(this.world, "social-fight-start", fightStartChance, this, o)) {
|
||||
this.conflictTargetId = o.id;
|
||||
o.conflictTargetId = this.id;
|
||||
const urge = 34 + (battleDrug ? 34 : 0) + (mixedZunchiSlave ? 10 : 0);
|
||||
|
|
@ -170,7 +203,7 @@
|
|||
const selfBehaviorId = typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(this) : this.behavior?.actionId;
|
||||
const otherBehaviorId = typeof getTarinaiBehaviorId === "function" ? getTarinaiBehaviorId(o) : o.behavior?.actionId;
|
||||
const mateIntent = selfBehaviorId === "approach_mate" || otherBehaviorId === "approach_mate" || loveDrug;
|
||||
if (mateIntent && Math.random() < dt * (loveDrug ? 0.78 : 0.30)) {
|
||||
if (mateIntent && deterministicChance(this.world, "birth-ritual-start", dt * (loveDrug ? 0.78 : 0.30), this, o)) {
|
||||
this.world.startBirthRitual(this, o);
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +216,7 @@
|
|||
this.fearTimer = Math.max(this.fearTimer, 0.18);
|
||||
}
|
||||
|
||||
if (this.loneliness < 12 && Math.random() < dt * 0.002) {
|
||||
if (this.loneliness < 12 && deterministicChance(this.world, "nearby-calm-log", dt * 0.002, this)) {
|
||||
this.world.log(`${this.name}\u306f\u8ab0\u304b\u306e\u8fd1\u304f\u3067\u3001\u305f\u308a\u306a\u3044\u3053\u3068\u3092\u5c11\u3057\u5fd8\u308c\u305f\u3002`, null, { participants: [this] });
|
||||
}
|
||||
},
|
||||
|
|
@ -221,49 +254,97 @@
|
|||
this.updateNestBoxPresence(dt);
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
this.impulseVx = 0;
|
||||
this.impulseVy = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = window.TarinaiMovementUpdateStep;
|
||||
const externalActive = marker?.markSleepExternalMotion?.(this, dt, "sleep-full-update") || marker?.sleepExternalMotionActive?.(this);
|
||||
if (this.target && !this.target.dead && this.isSleepFurniture(this.target)) {
|
||||
const spot = this.sleepSpotFor(this.target);
|
||||
const dx = spot.x - this.x;
|
||||
const dy = spot.y - this.y;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 2.2) {
|
||||
const step = Math.min(d, dt * (12 + bedComfort * 14));
|
||||
this.x += dx / d * step;
|
||||
this.y += dy / d * step;
|
||||
} else {
|
||||
const restore = externalActive ? (2.0 + bedComfort * 2.0) : (12 + bedComfort * 14);
|
||||
const maxRestore = externalActive ? Math.min(d, 3.2) : d;
|
||||
const step = Math.min(maxRestore, dt * restore);
|
||||
const sx = dx / d * step;
|
||||
const sy = dy / d * step;
|
||||
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, sx, sy, { dt, reason: externalActive ? "tarinai-sleep-restore-weak" : "tarinai-sleep-move" });
|
||||
else { this.x += sx; this.y += sy; }
|
||||
} else if (!externalActive && Math.hypot((this.vx || 0) + (this.impulseVx || 0), (this.vy || 0) + (this.impulseVy || 0)) < 0.35) {
|
||||
this.x = spot.x;
|
||||
this.y = spot.y;
|
||||
}
|
||||
}
|
||||
|
||||
const impulseVx = Number.isFinite(this.impulseVx) ? this.impulseVx : 0;
|
||||
const impulseVy = Number.isFinite(this.impulseVy) ? this.impulseVy : 0;
|
||||
const moveDx = ((Number.isFinite(this.vx) ? this.vx : 0) + impulseVx) * dt;
|
||||
const moveDy = ((Number.isFinite(this.vy) ? this.vy : 0) + impulseVy) * dt;
|
||||
if (Math.hypot(moveDx, moveDy) > 0.001) {
|
||||
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, moveDx, moveDy, { dt, reason: "tarinai-sleep-physical-full", maxChecks: 18, maxStep: 10 });
|
||||
else { this.x += moveDx; this.y += moveDy; }
|
||||
}
|
||||
if (this.world?.resolveSolidObstacleCollision) {
|
||||
const beforeContactX = Number(this.x || 0) || 0;
|
||||
const beforeContactY = Number(this.y || 0) || 0;
|
||||
const pushed = this.world.resolveSolidObstacleCollision(this, { maxPasses: 2, searchRadius: Math.max(72, (Number(this.radius) || 22) + 120), maxChecks: 18, slop: 0.28, reason: "tarinai-sleep-full-contact" });
|
||||
if (pushed) {
|
||||
this.lastSolidObstacleCollisionAt = this.world.time || 0;
|
||||
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) > 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.updateNestBoxPresence(dt);
|
||||
this.vx *= Math.pow(0.55, dt * 60);
|
||||
this.vy *= Math.pow(0.55, dt * 60);
|
||||
const bodyDrag = externalActive ? 0.86 : 0.55;
|
||||
this.vx *= Math.pow(bodyDrag, dt * 60);
|
||||
this.vy *= Math.pow(bodyDrag, dt * 60);
|
||||
const impulseDrag = Math.pow(0.58, dt * 3.2);
|
||||
this.impulseVx = Math.abs(impulseVx) < 0.35 ? 0 : impulseVx * impulseDrag;
|
||||
this.impulseVy = Math.abs(impulseVy) < 0.35 ? 0 : impulseVy * impulseDrag;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") {
|
||||
const partner = this.world.liveTarinaiById?.(this.birthPartnerId);
|
||||
if (partner) {
|
||||
const midX = (this.x + partner.x) / 2;
|
||||
const midY = (this.y + partner.y) / 2;
|
||||
const pdx = partner.x - this.x;
|
||||
const pdy = partner.y - this.y;
|
||||
const plen = Math.max(0.001, Math.hypot(pdx, pdy));
|
||||
const nx = -pdy / plen;
|
||||
const ny = pdx / plen;
|
||||
const desiredGap = this.radius * 0.36;
|
||||
const side = this.birthRitualRole || 1;
|
||||
const tx = midX + nx * desiredGap * side;
|
||||
const ty = midY + ny * desiredGap * side;
|
||||
this.vx += (tx - this.x) * dt * 3.8;
|
||||
this.vy += (ty - this.y) * dt * 3.8;
|
||||
const pendingImpulse = Math.hypot(Number.isFinite(this.impulseVx) ? this.impulseVx : 0, Number.isFinite(this.impulseVy) ? this.impulseVy : 0);
|
||||
const canceledByForce = this.world?.cancelBirthRitualOnForce?.(this, pendingImpulse, {
|
||||
reason: "強い衝撃で繁殖が中断された",
|
||||
target: this.target || null,
|
||||
fear: 1.0,
|
||||
cause: "stored_impulse",
|
||||
panic: true,
|
||||
silentLog: true,
|
||||
});
|
||||
if (!canceledByForce) {
|
||||
const partner = this.world.liveTarinaiById?.(this.birthPartnerId);
|
||||
if (partner) {
|
||||
const midX = (this.x + partner.x) / 2;
|
||||
const midY = (this.y + partner.y) / 2;
|
||||
const pdx = partner.x - this.x;
|
||||
const pdy = partner.y - this.y;
|
||||
const plen = Math.max(0.001, Math.hypot(pdx, pdy));
|
||||
const nx = -pdy / plen;
|
||||
const ny = pdx / plen;
|
||||
const desiredGap = this.radius * 0.36;
|
||||
const side = this.birthRitualRole || 1;
|
||||
const tx = midX + nx * desiredGap * side;
|
||||
const ty = midY + ny * desiredGap * side;
|
||||
this.vx += (tx - this.x) * dt * 3.8;
|
||||
this.vy += (ty - this.y) * dt * 3.8;
|
||||
}
|
||||
this.vx *= Math.pow(0.48, dt * 60);
|
||||
this.vy *= Math.pow(0.48, dt * 60);
|
||||
this.x += Math.sin(this.world.time * 26 + this.age) * 0.20;
|
||||
return;
|
||||
}
|
||||
this.vx *= Math.pow(0.48, dt * 60);
|
||||
this.vy *= Math.pow(0.48, dt * 60);
|
||||
this.x += Math.sin(this.world.time * 26 + this.age) * 0.20;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.intimidateTimer > 0.04 || this.state === "intimidate" || this.state === "ant_intimidate") {
|
||||
|
|
@ -349,22 +430,22 @@
|
|||
if (Number.isFinite(this.target.hp)) this.target.hp = Math.max(0, this.target.hp - (this.outgoingDamage ? this.outgoingDamage(4.6 + aggression * 3.4) : (4.6 + aggression * 3.4)));
|
||||
this.fearTimer = Math.max(this.fearTimer, 0.12);
|
||||
this.thought = "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
|
||||
if (Math.random() < 0.55) this.bubble("!", 0.8, "rgba(145,68,52,0.76)");
|
||||
if (deterministicChance(this.world, "ant-attack-bubble", 0.55, this, this.target)) this.bubble("!", 0.8, "rgba(145,68,52,0.76)");
|
||||
}
|
||||
}
|
||||
}
|
||||
ax += (dx / d) * baseSpeed * sign * strength;
|
||||
ay += (dy / d) * baseSpeed * sign * strength;
|
||||
} else if (this.state === "panic") {
|
||||
this.wanderAngle += rand(-3.8, 3.8) * dt;
|
||||
this.wanderAngle += deterministicRange(this.world, "panic-wander", -3.8, 3.8, this) * dt;
|
||||
ax += Math.cos(this.wanderAngle) * baseSpeed * 1.5;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 1.5;
|
||||
} else if (this.state === "zunchi_sick") {
|
||||
this.wanderAngle += rand(-2.4, 2.4) * dt + Math.sin(this.world.time * 1.7 + this.age) * dt * 0.8;
|
||||
this.wanderAngle += deterministicRange(this.world, "zunchi-sick-wander", -2.4, 2.4, this) * dt + Math.sin(this.world.time * 1.7 + this.age) * dt * 0.8;
|
||||
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.72;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.72;
|
||||
} else if (this.state === "fight_sick") {
|
||||
this.wanderAngle += rand(-4.2, 4.2) * dt + Math.sin(this.world.time * 3.2 + this.age) * dt * 1.6;
|
||||
this.wanderAngle += deterministicRange(this.world, "fight-sick-wander", -4.2, 4.2, this) * dt + Math.sin(this.world.time * 3.2 + this.age) * dt * 1.6;
|
||||
ax += Math.cos(this.wanderAngle) * baseSpeed * 0.88;
|
||||
ay += Math.sin(this.wanderAngle) * baseSpeed * 0.88;
|
||||
} else {
|
||||
|
|
@ -388,8 +469,18 @@
|
|||
|
||||
const impulseVx = Number.isFinite(this.impulseVx) ? this.impulseVx : 0;
|
||||
const impulseVy = Number.isFinite(this.impulseVy) ? this.impulseVy : 0;
|
||||
this.x += (this.vx + impulseVx) * dt;
|
||||
this.y += (this.vy + impulseVy) * dt;
|
||||
const moveDx = (this.vx + impulseVx) * dt;
|
||||
const moveDy = (this.vy + impulseVy) * dt;
|
||||
if (this.world?.moveTarinaiWithCollision) {
|
||||
const motionOnlyPass = Boolean(this._motionOnlyPhysicsPass);
|
||||
this.world.moveTarinaiWithCollision(this, moveDx, moveDy, motionOnlyPass
|
||||
? { dt, reason: "tarinai-move-smooth", light: true, maxChecks: this.state === "panic" ? 18 : 14, maxStep: this.state === "panic" ? 12 : 14 }
|
||||
: { dt, reason: "tarinai-move" });
|
||||
}
|
||||
else {
|
||||
this.x += moveDx;
|
||||
this.y += moveDy;
|
||||
}
|
||||
const impulseDrag = Math.pow(0.58, dt * 3.2);
|
||||
this.impulseVx = Math.abs(impulseVx) < 0.35 ? 0 : impulseVx * impulseDrag;
|
||||
this.impulseVy = Math.abs(impulseVy) < 0.35 ? 0 : impulseVy * impulseDrag;
|
||||
|
|
@ -403,10 +494,10 @@
|
|||
const p = this.entryTimer > 0 ? -this.radius * 2.4 : CONFIG.worldPadding;
|
||||
const maxX = this.entryTimer > 0 ? this.world.w + this.radius * 2.4 : this.world.w - CONFIG.worldPadding;
|
||||
const maxY = this.entryTimer > 0 ? this.world.h + this.radius * 2.4 : this.world.h - CONFIG.worldPadding;
|
||||
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx) * 0.5; this.wanderAngle = rand(-0.8, 0.8); this.surpriseTimer = 0.15; }
|
||||
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy) * 0.5; this.wanderAngle = rand(0.3, 1.8); this.surpriseTimer = 0.15; }
|
||||
if (this.x > maxX) { this.x = maxX; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = rand(2.5, 3.8); this.surpriseTimer = 0.15; }
|
||||
if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = rand(-2.2, -0.7); this.surpriseTimer = 0.15; }
|
||||
if (this.x < p) { this.x = p; this.vx = Math.abs(this.vx) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-left", -0.8, 0.8, this); this.surpriseTimer = 0.15; }
|
||||
if (this.y < p) { this.y = p; this.vy = Math.abs(this.vy) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-top", 0.3, 1.8, this); this.surpriseTimer = 0.15; }
|
||||
if (this.x > maxX) { this.x = maxX; this.vx = -Math.abs(this.vx) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-right", 2.5, 3.8, this); this.surpriseTimer = 0.15; }
|
||||
if (this.y > maxY) { this.y = maxY; this.vy = -Math.abs(this.vy) * 0.5; this.wanderAngle = deterministicRange(this.world, "wall-bounce-bottom", -2.2, -0.7, this); this.surpriseTimer = 0.15; }
|
||||
},
|
||||
|
||||
checkLife(dt = 0.016) {
|
||||
|
|
@ -441,7 +532,7 @@
|
|||
this.world.markSpatialDirty?.("tarinai-death-plushie-cleanup");
|
||||
this.world.addItem?.(new Item("trace", this.x, this.y), "tarinai-death-trace") || this.world.items.push(new Item("trace", this.x, this.y));
|
||||
this.world.addItem?.(new Item("splat", this.x, this.y), "tarinai-death-splat") || this.world.items.push(new Item("splat", this.x, this.y));
|
||||
if (Math.random() < 0.55) {
|
||||
if (deterministicChance(this.world, "death-grass-spawn", 0.55, this, finalReason)) {
|
||||
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 });
|
||||
if (spot) {
|
||||
const deathGrass = new Item("grass", spot.x, spot.y);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Layer: entity-runtime/tarinai/legacy
|
||||
// Compatibility owner for the former monolithic Tarinai.prototype.update body.
|
||||
|
|
@ -75,7 +105,7 @@
|
|||
this.defeatedTimer = Math.max(0, this.defeatedTimer - dt);
|
||||
if (wasFighting && this.fightTimer <= 0.04 && this.defeatedById) {
|
||||
const winner = this.world.liveTarinaiById?.(this.defeatedById) || null;
|
||||
this.defeatedTimer = Math.max(this.defeatedTimer, rand(3.4, 5.2));
|
||||
this.defeatedTimer = Math.max(this.defeatedTimer, deterministicRange(this.world, "defeated-recover-timer", 3.4, 5.2, this));
|
||||
this.fearTimer = Math.max(this.fearTimer, 1.0 * this.personalityProfile().fear);
|
||||
this.hurtTimer = Math.max(this.hurtTimer, 1.8);
|
||||
this.fallTimer = Math.max(this.fallTimer, 1.15);
|
||||
|
|
@ -95,27 +125,27 @@
|
|||
&& (this.fallTimer || 0) <= 0.04
|
||||
&& !this.dead
|
||||
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(this.state || ""))
|
||||
&& Math.random() < dt * 0.055) {
|
||||
const dir = Math.random() < 0.5 ? -1 : 1;
|
||||
this.groundStumbleCooldown = rand(8.5, 16.0);
|
||||
this.fallTimer = Math.max(this.fallTimer || 0, rand(0.86, 1.18));
|
||||
&& deterministicChance(this.world, "foot-massage-stumble", dt * 0.055, this)) {
|
||||
const dir = deterministicSigned(this.world, "foot-massage-stumble-dir", this);
|
||||
this.groundStumbleCooldown = deterministicRange(this.world, "foot-massage-stumble-cooldown", 8.5, 16.0, this);
|
||||
this.fallTimer = Math.max(this.fallTimer || 0, deterministicRange(this.world, "foot-massage-stumble-fall", 0.86, 1.18, this));
|
||||
this.fallMax = Math.max(this.fallMax || 1.05, this.fallTimer);
|
||||
this.fallDir = dir;
|
||||
this.vx += dir * rand(26, 52);
|
||||
this.vy += rand(-18, 18);
|
||||
this.vx += dir * deterministicRange(this.world, "foot-massage-stumble-vx", 26, 52, this);
|
||||
this.vy += deterministicRange(this.world, "foot-massage-stumble-vy", -18, 18, this);
|
||||
this.world?.spawnFallEffect?.(this.x, this.y + this.radius * 0.45);
|
||||
this.setActionState?.("idle", { target: null, reason: "足つぼの突起につまづいた。", sleeping: false, clearTarget: false });
|
||||
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble?.("うっ", 1.6, "rgba(86,70,96,0.78)");
|
||||
}
|
||||
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && Math.random() < dt * 0.050) {
|
||||
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && deterministicChance(this.world, "stress-breath-bubble", dt * 0.050, this)) {
|
||||
this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
|
||||
}
|
||||
if (this.state === "idle" && this.stress < 78 && Math.random() < dt * 0.018) {
|
||||
if (this.state === "idle" && this.stress < 78 && deterministicChance(this.world, "idle-bubble", dt * 0.018, this)) {
|
||||
this.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
|
||||
}
|
||||
if (this.hurtTimer > 0 && Math.random() < dt * 5.2) {
|
||||
if (this.hurtTimer > 0 && deterministicChance(this.world, "hurt-bleed-effect", dt * 5.2, this)) {
|
||||
const side = this.facingDir();
|
||||
this.world.spawnBleedEffect(this.x - side * this.radius * rand(0.15, 0.42), this.y - this.radius * rand(0.08, 0.34));
|
||||
this.world.spawnBleedEffect(this.x - side * this.radius * deterministicRange(this.world, "hurt-bleed-x", 0.15, 0.42, this), this.y - this.radius * deterministicRange(this.world, "hurt-bleed-y", 0.08, 0.34, this));
|
||||
}
|
||||
|
||||
this.tempTimer += dt;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,121 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: entity-runtime/tarinai/policy
|
||||
// Owns Tarinai update cadence decisions. The simulation creature system asks
|
||||
// this policy when an individual should run at realtime vs low-frequency cadence.
|
||||
// Owns Tarinai update cadence decisions. The cadence throttles expensive
|
||||
// thinking/environment work, while visible movement can still be integrated
|
||||
// every frame via the smooth-motion pass.
|
||||
(function (global) {
|
||||
const helpers = () => global.TarinaiSimulationRuntime?.helpers || {};
|
||||
|
||||
function updateInterval(worldRef, tarinai, visibleRect) {
|
||||
if (!tarinai || tarinai.dead) return Infinity;
|
||||
if (worldRef.selected === tarinai) return 0;
|
||||
if ((tarinai.hurtTimer || 0) > 0.04 || (tarinai.pokeFlashTimer || 0) > 0.03 || (tarinai.entryTimer || 0) > 0.01) return 0;
|
||||
if (helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, visibleRect)) return 0;
|
||||
const count = (worldRef.tarinai || []).length;
|
||||
function behaviorOf(tarinai) {
|
||||
return typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior;
|
||||
}
|
||||
|
||||
function behaviorId(tarinai) {
|
||||
const behavior = behaviorOf(tarinai);
|
||||
return String(behavior?.actionId || tarinai?.state || "none");
|
||||
}
|
||||
|
||||
function visible(worldRef, tarinai, visibleRect) {
|
||||
return Boolean(helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, visibleRect));
|
||||
}
|
||||
|
||||
function hasMotionNeed(tarinai) {
|
||||
if (!tarinai || tarinai.dead) return false;
|
||||
if (tarinai.target && !tarinai.target.dead) return true;
|
||||
const speed = Math.hypot((tarinai.vx || 0) + (tarinai.impulseVx || 0), (tarinai.vy || 0) + (tarinai.impulseVy || 0));
|
||||
if (speed > 1.8) return true;
|
||||
const id = behaviorId(tarinai);
|
||||
return /idle|wander|approach|follow|seek|panic|fight|birth_ritual|sunbath|zunchi_sick|fight_sick|cursor_/i.test(id);
|
||||
}
|
||||
|
||||
function hasPassivePhysicsNeed(tarinai, worldRef = null, isVisible = false) {
|
||||
if (!tarinai || tarinai.dead) return false;
|
||||
const vx = Number.isFinite(tarinai.vx) ? tarinai.vx : 0;
|
||||
const vy = Number.isFinite(tarinai.vy) ? tarinai.vy : 0;
|
||||
const ivx = Number.isFinite(tarinai.impulseVx) ? tarinai.impulseVx : 0;
|
||||
const ivy = Number.isFinite(tarinai.impulseVy) ? tarinai.impulseVy : 0;
|
||||
if (Math.hypot(vx + ivx, vy + ivy) > 0.35) return true;
|
||||
const sleeping = tarinai.state === "sleep" || tarinai.sleeping;
|
||||
if (sleeping && isVisible && !worldRef?.isTarinaiHiddenInNestBox?.(tarinai)) return true;
|
||||
if (sleeping && tarinai.target && !tarinai.target.dead && tarinai.isSleepFurniture?.(tarinai.target) && tarinai.target.type !== "nest_box") return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function cadence(worldRef, tarinai, visibleRect) {
|
||||
if (!tarinai || tarinai.dead) return { interval: Infinity, lane: "dead", smoothMotion: false, visible: false, urgent: false };
|
||||
const isSelected = worldRef.selected === tarinai;
|
||||
if (isSelected) return { interval: 0, lane: "selected", smoothMotion: false, visible: true, urgent: true };
|
||||
|
||||
const id = behaviorId(tarinai);
|
||||
const state = String(tarinai.state || id || "");
|
||||
const tier = global.TarinaiPerf?.renderQualityTier?.() || "high";
|
||||
if (tier === "high" && count <= 36) return 0;
|
||||
const farRect = helpers().updateVisibleWorldRect?.(worldRef, 520) || visibleRect;
|
||||
return helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, farRect) ? 0.22 : 0.70;
|
||||
const count = (worldRef.tarinai || []).length;
|
||||
const isVisible = visible(worldRef, tarinai, visibleRect);
|
||||
const farRect = isVisible ? visibleRect : (helpers().updateVisibleWorldRect?.(worldRef, 520) || visibleRect);
|
||||
const isNear = isVisible || Boolean(helpers().pointInRect?.(tarinai.x || 0, tarinai.y || 0, farRect));
|
||||
const motionNeed = hasMotionNeed(tarinai);
|
||||
|
||||
const hardRealtimeUrgent =
|
||||
(tarinai.hurtTimer || 0) > 0.04 ||
|
||||
(tarinai.pokeFlashTimer || 0) > 0.03 ||
|
||||
(tarinai.entryTimer || 0) > 0.01 ||
|
||||
(tarinai.fallTimer || 0) > 0.01 ||
|
||||
/fight|knockback|danger|hurt|ant_attack|cursor_enemy|explosion/i.test(id);
|
||||
if (hardRealtimeUrgent) return { interval: 0, lane: "urgent", smoothMotion: false, visible: isVisible, urgent: true };
|
||||
|
||||
// Panic movement must stay visually continuous, but the full creature
|
||||
// pipeline does not need to run every rendered frame. Treat panic/flee as
|
||||
// a high-priority motion lane: AI/timers are quantized, motion-only still
|
||||
// runs on visible skipped frames.
|
||||
const panicUrgent = (tarinai.fearTimer || 0) > 0.20 || /panic|flee/i.test(id);
|
||||
if (panicUrgent) {
|
||||
const interval = isVisible ? (tier === "low" ? 0.12 : 0.08) : (isNear ? 0.18 : 0.36);
|
||||
return { interval, lane: "panic", smoothMotion: Boolean(isVisible && !worldRef.isTarinaiHiddenInNestBox?.(tarinai)), visible: isVisible, urgent: true };
|
||||
}
|
||||
|
||||
let interval;
|
||||
let lane = "calm";
|
||||
|
||||
if (/birth_ritual/i.test(id) || state === "birth_ritual" || (tarinai.birthRitualTimer || 0) > 0.04) {
|
||||
lane = "birth";
|
||||
interval = isVisible ? (tier === "low" ? 0.16 : 0.12) : 0.24;
|
||||
} else if (/approach_parent_or_child|follow_parent|approach_mate|seek_bed|play_ball|seek_material|build/i.test(id) || /follow_parent|seek_bed|play_ball/.test(state)) {
|
||||
lane = "moving";
|
||||
interval = isVisible ? (tier === "low" ? 0.14 : 0.10) : (isNear ? 0.22 : 0.55);
|
||||
} else if (/eat_food|eat/i.test(id) || state === "eat" || (tarinai.eatTimer || 0) > 0.04) {
|
||||
lane = "eat";
|
||||
interval = isVisible ? 0.22 : 0.48;
|
||||
} else if (/sleep|sleep_in_bed/.test(id) || state === "sleep" || tarinai.sleeping) {
|
||||
lane = "sleep";
|
||||
interval = isVisible ? 0.42 : (isNear ? 0.70 : 1.10);
|
||||
} else if (/idle|none/.test(id) || state === "idle") {
|
||||
lane = "idle";
|
||||
if (isVisible) interval = tier === "low" ? 0.24 : (tier === "medium" || tier === "mid" ? 0.20 : (count > 28 ? 0.16 : 0.12));
|
||||
else interval = isNear ? 0.55 : 1.10;
|
||||
} else if (isVisible) {
|
||||
lane = "visible";
|
||||
interval = tier === "low" ? (count > 24 ? 0.18 : 0.14) : (tier === "medium" || tier === "mid" ? (count > 28 ? 0.14 : 0.10) : (count > 36 ? 0.11 : (count > 24 ? 0.08 : 0.05)));
|
||||
} else {
|
||||
lane = isNear ? "near" : "far";
|
||||
interval = isNear ? (tier === "low" ? 0.40 : 0.30) : (tier === "low" ? 1.20 : 0.90);
|
||||
}
|
||||
|
||||
const passivePhysicsNeed = hasPassivePhysicsNeed(tarinai, worldRef, isVisible);
|
||||
const sleepPhysical = lane === "sleep" && passivePhysicsNeed;
|
||||
const eatPhysical = lane === "eat" && passivePhysicsNeed;
|
||||
const smoothAllowed = lane !== "eat" && lane !== "sleep";
|
||||
const smoothMotion = Boolean(isVisible
|
||||
&& Number.isFinite(interval)
|
||||
&& interval > 0
|
||||
&& interval <= 0.75
|
||||
&& !worldRef.isTarinaiHiddenInNestBox?.(tarinai)
|
||||
&& ((smoothAllowed && motionNeed) || sleepPhysical || eatPhysical));
|
||||
return { interval, lane, smoothMotion, visible: isVisible, urgent: false, sleepPhysical, passivePhysicsNeed };
|
||||
}
|
||||
|
||||
function updateInterval(worldRef, tarinai, visibleRect) {
|
||||
return cadence(worldRef, tarinai, visibleRect).interval;
|
||||
}
|
||||
|
||||
function shouldRunRealtime(worldRef, tarinai, visibleRect) {
|
||||
|
|
@ -24,6 +124,8 @@
|
|||
|
||||
global.TarinaiCreatureUpdatePolicy = Object.freeze({
|
||||
updateInterval,
|
||||
updateCadence: cadence,
|
||||
behaviorId,
|
||||
shouldRunRealtime,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
// Layer: entity-runtime/tarinai/update-step
|
||||
// Owns per-frame timers, passive metabolism, disease/effect ticks, colony mood
|
||||
|
|
@ -76,7 +106,7 @@
|
|||
t.defeatedTimer = Math.max(0, t.defeatedTimer - dt);
|
||||
if (wasFighting && t.fightTimer <= 0.04 && t.defeatedById) {
|
||||
const winner = t.world.liveTarinaiById?.(t.defeatedById) || null;
|
||||
t.defeatedTimer = Math.max(t.defeatedTimer, rand(3.4, 5.2));
|
||||
t.defeatedTimer = Math.max(t.defeatedTimer, deterministicRange(t.world, "defeated-recover-timer", 3.4, 5.2, t));
|
||||
t.fearTimer = Math.max(t.fearTimer, 1.0 * t.personalityProfile().fear);
|
||||
t.hurtTimer = Math.max(t.hurtTimer, 1.8);
|
||||
t.fallTimer = Math.max(t.fallTimer, 1.15);
|
||||
|
|
@ -96,27 +126,27 @@
|
|||
&& (t.fallTimer || 0) <= 0.04
|
||||
&& !t.dead
|
||||
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(t.state || ""))
|
||||
&& Math.random() < dt * 0.055) {
|
||||
const dir = Math.random() < 0.5 ? -1 : 1;
|
||||
t.groundStumbleCooldown = rand(8.5, 16.0);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, rand(0.86, 1.18));
|
||||
&& deterministicChance(t.world, "foot-massage-stumble", dt * 0.055, t)) {
|
||||
const dir = deterministicSigned(t.world, "foot-massage-stumble-dir", t);
|
||||
t.groundStumbleCooldown = deterministicRange(t.world, "foot-massage-stumble-cooldown", 8.5, 16.0, t);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, deterministicRange(t.world, "foot-massage-stumble-fall", 0.86, 1.18, t));
|
||||
t.fallMax = Math.max(t.fallMax || 1.05, t.fallTimer);
|
||||
t.fallDir = dir;
|
||||
t.vx += dir * rand(26, 52);
|
||||
t.vy += rand(-18, 18);
|
||||
t.vx += dir * deterministicRange(t.world, "foot-massage-stumble-vx", 26, 52, t);
|
||||
t.vy += deterministicRange(t.world, "foot-massage-stumble-vy", -18, 18, t);
|
||||
t.world?.spawnFallEffect?.(t.x, t.y + t.radius * 0.45);
|
||||
t.setActionState?.("idle", { target: null, reason: "足つぼの突起につまづいた。", sleeping: false, clearTarget: false });
|
||||
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("うっ", 1.6, "rgba(86,70,96,0.78)");
|
||||
}
|
||||
if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && Math.random() < dt * 0.050) {
|
||||
if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && deterministicChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
|
||||
t.bubble("はっ...はっ...", 3.4, "rgba(58,48,63,0.80)");
|
||||
}
|
||||
if (t.state === "idle" && t.stress < 78 && Math.random() < dt * 0.018) {
|
||||
if (t.state === "idle" && t.stress < 78 && deterministicChance(t.world, "idle-bubble", dt * 0.018, t)) {
|
||||
t.bubble(pick(["はう", "はうぅ", "はうっ", "はぅ"]), 4.2, "rgba(62,84,45,0.76)");
|
||||
}
|
||||
if (t.hurtTimer > 0 && Math.random() < dt * 5.2) {
|
||||
if (t.hurtTimer > 0 && deterministicChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) {
|
||||
const side = t.facingDir();
|
||||
t.world.spawnBleedEffect(t.x - side * t.radius * rand(0.15, 0.42), t.y - t.radius * rand(0.08, 0.34));
|
||||
t.world.spawnBleedEffect(t.x - side * t.radius * deterministicRange(t.world, "hurt-bleed-x", 0.15, 0.42, t), t.y - t.radius * deterministicRange(t.world, "hurt-bleed-y", 0.08, 0.34, t));
|
||||
}
|
||||
|
||||
t.tempTimer += dt;
|
||||
|
|
|
|||
|
|
@ -37,22 +37,204 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function update(t, dt) {
|
||||
|
||||
function numeric(v, fallback = 0) {
|
||||
return Number.isFinite(Number(v)) ? Number(v) : fallback;
|
||||
}
|
||||
|
||||
function markSleepExternalMotion(t, dt, reason = "sleep-external-motion") {
|
||||
if (!t || !(t.state === "sleep" || t.sleeping)) return false;
|
||||
const world = t.world || null;
|
||||
const now = Number(world?.time || 0) || 0;
|
||||
const vx = numeric(t.vx);
|
||||
const vy = numeric(t.vy);
|
||||
const ivx = numeric(t.impulseVx);
|
||||
const ivy = numeric(t.impulseVy);
|
||||
const speed = Math.hypot(vx + ivx, vy + ivy);
|
||||
const recentlyHit = now - Number(t.lastSolidObstacleCollisionAt || t.lastPhysicalCollisionDamageAt || -999) < 0.35;
|
||||
if (speed > 0.55 || recentlyHit) {
|
||||
const hold = speed > 18 ? 1.15 : (speed > 3 ? 0.75 : 0.42);
|
||||
t._sleepExternalMotionUntil = Math.max(Number(t._sleepExternalMotionUntil || 0) || 0, now + hold);
|
||||
t._sleepExternalMotionReason = reason;
|
||||
return true;
|
||||
}
|
||||
return now < Number(t._sleepExternalMotionUntil || 0);
|
||||
}
|
||||
|
||||
function sleepExternalMotionActive(t) {
|
||||
const now = Number(t?.world?.time || 0) || 0;
|
||||
return Boolean(t && (t.state === "sleep" || t.sleeping) && now < Number(t._sleepExternalMotionUntil || 0));
|
||||
}
|
||||
|
||||
function passivePhysicalMotion(t, dt, context = {}) {
|
||||
if (!t || t.dead) return { done: true };
|
||||
const world = t.world;
|
||||
const beforeX = numeric(t.x);
|
||||
const beforeY = numeric(t.y);
|
||||
const beforeVelocity = Number.isFinite(t._lastPhysicalVelocityX) && Number.isFinite(t._lastPhysicalVelocityY)
|
||||
? { vx: t._lastPhysicalVelocityX, vy: t._lastPhysicalVelocityY }
|
||||
: combinedVelocity(t);
|
||||
|
||||
// Nest boxes intentionally hide and pin sleepers. Visible beds, eating, and
|
||||
// other locked states still need a passive physics shell so external
|
||||
// impulses/mechanical contacts are not delayed until the next low-frequency
|
||||
// full update.
|
||||
if (world?.isTarinaiHiddenInNestBox?.(t)) {
|
||||
t.vx = 0;
|
||||
t.vy = 0;
|
||||
t.impulseVx = 0;
|
||||
t.impulseVy = 0;
|
||||
t.updateNestBoxPresence?.(dt);
|
||||
return { done: false, passive: true };
|
||||
}
|
||||
|
||||
let moveX = 0;
|
||||
let moveY = 0;
|
||||
const state = String(t.state || "");
|
||||
|
||||
const sleepExternal = markSleepExternalMotion(t, dt, "passive-physical");
|
||||
if ((state === "sleep" || t.sleeping) && t.target && !t.target.dead && t.isSleepFurniture?.(t.target) && t.target.type !== "nest_box") {
|
||||
const comfort = world?.bedComfort ? world.bedComfort(t.target) : 0.72;
|
||||
const spot = t.sleepSpotFor ? t.sleepSpotFor(t.target) : t.target;
|
||||
const dx = numeric(spot?.x, beforeX) - beforeX;
|
||||
const dy = numeric(spot?.y, beforeY) - beforeY;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const strongExternal = sleepExternal && Math.hypot(numeric(t.vx) + numeric(t.impulseVx), numeric(t.vy) + numeric(t.impulseVy)) > 1.2;
|
||||
// Beds should gently keep a sleeper nearby, but must not pin the body.
|
||||
// Under external force the restoring pull is deliberately weak so a
|
||||
// sleeping Tarinai behaves like a physical object instead of snapping back
|
||||
// to the sleep spot every smooth/full update.
|
||||
if (d > 1.2) {
|
||||
const restore = strongExternal ? (2.0 + comfort * 2.0) : (12 + comfort * 14);
|
||||
const maxRestore = strongExternal ? Math.min(d, 2.8) : d;
|
||||
const step = Math.min(maxRestore, dt * restore);
|
||||
moveX += dx / d * step;
|
||||
moveY += dy / d * step;
|
||||
} else if (!strongExternal && d > 0.05 && Math.hypot(t.vx || 0, t.vy || 0, t.impulseVx || 0, t.impulseVy || 0) < 0.6) {
|
||||
moveX += dx;
|
||||
moveY += dy;
|
||||
}
|
||||
}
|
||||
|
||||
const vx = numeric(t.vx);
|
||||
const vy = numeric(t.vy);
|
||||
const impulseVx = numeric(t.impulseVx);
|
||||
const impulseVy = numeric(t.impulseVy);
|
||||
moveX += (vx + impulseVx) * dt;
|
||||
moveY += (vy + impulseVy) * dt;
|
||||
|
||||
const movedDistance = Math.hypot(moveX, moveY);
|
||||
if (movedDistance > 0.001) {
|
||||
if (world?.moveTarinaiWithCollision) {
|
||||
world.moveTarinaiWithCollision(t, moveX, moveY, {
|
||||
dt,
|
||||
reason: state === "sleep" ? "tarinai-sleep-physical" : "tarinai-passive-physical",
|
||||
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
||||
maxStep: 10,
|
||||
});
|
||||
} else {
|
||||
t.x = beforeX + moveX;
|
||||
t.y = beforeY + moveY;
|
||||
}
|
||||
}
|
||||
|
||||
const bodyDrag = state === "sleep" ? (sleepExternalMotionActive(t) ? 0.86 : 0.55) : (state === "eat" ? 0.42 : 0.66);
|
||||
t.vx = Math.abs(numeric(t.vx) * Math.pow(bodyDrag, dt * 60)) < 0.025 ? 0 : numeric(t.vx) * Math.pow(bodyDrag, dt * 60);
|
||||
t.vy = Math.abs(numeric(t.vy) * Math.pow(bodyDrag, dt * 60)) < 0.025 ? 0 : numeric(t.vy) * Math.pow(bodyDrag, dt * 60);
|
||||
const impulseDrag = Math.pow(0.58, dt * 3.2);
|
||||
t.impulseVx = Math.abs(impulseVx) < 0.35 ? 0 : impulseVx * impulseDrag;
|
||||
t.impulseVy = Math.abs(impulseVy) < 0.35 ? 0 : impulseVy * impulseDrag;
|
||||
|
||||
applyVelocityShockDamage(t, beforeVelocity, dt);
|
||||
t.updateNestBoxPresence?.(dt);
|
||||
|
||||
const dx = numeric(t.x) - beforeX;
|
||||
const dy = numeric(t.y) - beforeY;
|
||||
if (world?.resolveSolidObstacleCollision && !world.isTarinaiHiddenInNestBox?.(t) && (state === "sleep" || sleepExternalMotionActive(t))) {
|
||||
// A moving mechanical item can hit a sleeping Tarinai while the Tarinai's
|
||||
// own velocity is zero. In that case swept movement will never run, so
|
||||
// do a small contact pass every passive-physics tick.
|
||||
const beforeContactX = numeric(t.x);
|
||||
const beforeContactY = numeric(t.y);
|
||||
const pushed = world.resolveSolidObstacleCollision(t, {
|
||||
maxPasses: 2,
|
||||
searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120),
|
||||
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
||||
slop: 0.28,
|
||||
reason: "tarinai-sleep-passive-contact",
|
||||
});
|
||||
if (pushed) {
|
||||
t.lastSolidObstacleCollisionAt = world.time || 0;
|
||||
markSleepExternalMotion(t, dt, "sleep-contact-push");
|
||||
const cdx = numeric(t.x) - beforeContactX;
|
||||
const cdy = numeric(t.y) - beforeContactY;
|
||||
if (Math.hypot(cdx, cdy) > 0.05) {
|
||||
const pushCarry = state === "sleep" ? 0.085 : 0.040;
|
||||
t.vx = clamp(numeric(t.vx) + cdx / Math.max(0.016, dt) * pushCarry, -170, 170);
|
||||
t.vy = clamp(numeric(t.vy) + cdy / Math.max(0.016, dt) * pushCarry, -170, 170);
|
||||
}
|
||||
}
|
||||
} else if (dx * dx + dy * dy <= 0.25 && world?.resolveFenceCollision && !world.isTarinaiHiddenInNestBox?.(t)) {
|
||||
const now = world.time || 0;
|
||||
if (now >= (t._nextPassiveSolidRelaxAt || 0)) {
|
||||
const jitter = typeof stableUnit === "function" ? stableUnit(t.id || t.familyKey || 0, "passive-solid-relax") * 0.55 : Math.random() * 0.55;
|
||||
t._nextPassiveSolidRelaxAt = now + 1.20 + jitter;
|
||||
world.resolveFenceCollision(t);
|
||||
}
|
||||
}
|
||||
return { done: false, passive: true };
|
||||
}
|
||||
|
||||
function update(t, dt, context = {}) {
|
||||
if (!t || t.dead) return { done: true };
|
||||
const motionDt = Math.max(0.001, Math.min(0.05, Number(context.motionDt ?? dt) || 0.016));
|
||||
if (context.motionOnly && /^(sleep|eat|intimidate|ant_intimidate)$/.test(String(t.state || ""))) {
|
||||
return passivePhysicalMotion(t, motionDt, context);
|
||||
}
|
||||
const beforeX = Number.isFinite(t.x) ? t.x : 0;
|
||||
const beforeY = Number.isFinite(t.y) ? t.y : 0;
|
||||
const current = combinedVelocity(t);
|
||||
const before = Number.isFinite(t._lastPhysicalVelocityX) && Number.isFinite(t._lastPhysicalVelocityY)
|
||||
? { vx: t._lastPhysicalVelocityX, vy: t._lastPhysicalVelocityY }
|
||||
: current;
|
||||
if (global.TarinaiSunbathSystem?.updateOne) global.TarinaiSunbathSystem.updateOne(t, dt);
|
||||
else t.updateSunbath?.(dt);
|
||||
t.maintainTargetProgress(dt);
|
||||
t.move(dt);
|
||||
if (t.ammoCollisionKnockback) t.ammoCollisionKnockback(dt);
|
||||
applyVelocityShockDamage(t, before, dt);
|
||||
t.updateNestBoxPresence(dt);
|
||||
if (t.world.resolveFenceCollision) t.world.resolveFenceCollision(t);
|
||||
if (global.TarinaiSunbathSystem?.updateOne) global.TarinaiSunbathSystem.updateOne(t, motionDt);
|
||||
else t.updateSunbath?.(motionDt);
|
||||
t.maintainTargetProgress(motionDt);
|
||||
const prevMotionOnlyFlag = t._motionOnlyPhysicsPass;
|
||||
if (context.motionOnly) t._motionOnlyPhysicsPass = true;
|
||||
try {
|
||||
t.move(motionDt);
|
||||
} finally {
|
||||
if (context.motionOnly) t._motionOnlyPhysicsPass = prevMotionOnlyFlag;
|
||||
}
|
||||
if (t.ammoCollisionKnockback) t.ammoCollisionKnockback(motionDt);
|
||||
applyVelocityShockDamage(t, before, motionDt);
|
||||
t.updateNestBoxPresence(motionDt);
|
||||
|
||||
// t.move() normally calls world.moveTarinaiWithCollision(), which already
|
||||
// performs swept solid-obstacle resolution. Running resolveFenceCollision()
|
||||
// again for every creature made stationary/sleeping crowds pay the full
|
||||
// obstacle query cost twice per update. Keep the legacy fallback for worlds
|
||||
// without swept movement, and run a staggered low-frequency relax pass for
|
||||
// near-stationary bodies so old overlaps still self-heal.
|
||||
const world = t.world;
|
||||
if (world?.resolveFenceCollision) {
|
||||
const dx = (Number.isFinite(t.x) ? t.x : 0) - beforeX;
|
||||
const dy = (Number.isFinite(t.y) ? t.y : 0) - beforeY;
|
||||
const movedSq = dx * dx + dy * dy;
|
||||
if (!world.moveTarinaiWithCollision && movedSq > 0.25) {
|
||||
world.resolveFenceCollision(t);
|
||||
} else if (movedSq <= 0.25 && !world.isTarinaiHiddenInNestBox?.(t)) {
|
||||
const now = world.time || 0;
|
||||
if (now >= (t._nextSolidRelaxAt || 0)) {
|
||||
const jitter = typeof stableUnit === "function" ? stableUnit(t.id || t.familyKey || 0, "solid-relax") * 0.45 : Math.random() * 0.45;
|
||||
t._nextSolidRelaxAt = now + 0.85 + jitter;
|
||||
world.resolveFenceCollision(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { done: false };
|
||||
}
|
||||
|
||||
global.TarinaiMovementUpdateStep = Object.freeze({ update, applyVelocityShockDamage });
|
||||
global.TarinaiMovementUpdateStep = Object.freeze({ update, applyVelocityShockDamage, passivePhysicalMotion, markSleepExternalMotion, sleepExternalMotionActive });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ function bindUI() {
|
|||
window.bindGroundUI?.(world);
|
||||
renderEcologyCards();
|
||||
window.TarinaiSaveSystem?.bindSaveSystem?.();
|
||||
window.TarinaiFreezeSystem?.bindFreezeSystem?.();
|
||||
uiCache.toolButtons = Array.from(ui.toolPalette?.querySelectorAll(".tool") || []);
|
||||
function isActiveToolMode() {
|
||||
return Boolean(world?.tool && world.tool !== "observe");
|
||||
|
|
|
|||
|
|
@ -131,6 +131,17 @@
|
|||
return false;
|
||||
}
|
||||
|
||||
function syncGrabbedPhysicsItem(target, reason = "pinch-move") {
|
||||
if (!target || target.dead) return false;
|
||||
const pb = (typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem;
|
||||
if (!pb?.isBodyType?.(target.type)) return false;
|
||||
pb.syncPoseFromItem?.(target);
|
||||
pb.setScalar?.(target, "awakeUntil", Math.max(Number(pb.scalar?.(target, "awakeUntil", 0) || 0), (world.time || 0) + 0.65), reason);
|
||||
pb.markBodyChanged?.(target, reason);
|
||||
(typeof window !== "undefined" ? window : globalThis).TarinaiMechanicalSystem?.invalidateGeometry?.(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
function prepareGrabTarget(target) {
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
const lodged = target.currentLodgedPin?.() || (world.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === target.id);
|
||||
|
|
@ -168,6 +179,7 @@
|
|||
target.playerHeld = true;
|
||||
target._heldByPlayer = true;
|
||||
target._playerGrabStartedAt = world.time || 0;
|
||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-start");
|
||||
if (target.type === "duplicator") {
|
||||
target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.0);
|
||||
target._duplicatorCandidateKey = "";
|
||||
|
|
@ -206,11 +218,13 @@
|
|||
const movedX = (Number(target.x || 0) || 0) - beforeX;
|
||||
const movedY = (Number(target.y || 0) || 0) - beforeY;
|
||||
if (uiCache.grabKind === "item" && target.type === "reciprocator") {
|
||||
target.reciprocatorAnchorX = (Number(target.reciprocatorAnchorX || beforeX) || beforeX) + movedX;
|
||||
target.reciprocatorAnchorY = (Number(target.reciprocatorAnchorY || beforeY) || beforeY) + movedY;
|
||||
const pb = (typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem;
|
||||
pb?.setScalar?.(target, "railAnchorX", (Number(pb?.scalar?.(target, "railAnchorX", beforeX)) || beforeX) + movedX, "grab-move");
|
||||
pb?.setScalar?.(target, "railAnchorY", (Number(pb?.scalar?.(target, "railAnchorY", beforeY)) || beforeY) + movedY, "grab-move");
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
}
|
||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-move");
|
||||
target.vx = clamp(dx * 16, -160, 160);
|
||||
target.vy = clamp(dy * 16, -160, 160);
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
|
|
@ -245,6 +259,7 @@
|
|||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
}
|
||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release");
|
||||
target.playerHeld = false;
|
||||
target._heldByPlayer = false;
|
||||
target._lastPlayerReleasedAt = world.time || 0;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.00.00";
|
||||
const APP_BUILD = "lab-ground-wash-and-freezer-removal-v38";
|
||||
const APP_VERSION = "39.15.18";
|
||||
const APP_BUILD = "tarinai-nearby-constraint-reduce-v39-15-18";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ class World { constructor() {
|
|||
this.toolSizes = {};
|
||||
this.toolAngles = {};
|
||||
this.tarinai = [];
|
||||
this.frozenTarinai = [];
|
||||
this.items = [];
|
||||
this.ants = [];
|
||||
this.effects = [];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
const World = global.World;
|
||||
|
|
@ -29,7 +59,7 @@
|
|||
let nx = (ant.x - x) / d;
|
||||
let ny = (ant.y - y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
const a = deterministicAngle(this, "ant-push-overlap", ant, x, y);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
|
|
@ -58,13 +88,13 @@
|
|||
}
|
||||
this.blastZunchiFrom(x, y, blastRadius * 0.92, it.blastScale || 1);
|
||||
for (let i = 0; i < 34; i++) {
|
||||
const a = Math.PI * 2 * i / 34 + rand(-0.10, 0.10);
|
||||
const speed = rand(90, 260);
|
||||
this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * rand(2, 22), y + Math.sin(a) * rand(2, 22), {
|
||||
const a = Math.PI * 2 * i / 34 + deterministicRange(this, "firecracker-spark-angle", -0.10, 0.10, it, i);
|
||||
const speed = deterministicRange(this, "firecracker-spark-speed", 90, 260, it, i);
|
||||
this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * deterministicRange(this, "firecracker-spark-x", 2, 22, it, i), y + Math.sin(a) * deterministicRange(this, "firecracker-spark-y", 2, 22, it, i), {
|
||||
vx: Math.cos(a) * speed,
|
||||
vy: Math.sin(a) * speed,
|
||||
size: rand(8, i % 3 === 0 ? 24 : 20),
|
||||
life: rand(0.26, 0.72),
|
||||
size: deterministicRange(this, "firecracker-spark-size", 8, i % 3 === 0 ? 24 : 20, it, i),
|
||||
life: deterministicRange(this, "firecracker-spark-life", 0.26, 0.72, it, i),
|
||||
color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)",
|
||||
}));
|
||||
}
|
||||
|
|
@ -76,7 +106,7 @@
|
|||
if (t.enterPanic) t.enterPanic({ target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", fear: 1.2, surpriseTimer: 0.95, wake: true, cause: "firecracker_wakeup" });
|
||||
else { t.setActionState?.("panic", { target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", wake: true }); t.surpriseTimer = Math.max(t.surpriseTimer, 0.95); t.fearTimer = Math.max(t.fearTimer, 1.2); }
|
||||
}
|
||||
if (t.addStress) t.addStress(rand(8, 15), { threshold: 8 });
|
||||
if (t.addStress) t.addStress(deterministicRange(this, "firecracker-startle-stress", 8, 15, it, t), { threshold: 8 });
|
||||
t.fearTimer = Math.max(t.fearTimer, 0.58);
|
||||
const dx = t.x - x;
|
||||
const dy = t.y - y;
|
||||
|
|
@ -92,13 +122,13 @@
|
|||
let nx = dx / d;
|
||||
let ny = dy / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
const a = deterministicAngle(this, "drop-impact-overlap-normal", it, t);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
const distanceBoost = p * p * 0.55 + p * 0.45;
|
||||
const power = (260 + distanceBoost * 920) * rand(0.78, 1.24) * (it.blastScale || 1);
|
||||
this.applyImpulse(t, nx * power + rand(-95, 95) * (0.6 + p), ny * power + rand(-95, 95) * (0.6 + p), {
|
||||
const power = (260 + distanceBoost * 920) * deterministicRange(this, "firecracker-tarinai-power", 0.78, 1.24, it, t) * (it.blastScale || 1);
|
||||
this.applyImpulse(t, nx * power + deterministicRange(this, "firecracker-tarinai-jitter-x", -95, 95, it, t) * (0.6 + p), ny * power + deterministicRange(this, "firecracker-tarinai-jitter-y", -95, 95, it, t) * (0.6 + p), {
|
||||
panic: true,
|
||||
target: { x, y },
|
||||
fearTimer: 1.7 + p,
|
||||
|
|
@ -106,13 +136,13 @@
|
|||
});
|
||||
t.fallTimer = Math.max(t.fallTimer, 1.45 + p * 1.85);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
t.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1);
|
||||
t.fallDir = (dx >= 0 ? 1 : -1) * deterministicSigned(this, "firecracker-fall-dir", it, t);
|
||||
t.blastSpinTimer = Math.max(t.blastSpinTimer || 0, 1.35 + p * 0.70);
|
||||
t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer);
|
||||
this.spawnFallEffect(t.x, t.y + t.radius * 0.45, 1.1 + p);
|
||||
const explosionChance = window.TarinaiDiseaseRegistry?.infectionChance?.("disease_explosion", 0.15) ?? 0.15;
|
||||
if (!t.dead && p > 0.22 && Math.random() < (t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance)) t.infectExplosionDisease?.(it);
|
||||
if (!t.dead && Math.random() < 0.45) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
if (!t.dead && p > 0.22 && deterministicChance(this, "firecracker-explosion-disease", t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance, it, t)) t.infectExplosionDisease?.(it);
|
||||
if (!t.dead && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
}
|
||||
if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it);
|
||||
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u7af9", {
|
||||
|
|
@ -127,13 +157,13 @@
|
|||
if (d > blastRadius * 1.18) continue;
|
||||
const p = clamp(1 - d / (blastRadius * 1.18), 0, 1);
|
||||
if (d < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
const a = deterministicAngle(this, "firecracker-ball-overlap", it, ball);
|
||||
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||
}
|
||||
const blast = 430 + p * 960;
|
||||
this.applyImpulse(ball, dx / d * blast + rand(-80, 80), dy / d * blast + rand(-80, 80));
|
||||
this.applyImpulse(ball, dx / d * blast + deterministicRange(this, "firecracker-ball-jitter-x", -80, 80, it, ball), dy / d * blast + deterministicRange(this, "firecracker-ball-jitter-y", -80, 80, it, ball));
|
||||
const speed = Math.hypot(ball.vx || 0, ball.vy || 0);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38);
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "disease-explosion-ball-spin", -24, 24, t, ball), -38, 38);
|
||||
ball.lastPokedAt = this.time || 0;
|
||||
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" }));
|
||||
|
|
@ -157,13 +187,13 @@
|
|||
}
|
||||
this.blastZunchiFrom(x, y, blastRadius * 0.92, blastScale);
|
||||
for (let i = 0; i < 34; i++) {
|
||||
const a = Math.PI * 2 * i / 34 + rand(-0.10, 0.10);
|
||||
const speed = rand(90, 260) * blastScale;
|
||||
this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * rand(2, 22) * blastScale, y + Math.sin(a) * rand(2, 22) * blastScale, {
|
||||
const a = Math.PI * 2 * i / 34 + deterministicRange(this, "disease-explosion-spark-angle", -0.10, 0.10, t, i);
|
||||
const speed = deterministicRange(this, "disease-explosion-spark-speed", 90, 260, t, i) * blastScale;
|
||||
this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * deterministicRange(this, "disease-explosion-spark-x", 2, 22, t, i) * blastScale, y + Math.sin(a) * deterministicRange(this, "disease-explosion-spark-y", 2, 22, t, i) * blastScale, {
|
||||
vx: Math.cos(a) * speed,
|
||||
vy: Math.sin(a) * speed,
|
||||
size: rand(8, i % 3 === 0 ? 24 : 20) * blastScale,
|
||||
life: rand(0.26, 0.72),
|
||||
size: deterministicRange(this, "disease-explosion-spark-size", 8, i % 3 === 0 ? 24 : 20, t, i) * blastScale,
|
||||
life: deterministicRange(this, "disease-explosion-spark-life", 0.26, 0.72, t, i),
|
||||
color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)",
|
||||
}));
|
||||
}
|
||||
|
|
@ -179,13 +209,13 @@
|
|||
let nx = dx / d;
|
||||
let ny = dy / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
const a = deterministicAngle(this, "disease-explosion-overlap-normal", t, o);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
const distanceBoost = q * q * 0.55 + q * 0.45;
|
||||
const power = (260 + distanceBoost * 920) * rand(0.78, 1.24) * blastScale;
|
||||
this.applyImpulse(o, nx * power + rand(-95, 95) * (0.6 + q), ny * power + rand(-95, 95) * (0.6 + q), {
|
||||
const power = (260 + distanceBoost * 920) * deterministicRange(this, "disease-explosion-tarinai-power", 0.78, 1.24, t, o) * blastScale;
|
||||
this.applyImpulse(o, nx * power + deterministicRange(this, "disease-explosion-tarinai-jitter-x", -95, 95, t, o) * (0.6 + q), ny * power + deterministicRange(this, "disease-explosion-tarinai-jitter-y", -95, 95, t, o) * (0.6 + q), {
|
||||
panic: true,
|
||||
target: { x, y },
|
||||
fearTimer: 1.7 + q,
|
||||
|
|
@ -197,13 +227,13 @@
|
|||
else { o.fearTimer = Math.max(o.fearTimer || 0, 1.7 + q); o.setActionState?.("panic", { target: { x, y }, reason: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b", wake: true }); }
|
||||
o.fallTimer = Math.max(o.fallTimer || 0, 1.45 + q * 1.85);
|
||||
o.fallMax = Math.max(o.fallMax || 0, o.fallTimer);
|
||||
o.fallDir = (dx >= 0 ? 1 : -1) * (Math.random() < 0.5 ? 1 : -1);
|
||||
o.fallDir = (dx >= 0 ? 1 : -1) * deterministicSigned(this, "disease-explosion-fall-dir", t, o);
|
||||
o.blastSpinTimer = Math.max(o.blastSpinTimer || 0, 1.35 + q * 0.70);
|
||||
o.blastSpinMax = Math.max(o.blastSpinMax || 0, o.blastSpinTimer);
|
||||
this.spawnFallEffect(o.x, o.y + o.radius * 0.45, 1.1 + q);
|
||||
const explosionChance = window.TarinaiDiseaseRegistry?.infectionChance?.("disease_explosion", 0.15) ?? 0.15;
|
||||
if (!o.dead && q > 0.22 && Math.random() < (o.diseaseChance ? o.diseaseChance(explosionChance) : explosionChance)) o.infectExplosionDisease?.(t);
|
||||
if (!o.dead && Math.random() < 0.45) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
if (!o.dead && q > 0.22 && deterministicChance(this, "disease-explosion-spread", o.diseaseChance ? o.diseaseChance(explosionChance) : explosionChance, t, o)) o.infectExplosionDisease?.(t);
|
||||
if (!o.dead && deterministicChance(this, "disease-explosion-panic-bubble", 0.45, t, o)) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
}
|
||||
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", {
|
||||
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale,
|
||||
|
|
@ -216,12 +246,12 @@
|
|||
if (d > blastRadius * 1.18) continue;
|
||||
const q = clamp(1 - d / (blastRadius * 1.18), 0, 1);
|
||||
if (d < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
const a = deterministicAngle(this, "disease-explosion-ball-overlap", t, ball);
|
||||
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
||||
}
|
||||
const blast = 430 + q * 960;
|
||||
this.applyImpulse(ball, dx / d * blast * blastScale + rand(-80, 80), dy / d * blast * blastScale + rand(-80, 80));
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + rand(-24, 24), -38, 38);
|
||||
this.applyImpulse(ball, dx / d * blast * blastScale + deterministicRange(this, "disease-explosion-ball-jitter-x", -80, 80, t, ball), dy / d * blast * blastScale + deterministicRange(this, "disease-explosion-ball-jitter-y", -80, 80, t, ball));
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "disease-explosion-ball-spin", -24, 24, t, ball), -38, 38);
|
||||
ball.lastPokedAt = this.time || 0;
|
||||
ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5);
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" }));
|
||||
|
|
@ -271,13 +301,13 @@
|
|||
const d = distXY(t.x, t.y, x, y);
|
||||
if (d > radius + t.radius) continue;
|
||||
const p = clamp(1 - d / (radius + t.radius), 0, 1);
|
||||
t.vx += nx * (95 + 165 * p) + rand(-10, 10);
|
||||
t.vy += ny * (95 + 165 * p) + rand(-10, 10);
|
||||
t.vx += nx * (95 + 165 * p) + deterministicRange(this, "water-hose-jitter-x", -10, 10, t, x, y);
|
||||
t.vy += ny * (95 + 165 * p) + deterministicRange(this, "water-hose-jitter-y", -10, 10, t, x, y);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24);
|
||||
t.applyWaterEffect?.(dt * (1.0 + p * 1.8), "hose");
|
||||
}
|
||||
if (Math.random() < 0.72) {
|
||||
this.effects.push(new Effect("ring", x + rand(-18, 18), y + rand(-12, 12), { size: rand(8, 22), life: rand(0.18, 0.32), color: "rgba(128,204,238,0.60)" }));
|
||||
if (deterministicChance(this, "water-hose-ring", 0.72, x, y)) {
|
||||
this.effects.push(new Effect("ring", x + deterministicRange(this, "water-hose-ring-x", -18, 18, x, y), y + deterministicRange(this, "water-hose-ring-y", -12, 12, x, y), { size: deterministicRange(this, "water-hose-ring-size", 8, 22, x, y), life: deterministicRange(this, "water-hose-ring-life", 0.18, 0.32, x, y), color: "rgba(128,204,238,0.60)" }));
|
||||
}
|
||||
if (cleaned > 0.25) {
|
||||
this.drawListDirty = true;
|
||||
|
|
@ -293,11 +323,11 @@
|
|||
const sx = Number.isFinite(source?.x) ? source.x : (pin?.x || this.w * 0.5);
|
||||
const sy = Number.isFinite(source?.y) ? source.y : (pin?.y || this.h * 0.5);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const a = -Math.PI * 0.15 + rand(-0.92, 0.92) + (i / Math.max(1, n - 1) - 0.5) * 1.2;
|
||||
const it = new Item("zunchi", clamp(sx + rand(-8, 8), 40, this.w - 40), clamp(sy + rand(-8, 8), 40, this.h - 40));
|
||||
const a = -Math.PI * 0.15 + deterministicRange(this, "zunchi-burst-angle", -0.92, 0.92, source, pin, i) + (i / Math.max(1, n - 1) - 0.5) * 1.2;
|
||||
const it = new Item("zunchi", clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40), clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40));
|
||||
it.amount = 240;
|
||||
it.vx = Math.cos(a) * rand(260, 560) + rand(-40, 40);
|
||||
it.vy = Math.sin(a) * rand(200, 480) - rand(30, 160);
|
||||
it.vx = Math.cos(a) * deterministicRange(this, "zunchi-burst-speed-x", 260, 560, source, pin, i) + deterministicRange(this, "zunchi-burst-jitter-x", -40, 40, source, pin, i);
|
||||
it.vy = Math.sin(a) * deterministicRange(this, "zunchi-burst-speed-y", 200, 480, source, pin, i) - deterministicRange(this, "zunchi-burst-up", 30, 160, source, pin, i);
|
||||
it.stage = "fresh";
|
||||
it.burstFromOshibyo = true;
|
||||
this.addItem?.(it, "zunchi-burst") || this.items.push(it);
|
||||
|
|
@ -325,9 +355,9 @@
|
|||
if (text === "z") text = "Zzz...";
|
||||
audio.bubble(text);
|
||||
this.effects.push(new Effect("bubble", x, y, {
|
||||
vx: rand(-2, 2),
|
||||
vy: rand(-5, -2),
|
||||
life: rand(2.2, 3.2),
|
||||
vx: deterministicRange(this, "bubble-vx", -2, 2, x, y, text),
|
||||
vy: deterministicRange(this, "bubble-vy", -5, -2, x, y, text),
|
||||
life: deterministicRange(this, "bubble-life", 2.2, 3.2, x, y, text),
|
||||
size: 8,
|
||||
text,
|
||||
color,
|
||||
|
|
@ -337,45 +367,45 @@
|
|||
|
||||
spawnHeadbuttEffect(x, y) {
|
||||
this.effects.push(new Effect("fight", x, y, {
|
||||
size: rand(10, 16),
|
||||
life: rand(0.20, 0.34),
|
||||
size: deterministicRange(this, "headbutt-effect-size", 10, 16, x, y),
|
||||
life: deterministicRange(this, "headbutt-effect-life", 0.20, 0.34, x, y),
|
||||
color: "rgba(105, 62, 34, 0.86)",
|
||||
}));
|
||||
if (Math.random() < 0.5) audio.fight();
|
||||
if (deterministicChance(this, "headbutt-audio", 0.5, x, y)) audio.fight();
|
||||
},
|
||||
|
||||
spawnFallEffect(x, y, scale = 1) {
|
||||
if ((this.effectCounts.fall || 0) > 18) return;
|
||||
this.effects.push(new Effect("fall", x, y, {
|
||||
vx: rand(-12, 12),
|
||||
vy: rand(-4, 3),
|
||||
size: rand(20, 32) * scale,
|
||||
life: rand(0.48, 0.72),
|
||||
vx: deterministicRange(this, "fall-effect-vx", -12, 12, x, y),
|
||||
vy: deterministicRange(this, "fall-effect-vy", -4, 3, x, y),
|
||||
size: deterministicRange(this, "fall-effect-size", 20, 32, x, y) * scale,
|
||||
life: deterministicRange(this, "fall-effect-life", 0.48, 0.72, x, y),
|
||||
color: "rgba(154, 124, 80, 0.58)",
|
||||
}));
|
||||
},
|
||||
|
||||
spawnEatEffect(x, y, color = "#f1dfb7") {
|
||||
if (Math.random() < 0.70) {
|
||||
this.effects.push(new Effect("eat", x + rand(-4, 4), y + rand(-4, 4), {
|
||||
vx: rand(-10, 10),
|
||||
vy: rand(-18, -3),
|
||||
size: rand(2.2, 4.2),
|
||||
life: rand(0.18, 0.32),
|
||||
if (deterministicChance(this, "eat-effect", 0.70, x, y, color)) {
|
||||
this.effects.push(new Effect("eat", x + deterministicRange(this, "eat-effect-x", -4, 4, x, y, color), y + deterministicRange(this, "eat-effect-y", -4, 4, x, y, color), {
|
||||
vx: deterministicRange(this, "eat-effect-vx", -10, 10, x, y, color),
|
||||
vy: deterministicRange(this, "eat-effect-vy", -18, -3, x, y, color),
|
||||
size: deterministicRange(this, "eat-effect-size", 2.2, 4.2, x, y, color),
|
||||
life: deterministicRange(this, "eat-effect-life", 0.18, 0.32, x, y, color),
|
||||
color,
|
||||
}));
|
||||
}
|
||||
if (Math.random() < 0.08) {
|
||||
if (deterministicChance(this, "eat-ring", 0.08, x, y, color)) {
|
||||
this.effects.push(new Effect("ring", x, y, { size: 5, life: 0.20, color }));
|
||||
}
|
||||
},
|
||||
|
||||
spawnBleedEffect(x, y) {
|
||||
this.effects.push(new Effect("bleed", x + rand(-3, 3), y + rand(-2, 3), {
|
||||
vx: rand(-8, 8),
|
||||
vy: rand(3, 14),
|
||||
size: rand(2.0, 3.8),
|
||||
life: rand(0.34, 0.62),
|
||||
this.effects.push(new Effect("bleed", x + deterministicRange(this, "bleed-effect-x", -3, 3, x, y), y + deterministicRange(this, "bleed-effect-y", -2, 3, x, y), {
|
||||
vx: deterministicRange(this, "bleed-effect-vx", -8, 8, x, y),
|
||||
vy: deterministicRange(this, "bleed-effect-vy", 3, 14, x, y),
|
||||
size: deterministicRange(this, "bleed-effect-size", 2.0, 3.8, x, y),
|
||||
life: deterministicRange(this, "bleed-effect-life", 0.34, 0.62, x, y),
|
||||
color: "rgba(80, 172, 55, 0.78)",
|
||||
}));
|
||||
},
|
||||
|
|
@ -419,6 +449,17 @@
|
|||
applyImpulse(entity, vx = 0, vy = 0, options = {}) {
|
||||
if (!entity || entity.dead) return false;
|
||||
if (!Number.isFinite(vx) || !Number.isFinite(vy)) return false;
|
||||
const impulseMag = Math.hypot(vx, vy);
|
||||
if (entity instanceof Tarinai && (entity.birthRitualTimer || 0) > 0.04) {
|
||||
this.cancelBirthRitualOnForce?.(entity, impulseMag, {
|
||||
reason: options.birthCancelReason || options.thought || "強い衝撃で繁殖が中断された",
|
||||
target: options.target || null,
|
||||
fear: options.fearTimer ?? 1.0,
|
||||
cause: options.cause || "impulse",
|
||||
panic: options.panic !== false,
|
||||
silentLog: !!options.silentBirthCancel,
|
||||
});
|
||||
}
|
||||
if (entity instanceof Tarinai || "impulseVx" in entity || "impulseVy" in entity) {
|
||||
entity.impulseVx = (Number.isFinite(entity.impulseVx) ? entity.impulseVx : 0) + vx;
|
||||
entity.impulseVy = (Number.isFinite(entity.impulseVy) ? entity.impulseVy : 0) + vy;
|
||||
|
|
@ -435,8 +476,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("落ちてきた");
|
||||
if (target instanceof Tarinai && (target.birthRitualTimer || 0) > 0.04 && physicalHit && amount >= (options.birthCancelDamageThreshold ?? 8)) {
|
||||
this.cancelBirthRitualOnForce?.(target, Math.max(180, amount * 18), {
|
||||
reason: "強い衝撃で繁殖が中断された",
|
||||
target: options.source || null,
|
||||
fear: 1.0,
|
||||
cause: "impact_damage",
|
||||
panic: true,
|
||||
silentLog: !!options.silentBirthCancel,
|
||||
});
|
||||
}
|
||||
this.emit?.(String(normalized).includes("\u55a7\u5629") ? "fight:hit" : "impact:hit", { target, amount, cause: normalized, source: options.source || null });
|
||||
if (String(normalized).includes("衝突") || String(normalized).includes("collision") || String(cause || "").includes("急激な速度変化")) {
|
||||
if (physicalHit) {
|
||||
target.lastPhysicalCollisionDamageAt = this.time || 0;
|
||||
}
|
||||
if (target.damage) target.damage(amount, normalized);
|
||||
|
|
@ -454,21 +506,21 @@
|
|||
const damageRisk = clamp((damage || 0) / Math.max(2.5, 9 + (victim.energy || 0) * 0.045), 0, 1.4);
|
||||
const stressRisk = clamp(((victim.stress || 0) - 70) / 70, 0, 0.8);
|
||||
const chance = clamp(0.025 + damageRisk * 0.23 + healthRisk * 0.34 + stressRisk * 0.10 - aggressionHold * 0.12, 0.015, 0.78);
|
||||
if (Math.random() >= chance) return false;
|
||||
if (!deterministicChance(this, "fight-defeat", chance, victim, attacker, damage)) return false;
|
||||
victim.defeatedById = attacker.id;
|
||||
victim.fightWinnerId = attacker.id;
|
||||
attacker.fightWinnerId = attacker.id;
|
||||
attacker.defeatedById = null;
|
||||
victim.fightTimer = Math.min(victim.fightTimer || 0, rand(0.20, 0.42));
|
||||
attacker.fightTimer = Math.min(attacker.fightTimer || 0, rand(0.20, 0.42));
|
||||
victim.fightTimer = Math.min(victim.fightTimer || 0, deterministicRange(this, "fight-defeat-victim-timer", 0.20, 0.42, victim, attacker));
|
||||
attacker.fightTimer = Math.min(attacker.fightTimer || 0, deterministicRange(this, "fight-defeat-attacker-timer", 0.20, 0.42, victim, attacker));
|
||||
this.emit?.("fight:lost", { winner: attacker, loser: victim, damage });
|
||||
if (victim.enterPanic) victim.enterPanic({ target: attacker, threat: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.1, wake: true, cause: "fight_lost" });
|
||||
else { victim.setActionState?.("panic", { target: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1)); }
|
||||
const dx = victim.x - attacker.x;
|
||||
const dy = victim.y - attacker.y;
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
victim.vx += dx / d * rand(38, 76);
|
||||
victim.vy += dy / d * rand(22, 56);
|
||||
victim.vx += dx / d * deterministicRange(this, "fight-defeat-knockback-x", 38, 76, victim, attacker);
|
||||
victim.vy += dy / d * deterministicRange(this, "fight-defeat-knockback-y", 22, 56, victim, attacker);
|
||||
this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -493,7 +545,7 @@
|
|||
this.emit?.("fight:ended", { winner, loser });
|
||||
this.markFightPairCooldown?.(winner, loser, CONFIG.fightCooldown + 8);
|
||||
for (const t of [winner, loser]) {
|
||||
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + rand(4, 8));
|
||||
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this, "fight-outcome-cooldown", 4, 8, t, winner, loser));
|
||||
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
|
||||
}
|
||||
winner.adjustPersonality?.("aggression", 0.018, "after winning fights.");
|
||||
|
|
@ -503,7 +555,7 @@
|
|||
loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear);
|
||||
this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
winner.affection = clamp(winner.affection + 0.8, 0, 80);
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(winner, { safety: -rand(12, 22), fulfill: -rand(8, 16) });
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(winner, { safety: -deterministicRange(this, "fight-win-safety-relief", 12, 22, winner, loser), fulfill: -deterministicRange(this, "fight-win-fulfill-relief", 8, 16, winner, loser) });
|
||||
if (this.relationNotice(winner.id, loser.id, "fight-result", 8)) {
|
||||
this.log(`${loser.name}\u306f${winner.name}\u306b\u8ca0\u3051\u3001\u305d\u306e\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "fight", { participants: [loser, winner] });
|
||||
}
|
||||
|
|
@ -545,7 +597,7 @@
|
|||
let nx = (t.x - it.x) / d;
|
||||
let ny = (t.y - it.y) / d;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const a = rand(0, Math.PI * 2);
|
||||
const a = deterministicAngle(this, "drop-impact-overlap-normal", it, t);
|
||||
nx = Math.cos(a);
|
||||
ny = Math.sin(a);
|
||||
}
|
||||
|
|
@ -574,7 +626,7 @@
|
|||
if (isGenkotsu) {
|
||||
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75);
|
||||
for (let r = 0; r < 4; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 34, life: 0.34 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.72)" : "rgba(74,57,42,0.42)" }));
|
||||
for (let i = 0; i < 12; i++) this.effects.push(new Effect("fight", it.x + rand(-38, 38), it.y + rand(-22, 28), { size: rand(12, 22), life: rand(0.25, 0.48), color: "rgba(255,205,46,0.82)" }));
|
||||
for (let i = 0; i < 12; i++) this.effects.push(new Effect("fight", it.x + deterministicRange(this, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(this, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(this, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(this, "genkotsu-impact-effect-life", 0.25, 0.48, it, i), color: "rgba(255,205,46,0.82)" }));
|
||||
} else {
|
||||
this.effects.push(new Effect("ring", it.x, it.y, { size: isStone ? 32 : 18, life: 0.34, color: isStone ? "rgba(128,96,58,0.76)" : "rgba(174,128,70,0.52)" }));
|
||||
}
|
||||
|
|
@ -609,7 +661,7 @@
|
|||
});
|
||||
for (const t of arr.slice(5)) {
|
||||
t.goIdle("\u30dc\u30fc\u30eb\u304c\u6df7\u307f\u5408\u3063\u3066\u3044\u308b\u306e\u3067\u773a\u3081\u3066\u3044\u308b");
|
||||
t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + rand(-0.45, 0.45);
|
||||
t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + deterministicRange(this, "ball-chaser-release-angle", -0.45, 0.45, t, ball);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -621,7 +673,7 @@
|
|||
let dy = ball.y - y;
|
||||
let d = Math.hypot(dx, dy);
|
||||
if (d < 0.001) {
|
||||
const a = (ball.lastPokeAngle ?? rand(0, Math.PI * 2)) + rand(-0.55, 0.55);
|
||||
const a = (ball.lastPokeAngle ?? deterministicAngle(this, "ball-poke-overlap-base", ball, x, y)) + deterministicRange(this, "ball-poke-overlap-jitter", -0.55, 0.55, ball, x, y);
|
||||
dx = Math.cos(a);
|
||||
dy = Math.sin(a);
|
||||
d = 1;
|
||||
|
|
@ -703,7 +755,7 @@
|
|||
const rvy = (b.vy || 0) - (a.vy || 0);
|
||||
const rs = Math.hypot(rvx, rvy);
|
||||
if (rs > 0.001) { dx = rvx / rs; dy = rvy / rs; d = 1; }
|
||||
else { const ang = rand(0, Math.PI * 2); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; }
|
||||
else { const ang = deterministicAngle(this, "ball-ball-overlap-normal", a, b); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; }
|
||||
}
|
||||
const nx = dx / d;
|
||||
const ny = dy / d;
|
||||
|
|
@ -770,7 +822,7 @@
|
|||
ny = (ball.vy || 0) / ballSpeed;
|
||||
} else {
|
||||
nx = t.facingDir ? t.facingDir() : 1;
|
||||
ny = rand(-0.22, 0.22);
|
||||
ny = deterministicRange(this, "ball-interaction-fallback-ny", -0.22, 0.22, ball, t);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -834,7 +886,7 @@
|
|||
}
|
||||
t.goodMode = playIntent || playful ? "smile" : t.goodMode;
|
||||
if (playIntent && now > (t.nextPlayBubbleAt || 0)) {
|
||||
t.nextPlayBubbleAt = now + rand(2.0, 3.8);
|
||||
t.nextPlayBubbleAt = now + deterministicRange(this, "ball-play-bubble-delay", 2.0, 3.8, ball, t);
|
||||
this.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)");
|
||||
}
|
||||
if (playful && this.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) this.log(`${t.name}\u306f\u30dc\u30fc\u30eb\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] });
|
||||
|
|
@ -847,20 +899,26 @@
|
|||
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
||||
const now = this.time || 0;
|
||||
if (!this.tarinaiCollisionMemo) this.tarinaiCollisionMemo = new Map();
|
||||
for (const a of this.tarinai || []) {
|
||||
if (!a || a.dead || this.isTarinaiHiddenInNestBox(a)) continue;
|
||||
const live = Array.from(this.tarinai || [])
|
||||
.filter(t => t && !t.dead && !this.isTarinaiHiddenInNestBox(t))
|
||||
.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
||||
const candidates = [];
|
||||
const maxCandidates = Math.max(24, Math.min(320, live.length * 6));
|
||||
for (let i = 0; i < live.length; i += 1) {
|
||||
const a = live[i];
|
||||
const avx = a.vx || 0;
|
||||
const avy = a.vy || 0;
|
||||
const aSpeed = Math.hypot(avx, avy);
|
||||
if (aSpeed < threshold) continue;
|
||||
const px = Number.isFinite(a.prevX) ? a.prevX : a.x;
|
||||
const py = Number.isFinite(a.prevY) ? a.prevY : a.y;
|
||||
const sx = a.x - px;
|
||||
const sy = a.y - py;
|
||||
const segLenSq = sx * sx + sy * sy;
|
||||
const range = (a.radius || 22) * 2.5 + aSpeed * Math.max(0.016, dt || 0.016) + 64;
|
||||
for (const b of this.nearbyTarinai(a.x, a.y, range)) {
|
||||
if (!b || b === a || b.dead || this.isTarinaiHiddenInNestBox(b)) continue;
|
||||
const ax0 = Number.isFinite(a.prevX) ? a.prevX : a.x;
|
||||
const ay0 = Number.isFinite(a.prevY) ? a.prevY : a.y;
|
||||
const ax1 = Number.isFinite(a.x) ? a.x : ax0;
|
||||
const ay1 = Number.isFinite(a.y) ? a.y : ay0;
|
||||
const asx = ax1 - ax0;
|
||||
const asy = ay1 - ay0;
|
||||
for (let j = 0; j < live.length; j += 1) {
|
||||
const b = live[j];
|
||||
if (!b || b === a) continue;
|
||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
const bvx = b.vx || 0;
|
||||
|
|
@ -869,54 +927,93 @@
|
|||
const relVy = avy - bvy;
|
||||
const relSpeed = Math.hypot(relVx, relVy);
|
||||
if (relSpeed < threshold) continue;
|
||||
let hitX = a.x;
|
||||
let hitY = a.y;
|
||||
if (segLenSq > 1) {
|
||||
const u = clamp(((b.x - px) * sx + (b.y - py) * sy) / segLenSq, 0, 1);
|
||||
hitX = px + sx * u;
|
||||
hitY = py + sy * u;
|
||||
}
|
||||
const d = Math.max(0.001, distXY(hitX, hitY, b.x, b.y));
|
||||
const bx0 = Number.isFinite(b.prevX) ? b.prevX : b.x;
|
||||
const by0 = Number.isFinite(b.prevY) ? b.prevY : b.y;
|
||||
const bx1 = Number.isFinite(b.x) ? b.x : bx0;
|
||||
const by1 = Number.isFinite(b.y) ? b.y : by0;
|
||||
const bsx = bx1 - bx0;
|
||||
const bsy = by1 - by0;
|
||||
const rx0 = ax0 - bx0;
|
||||
const ry0 = ay0 - by0;
|
||||
const rvx = asx - bsx;
|
||||
const rvy = asy - bsy;
|
||||
const relMoveSq = rvx * rvx + rvy * rvy;
|
||||
const u = relMoveSq > 0.001 ? clamp(-(rx0 * rvx + ry0 * rvy) / relMoveSq, 0, 1) : 1;
|
||||
const cx = rx0 + rvx * u;
|
||||
const cy = ry0 + rvy * u;
|
||||
const d = Math.max(0.001, Math.hypot(cx, cy));
|
||||
const hitDistance = (a.radius || 22) * 0.74 + (b.radius || 22) * 0.74;
|
||||
if (d > hitDistance) continue;
|
||||
this.tarinaiCollisionMemo.set(key, now);
|
||||
const impactor = aSpeed >= Math.hypot(bvx, bvy) ? a : b;
|
||||
const target = impactor === a ? b : a;
|
||||
const ivx = impactor.vx || 0;
|
||||
const ivy = impactor.vy || 0;
|
||||
const impactSpeed = Math.hypot(ivx, ivy) || relSpeed || 1;
|
||||
let nx = impactSpeed > 0.001 ? ivx / impactSpeed : (target.x - impactor.x) / Math.max(1, distXY(target.x, target.y, impactor.x, impactor.y));
|
||||
let ny = impactSpeed > 0.001 ? ivy / impactSpeed : (target.y - impactor.y) / Math.max(1, distXY(target.x, target.y, impactor.x, impactor.y));
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const aa = rand(0, Math.PI * 2);
|
||||
nx = Math.cos(aa);
|
||||
ny = Math.sin(aa);
|
||||
}
|
||||
const damage = physicalDamageFromImpactSpeed(relSpeed, { min: 1.2, max: 22, scale: 26 });
|
||||
this.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, {
|
||||
panic: true,
|
||||
target: { x: impactor.x, y: impactor.y },
|
||||
fearTimer: 0.55,
|
||||
thought: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b",
|
||||
});
|
||||
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, "\u885d\u7a81");
|
||||
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81");
|
||||
for (const t of [target, impactor]) {
|
||||
if (!t || t.dead) continue;
|
||||
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
t.fallDir = nx >= 0 ? 1 : -1;
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
|
||||
}
|
||||
this.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" }));
|
||||
if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] });
|
||||
const score = (1 - u) * 4 + Math.max(0, hitDistance - d) * 0.12 + Math.min(5, relSpeed / 120);
|
||||
candidates.push({ a, b, key, relSpeed, u, cx, cy, d, hitDistance, score });
|
||||
if (candidates.length >= maxCandidates) break;
|
||||
}
|
||||
if (candidates.length >= maxCandidates) break;
|
||||
}
|
||||
candidates.sort((p, q) => (p.u - q.u) || (q.score - p.score) || String(p.key).localeCompare(String(q.key)));
|
||||
const maxImpacts = Math.max(8, Math.min(64, Number(this.maxTarinaiCollisionImpacts || 32) || 32));
|
||||
let solved = 0;
|
||||
for (const c of candidates) {
|
||||
if (solved >= maxImpacts) break;
|
||||
const { a, b, key, relSpeed, hitDistance } = c;
|
||||
if (!a || !b || a.dead || b.dead) continue;
|
||||
if (now - (this.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
this.tarinaiCollisionMemo.set(key, now);
|
||||
solved += 1;
|
||||
const as = Math.hypot(a.vx || 0, a.vy || 0);
|
||||
const bs = Math.hypot(b.vx || 0, b.vy || 0);
|
||||
const impactor = as >= bs ? a : b;
|
||||
const target = impactor === a ? b : a;
|
||||
const ivx = impactor.vx || 0;
|
||||
const ivy = impactor.vy || 0;
|
||||
const impactSpeed = Math.hypot(ivx, ivy) || relSpeed || 1;
|
||||
let nx = impactSpeed > 0.001 ? ivx / impactSpeed : 0;
|
||||
let ny = impactSpeed > 0.001 ? ivy / impactSpeed : 0;
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
||||
const dx = target.x - impactor.x;
|
||||
const dy = target.y - impactor.y;
|
||||
const dd = Math.hypot(dx, dy);
|
||||
if (dd > 0.001) { nx = dx / dd; ny = dy / dd; }
|
||||
else {
|
||||
const angle = (typeof stableUnit === "function" ? stableUnit(key, "tarinai-collision-normal") : 0.5) * Math.PI * 2;
|
||||
nx = Math.cos(angle);
|
||||
ny = Math.sin(angle);
|
||||
}
|
||||
}
|
||||
const overlap = Math.max(0, hitDistance - Math.hypot((a.x || 0) - (b.x || 0), (a.y || 0) - (b.y || 0)));
|
||||
if (overlap > 0.5) {
|
||||
const sep = Math.min(18, overlap * 0.45 + 0.8);
|
||||
impactor.x = clamp((impactor.x || 0) - nx * sep * 0.45, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
impactor.y = clamp((impactor.y || 0) - ny * sep * 0.45, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
target.x = clamp((target.x || 0) + nx * sep * 0.55, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
|
||||
target.y = clamp((target.y || 0) + ny * sep * 0.55, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
|
||||
this.markSpatialDirty?.("tarinai-highspeed-separation");
|
||||
}
|
||||
const damage = physicalDamageFromImpactSpeed(relSpeed, { min: 1.2, max: 22, scale: 26 });
|
||||
this.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, {
|
||||
panic: true,
|
||||
target: { x: impactor.x, y: impactor.y },
|
||||
fearTimer: 0.55,
|
||||
thought: "高速でぶつかって混乱している",
|
||||
});
|
||||
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, "衝突");
|
||||
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" });
|
||||
else t.setActionState?.("panic", { target: { x: impactor.x, y: impactor.y }, reason: "高速でぶつかって混乱している", wake: true });
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.9 + damage * 0.035);
|
||||
t.fallTimer = Math.max(t.fallTimer || 0, 0.36 + damage * 0.025);
|
||||
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
||||
t.fallDir = nx >= 0 ? 1 : -1;
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 0.55);
|
||||
}
|
||||
this.effects.push(new Effect("ring", (a.x + b.x) * 0.5, (a.y + b.y) * 0.5, { size: Math.max(16, hitDistance * 0.45), life: 0.20, color: "rgba(210,75,65,0.40)" }));
|
||||
if (this.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) this.log(`${target.name}は高速の${impactor.name}に衝突して弾き飛ばされた。`, "accident", { participants: [target, impactor] });
|
||||
}
|
||||
if (solved && this.spatialDirty) this.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
|
||||
}
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -28,10 +28,60 @@
|
|||
return { left: cx - ex, right: cx + ex, top: cy - ey, bottom: cy + ey, cos: c, sin: s };
|
||||
}
|
||||
|
||||
function obstacleQueryDistanceLocal(x, y, it) {
|
||||
if (!it) return Infinity;
|
||||
const type = String(it.type || "");
|
||||
let reach = Math.max(32, Number(it.r || it.radius || 32) || 32);
|
||||
if (type === "fence" || type === "fence_v" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") reach = Math.max(120, reach * 3.9);
|
||||
else if (type === "nest_box") reach = Math.max(92, reach * 1.9);
|
||||
else reach = global.TarinaiMechanicalSystem?.reach?.(it) || Math.max(60, reach * 1.6);
|
||||
return Math.max(0, distXY(x, y, Number(it.x) || 0, Number(it.y) || 0) - reach);
|
||||
}
|
||||
|
||||
function visitNearestObstaclesLocal(x, y, candidates, limit, visitor, opts = {}) {
|
||||
if (!candidates || typeof visitor !== "function") return 0;
|
||||
const source = candidates;
|
||||
const n = Number(source.length || 0) || 0;
|
||||
if (n <= 0) return 0;
|
||||
const keepMax = Math.max(1, Math.min(n, Math.max(limit + 6, limit * 3)));
|
||||
const kept = [];
|
||||
const dists = [];
|
||||
const include = opts.include || null;
|
||||
const exclude = opts.exclude || null;
|
||||
const stamp = opts.stamp;
|
||||
|
||||
// Hot path: candidate counts are usually small. Keep a bounded sorted
|
||||
// prefix without allocating intermediate Array.from()/sort() output. For
|
||||
// large candidate sets, insertion is limited to the top keepMax entries.
|
||||
for (let i = 0; i < n; i++) {
|
||||
const it = source[i];
|
||||
if (!it || it === exclude || it.dead || it._obstacleRectQueryStamp === stamp) continue;
|
||||
if (include && !include(it)) continue;
|
||||
const d = obstacleQueryDistanceLocal(x, y, it);
|
||||
if (kept.length >= keepMax && d >= dists[dists.length - 1]) continue;
|
||||
let pos = kept.length;
|
||||
while (pos > 0 && d < dists[pos - 1]) pos -= 1;
|
||||
if (pos >= keepMax) continue;
|
||||
kept.splice(pos, 0, it);
|
||||
dists.splice(pos, 0, d);
|
||||
if (kept.length > keepMax) {
|
||||
kept.length = keepMax;
|
||||
dists.length = keepMax;
|
||||
}
|
||||
}
|
||||
|
||||
let visited = 0;
|
||||
for (let i = 0; i < kept.length; i++) {
|
||||
visited += 1;
|
||||
if (visitor(kept[i]) === true) break;
|
||||
}
|
||||
return visited;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function sanitizeRotatorSegmentsLocal(item) {
|
||||
const raw = Array.isArray(item?.rotatorSegments) ? item.rotatorSegments : [];
|
||||
const raw = global.TarinaiPhysicsBodySystem?.segments?.(item) || [];
|
||||
const out = [];
|
||||
const clampCoord = (v) => Math.max(-420, Math.min(420, Number(v) || 0));
|
||||
for (const seg of raw) {
|
||||
|
|
@ -53,7 +103,7 @@
|
|||
for (const seg of sanitizeRotatorSegmentsLocal(item)) {
|
||||
maxD = Math.max(maxD, Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3]));
|
||||
}
|
||||
return Math.min(460, maxD + Math.max(8, Number(item?.rotatorThickness || 12) || 12) + 8);
|
||||
return Math.min(460, maxD + Math.max(8, global.TarinaiPhysicsBodySystem?.scalar?.(item, "thickness", 12) || 12) + 8);
|
||||
}
|
||||
|
||||
function rotatorWorldSegmentsLocal(item) {
|
||||
|
|
@ -74,7 +124,7 @@
|
|||
const len = Math.max(4, Math.hypot(x2 - x1, y2 - y1));
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const halfW = len / 2;
|
||||
const halfH = Math.max(4, Math.min(34, Number(item?.rotatorThickness || 12) || 12)) / 2;
|
||||
const halfH = Math.max(4, Math.min(34, global.TarinaiPhysicsBodySystem?.scalar?.(item, "thickness", 12) || 12)) / 2;
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const aabb = orientedRectAabb(cx, cy, halfW, halfH, angle);
|
||||
|
|
@ -82,7 +132,7 @@
|
|||
left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom,
|
||||
cx, cy, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
|
||||
type: "rotator", item, rotator: true, restitution: 0.72,
|
||||
angularVelocity: (item?.rotatorPowered === false ? 0 : (Number(item?.rotatorSpeed || 0) || 0)) + (Number(item?.rotatorAngularVelocity || 0) || 0),
|
||||
angularVelocity: (global.TarinaiPhysicsBodySystem?.scalar?.(item, "motorOn", true) === false ? 0 : (Number(global.TarinaiPhysicsBodySystem?.scalar?.(item, "motorSpeed", 0) || 0) || 0)) + (Number(global.TarinaiPhysicsBodySystem?.scalar?.(item, "spin", 0) || 0) || 0),
|
||||
centerX: Number(item?.x) || 0, centerY: Number(item?.y) || 0,
|
||||
};
|
||||
}
|
||||
|
|
@ -96,7 +146,7 @@
|
|||
|
||||
function reciprocatorAxisLocal(item) {
|
||||
const fallback = angleForItemLocal(item);
|
||||
const angle = typeof normalizedItemAngle === "function" ? normalizedItemAngle(item?.reciprocatorAxisAngle, fallback) : (Number.isFinite(Number(item?.reciprocatorAxisAngle)) ? Number(item.reciprocatorAxisAngle) : fallback);
|
||||
const angle = typeof normalizedItemAngle === "function" ? normalizedItemAngle(global.TarinaiPhysicsBodySystem?.scalar?.(item, "railAxis", fallback), fallback) : (global.TarinaiPhysicsBodySystem?.scalar?.(item, "railAxis", fallback) || fallback);
|
||||
return { x: Math.cos(angle), y: Math.sin(angle), angle };
|
||||
}
|
||||
|
||||
|
|
@ -104,16 +154,16 @@
|
|||
const len = Math.max(4, Math.hypot(x2 - x1, y2 - y1));
|
||||
const segAngle = Math.atan2(y2 - y1, x2 - x1);
|
||||
const halfW = len / 2;
|
||||
const halfH = Math.max(4, Math.min(34, Number(item?.rotatorThickness || 12) || 12)) / 2;
|
||||
const halfH = Math.max(4, Math.min(34, global.TarinaiPhysicsBodySystem?.scalar?.(item, "thickness", 12) || 12)) / 2;
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const aabb = orientedRectAabb(cx, cy, halfW, halfH, segAngle);
|
||||
const axis = reciprocatorAxisLocal(item);
|
||||
const itemX = Number(item?.x || 0) || 0;
|
||||
const itemY = Number(item?.y || 0) || 0;
|
||||
const reciprocatorDirection = Math.sign(Number(item?.reciprocatorDirection || 1) || 1) || 1;
|
||||
const motorVelocity = item?.reciprocatorPowered === false ? 0 : reciprocatorDirection * Math.max(0, Number(item?.reciprocatorSpeed || 0) || 0);
|
||||
const totalVelocity = motorVelocity + (Number(item?.reciprocatorVelocity || 0) || 0);
|
||||
const railDirection = Math.sign(Number(global.TarinaiPhysicsBodySystem?.scalar?.(item, "railDir", 1) || 1) || 1) || 1;
|
||||
const motorVelocity = global.TarinaiPhysicsBodySystem?.scalar?.(item, "railOn", true) === false ? 0 : railDirection * Math.max(0, Number(global.TarinaiPhysicsBodySystem?.scalar?.(item, "railMotorSpeed", 0) || 0) || 0);
|
||||
const totalVelocity = motorVelocity + (Number(global.TarinaiPhysicsBodySystem?.scalar?.(item, "slideSpeed", 0) || 0) || 0);
|
||||
const vx = axis.x * totalVelocity;
|
||||
const vy = axis.y * totalVelocity;
|
||||
return {
|
||||
|
|
@ -145,6 +195,74 @@
|
|||
return { x: nx * c - ny * s, y: nx * s + ny * c };
|
||||
}
|
||||
|
||||
|
||||
|
||||
function segmentAabbHitLocal(x1, y1, x2, y2, left, top, right, bottom) {
|
||||
let t0 = 0;
|
||||
let t1 = 1;
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const clip = (p, q) => {
|
||||
if (Math.abs(p) < 1e-9) return q >= 0;
|
||||
const t = q / p;
|
||||
if (p < 0) { if (t > t1) return false; if (t > t0) t0 = t; }
|
||||
else { if (t < t0) return false; if (t < t1) t1 = t; }
|
||||
return true;
|
||||
};
|
||||
if (!clip(-dx, x1 - left)) return null;
|
||||
if (!clip(dx, right - x1)) return null;
|
||||
if (!clip(-dy, y1 - top)) return null;
|
||||
if (!clip(dy, bottom - y1)) return null;
|
||||
if (t1 < 0 || t0 > 1) return null;
|
||||
const t = clamp(t0, 0, 1);
|
||||
const hx = x1 + dx * t;
|
||||
const hy = y1 + dy * t;
|
||||
const dl = Math.abs(hx - left);
|
||||
const dr = Math.abs(right - hx);
|
||||
const dt = Math.abs(hy - top);
|
||||
const db = Math.abs(bottom - hy);
|
||||
const m = Math.min(dl, dr, dt, db);
|
||||
let nx = 0, ny = 0;
|
||||
if (m === dl) nx = -1;
|
||||
else if (m === dr) nx = 1;
|
||||
else if (m === dt) ny = -1;
|
||||
else ny = 1;
|
||||
return { t, nx, ny };
|
||||
}
|
||||
|
||||
function sweptCircleRectHitLocal(x1, y1, x2, y2, radius, r, padding = 0) {
|
||||
if (!r) return null;
|
||||
const rr = Math.max(0, Number(radius || 0) || 0) + Math.max(0, Number(padding || 0) || 0);
|
||||
if (r.oriented) {
|
||||
const a = rectLocalPoint(r, x1, y1);
|
||||
const b = rectLocalPoint(r, x2, y2);
|
||||
const hit = segmentAabbHitLocal(a.x, a.y, b.x, b.y, -(r.halfW || 0) - rr, -(r.halfH || 0) - rr, (r.halfW || 0) + rr, (r.halfH || 0) + rr);
|
||||
if (!hit) return null;
|
||||
const n = rectWorldNormal(r, hit.nx, hit.ny);
|
||||
return { t: hit.t, nx: n.x, ny: n.y, rect: r };
|
||||
}
|
||||
const hit = segmentAabbHitLocal(x1, y1, x2, y2, (r.left || 0) - rr, (r.top || 0) - rr, (r.right || 0) + rr, (r.bottom || 0) + rr);
|
||||
return hit ? { t: hit.t, nx: hit.nx, ny: hit.ny, rect: r } : null;
|
||||
}
|
||||
|
||||
function firstSweptCircleObstacleHitLocal(worldRef, t, x1, y1, x2, y2, radius, opts = {}) {
|
||||
if (!worldRef?.nearbySolidObstacleRects) return null;
|
||||
const midX = (x1 + x2) * 0.5;
|
||||
const midY = (y1 + y2) * 0.5;
|
||||
const travel = Math.hypot(x2 - x1, y2 - y1);
|
||||
const searchRadius = Math.max(Number(opts.searchRadius || 0) || 0, radius + travel * 0.5 + 180);
|
||||
const maxChecks = Math.max(4, Number(opts.maxChecks || 0) || 48);
|
||||
let best = null;
|
||||
for (const rect of worldRef.nearbySolidObstacleRects(midX, midY, searchRadius, { maxChecks })) {
|
||||
if (rect?.type === "nest_box" && worldRef.shouldIgnoreNestBoxCollisionFor?.(t, rect.item)) continue;
|
||||
const hit = sweptCircleRectHitLocal(x1, y1, x2, y2, radius, rect, 0.5);
|
||||
if (!hit) continue;
|
||||
if (!best || hit.t < best.t) best = hit;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
|
||||
function segmentIntersectsOrientedRectLocal(x1, y1, x2, y2, r, padding = 0) {
|
||||
const a = rectLocalPoint(r, x1, y1);
|
||||
const b = rectLocalPoint(r, x2, y2);
|
||||
|
|
@ -296,7 +414,7 @@
|
|||
},
|
||||
|
||||
|
||||
rotatorSegments(it) {
|
||||
mechanicalSegments(it) {
|
||||
return global.TarinaiMechanicalSystem?.sanitizeSegments?.(it) || sanitizeRotatorSegmentsLocal(it);
|
||||
},
|
||||
|
||||
|
|
@ -359,33 +477,77 @@
|
|||
},
|
||||
|
||||
nearbySolidObstacleRects(x, y, radius, { include = null, exclude = null, maxChecks = CONFIG.maxFenceCollisionChecks ?? 24 } = {}) {
|
||||
const limit = Math.max(1, Number(maxChecks || 0) || 24);
|
||||
const cacheable = !include && !exclude;
|
||||
const stats = this.nearbyQueryStatsThisFrame || null;
|
||||
const q = 48;
|
||||
const qx = Math.floor((Number(x) || 0) / q);
|
||||
const qy = Math.floor((Number(y) || 0) / q);
|
||||
const qcx = (qx + 0.5) * q;
|
||||
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 cacheKey = cacheable
|
||||
? `${this.spatialVersion || 0}:${this.spatialDirtyMarksTotal || 0}:${qx},${qy},${qr},${cacheLimit}`
|
||||
: "";
|
||||
if (cacheable) {
|
||||
if (!this._solidObstacleRectQueryCache) this._solidObstacleRectQueryCache = new Map();
|
||||
const cached = this._solidObstacleRectQueryCache.get(cacheKey);
|
||||
if (cached) {
|
||||
if (stats) stats.obstacleRectHits = (stats.obstacleRectHits || 0) + 1;
|
||||
return cached;
|
||||
}
|
||||
} else if (stats) {
|
||||
stats.obstacleRectBypass = (stats.obstacleRectBypass || 0) + 1;
|
||||
}
|
||||
|
||||
const queryX = cacheable ? qcx : x;
|
||||
const queryY = cacheable ? qcy : y;
|
||||
const queryRadius = cacheable ? qr * q : radius;
|
||||
const queryLimit = cacheable ? cacheLimit : limit;
|
||||
const rects = [];
|
||||
const seen = new Set();
|
||||
const stamp = (this._obstacleRectQueryStamp = (this._obstacleRectQueryStamp || 0) + 1);
|
||||
let checked = 0;
|
||||
const addRectsFor = (it) => {
|
||||
if (!it || it === exclude || it.dead || seen.has(it)) return false;
|
||||
if (!it || it === exclude || it.dead || it._obstacleRectQueryStamp === stamp) return false;
|
||||
if (include && !include(it)) return false;
|
||||
const partRects = this.solidObstacleRects(it);
|
||||
if (!partRects.length) return false;
|
||||
seen.add(it);
|
||||
it._obstacleRectQueryStamp = stamp;
|
||||
checked += 1;
|
||||
for (const rect of partRects) rects.push(rect);
|
||||
return true;
|
||||
};
|
||||
for (const it of this.nearbyItems(x, y, radius)) {
|
||||
const candidates = this.nearbyObstacles?.(queryX, queryY, queryRadius, false) || this.nearbyItems?.(queryX, queryY, queryRadius, false) || [];
|
||||
visitNearestObstaclesLocal(queryX, queryY, candidates, queryLimit, (it) => {
|
||||
addRectsFor(it);
|
||||
if (checked >= maxChecks) break;
|
||||
}
|
||||
// Large custom rotators can extend well beyond their center cell; scan them
|
||||
// explicitly so their blades keep colliding even when the hub is far away.
|
||||
if (checked < maxChecks) {
|
||||
for (const it of this.items || []) {
|
||||
if (!it || !global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type) || it.dead || seen.has(it)) continue;
|
||||
const reach = global.TarinaiMechanicalSystem?.reach?.(it) || Math.max(60, it.r || 64);
|
||||
if (distXY(x, y, it.x, it.y) > radius + reach + 36) continue;
|
||||
addRectsFor(it);
|
||||
if (checked >= maxChecks) break;
|
||||
return checked >= queryLimit;
|
||||
}, { include, exclude, stamp });
|
||||
if (checked < queryLimit && !this.nearbyObstacles) {
|
||||
const mech = global.TarinaiMechanicalSystem;
|
||||
const fallback = [];
|
||||
for (const type of ["rotator", "poison_block", "reciprocator"]) {
|
||||
for (const it of this.itemsOfType?.(type) || []) {
|
||||
if (!it || it.dead || it._obstacleRectQueryStamp === stamp) continue;
|
||||
const reach = mech?.reach?.(it) || Math.max(60, it.r || 64);
|
||||
if (distXY(queryX, queryY, it.x, it.y) > queryRadius + reach + 36) continue;
|
||||
fallback.push(it);
|
||||
}
|
||||
}
|
||||
visitNearestObstaclesLocal(queryX, queryY, fallback, queryLimit - checked, (it) => {
|
||||
addRectsFor(it);
|
||||
return checked >= queryLimit;
|
||||
}, { include, exclude, stamp });
|
||||
}
|
||||
if (stats) {
|
||||
stats.obstacleRectMisses = (stats.obstacleRectMisses || 0) + 1;
|
||||
stats.obstacleRectBuilt = (stats.obstacleRectBuilt || 0) + rects.length;
|
||||
}
|
||||
if (cacheable) {
|
||||
const cache = this._solidObstacleRectQueryCache || (this._solidObstacleRectQueryCache = new Map());
|
||||
if (cache.size > 160) cache.clear();
|
||||
cache.set(cacheKey, rects);
|
||||
}
|
||||
return rects;
|
||||
},
|
||||
|
|
@ -394,7 +556,101 @@
|
|||
return global.TarinaiCollisionFootprints?.pointInRect?.(x, y, r, padding) ?? false;
|
||||
},
|
||||
|
||||
pushTarinaiOutOfRect(t, r, rr) {
|
||||
|
||||
nearbyPoisonBlockHazardRects(x, y, radius, { maxChecks = 24 } = {}) {
|
||||
if ((this.itemCounts?.poison_block || 0) <= 0) return [];
|
||||
const rects = [];
|
||||
const stamp = (this._poisonHazardQueryStamp = (this._poisonHazardQueryStamp || 0) + 1);
|
||||
let checked = 0;
|
||||
const addRectsFor = (it) => {
|
||||
if (!it || it.dead || it.type !== "poison_block" || it._poisonHazardQueryStamp === stamp) return false;
|
||||
const reach = global.TarinaiMechanicalSystem?.reach?.(it) || Math.max(60, it.r || 64);
|
||||
if (distXY(x, y, it.x, it.y) > radius + reach + 36) return false;
|
||||
const partRects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(it) || [];
|
||||
if (!partRects.length) return false;
|
||||
it._poisonHazardQueryStamp = stamp;
|
||||
checked += 1;
|
||||
for (const rect of partRects) rects.push(rect);
|
||||
return true;
|
||||
};
|
||||
const candidates = this.nearbyHazards?.(x, y, radius + 48, false) || [];
|
||||
for (const it of candidates) {
|
||||
addRectsFor(it);
|
||||
if (checked >= maxChecks) break;
|
||||
}
|
||||
if (checked < maxChecks && !this.nearbyHazards) {
|
||||
for (const it of this.itemsOfType?.("poison_block") || []) {
|
||||
if (addRectsFor(it) && checked >= maxChecks) break;
|
||||
}
|
||||
}
|
||||
return rects;
|
||||
},
|
||||
|
||||
circleOverlapsObstacleRect(x, y, radius, r, padding = 0) {
|
||||
if (!r) return false;
|
||||
const rr = Math.max(0, Number(radius || 0) || 0) + Math.max(0, Number(padding || 0) || 0);
|
||||
if (r.oriented) {
|
||||
const local = rectLocalPoint(r, x, y);
|
||||
const clx = clamp(local.x, -(r.halfW || 0), r.halfW || 0);
|
||||
const cly = clamp(local.y, -(r.halfH || 0), r.halfH || 0);
|
||||
return Math.hypot(local.x - clx, local.y - cly) < rr;
|
||||
}
|
||||
const cx = clamp(x, r.left, r.right);
|
||||
const cy = clamp(y, r.top, r.bottom);
|
||||
return Math.hypot(x - cx, y - cy) < rr;
|
||||
},
|
||||
|
||||
applyPoisonBlockContactDamage(t, rect, rr) {
|
||||
if (!t || t.dead || !rect?.poisonBlock || !rect.item || rect.item.dead) return false;
|
||||
if (!this.circleOverlapsObstacleRect(t.x, t.y, rr, rect, 1.0)) return false;
|
||||
const now = this.time || 0;
|
||||
const key = rect.item.id || "poison";
|
||||
t._poisonBlockHitAt = t._poisonBlockHitAt || Object.create(null);
|
||||
const body = global.TarinaiPhysicsBodySystem?.ensureBody?.(rect.item, this, { syncFromLegacy: false });
|
||||
const solid = body?.collision ? body.collision.solid !== false : true;
|
||||
const cooldown = solid ? 0.30 : 0.42;
|
||||
if ((t._poisonBlockHitAt[key] || -999) + cooldown > now) return false;
|
||||
t._poisonBlockHitAt[key] = now;
|
||||
const velocity = body?.velocity || {};
|
||||
const vx = Number((velocity.x ?? rect.item.vx) || 0) || 0;
|
||||
const vy = Number((velocity.y ?? rect.item.vy) || 0) || 0;
|
||||
const omega = Number((velocity.angular ?? global.TarinaiPhysicsBodySystem?.scalar?.(rect.item, "spin", 0)) || 0) || 0;
|
||||
const speed = Math.hypot(vx, vy) + Math.abs(omega) * Math.max(28, rect.item.r || 64) * 0.35;
|
||||
const base = Math.max(1, Number((body?.hazard?.damage ?? global.TarinaiPhysicsBodySystem?.scalar?.(rect.item, "damage", 7)) || 7) || 7);
|
||||
const damage = clamp(base * (solid ? 1.0 : 0.72) + speed * 0.018, 2.0, 18.0);
|
||||
if (typeof this.applyImpactDamage === "function") this.applyImpactDamage(t, damage, "毒ブロック", { source: rect.item, x: t.x, y: t.y });
|
||||
else if (typeof t.damage === "function") t.damage(damage, "毒ブロック");
|
||||
// HPバーは描画側で「ダメージ中」の表示条件を見るため、毒ブロックでも明示的に傷み状態を立てる。
|
||||
t.showHpBar?.(4.8);
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.82);
|
||||
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.28);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24);
|
||||
if (typeof t.bubble === "function" && now >= (t.lastPoisonBlockBubbleAt || -999) + 0.85) {
|
||||
t.lastPoisonBlockBubbleAt = now;
|
||||
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
||||
}
|
||||
if (now >= (t.lastPoisonBlockEffectAt || -999) + 0.10) {
|
||||
t.lastPoisonBlockEffectAt = now;
|
||||
this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(16, rr * 1.05), life: 0.18, color: "rgba(92,188,68,0.45)" }));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
resolvePoisonBlockContactDamage(t, rr) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return false;
|
||||
if ((this.itemCounts?.poison_block || 0) <= 0) return false;
|
||||
const now = this.time || 0;
|
||||
const interval = Math.max(0.045, Math.min(0.10, Number(CONFIG?.poisonBlockProbeInterval || 0.075) || 0.075));
|
||||
if ((t._nextPoisonBlockProbeAt || 0) > now) return false;
|
||||
t._nextPoisonBlockProbeAt = now + interval;
|
||||
let hit = false;
|
||||
for (const rect of this.nearbyPoisonBlockHazardRects(t.x, t.y, rr + 150, { maxChecks: 32 })) {
|
||||
hit = this.applyPoisonBlockContactDamage(t, rect, rr) || hit;
|
||||
}
|
||||
return hit;
|
||||
},
|
||||
|
||||
pushTarinaiOutOfRect(t, r, rr, opts = {}) {
|
||||
if (!t || !r) return false;
|
||||
const preVx = Number(t.vx || 0) || 0;
|
||||
const preVy = Number(t.vy || 0) || 0;
|
||||
|
|
@ -444,9 +700,12 @@
|
|||
nx = dx / d;
|
||||
ny = dy / d;
|
||||
}
|
||||
const slop = Number.isFinite(opts.slop) ? opts.slop : 0.8;
|
||||
const defaultMaxPush = r.oriented && insidePenetration > 0 ? Math.max(18, rr * 2.8) : Math.max(10, rr * 0.92);
|
||||
const maxPush = Math.max(1, Number.isFinite(opts.maxPush) ? opts.maxPush : defaultMaxPush);
|
||||
const push = r.oriented && insidePenetration > 0
|
||||
? Math.min(insidePenetration + rr + 0.8, Math.max(18, rr * 2.8))
|
||||
: Math.min(rr - d + 0.8, Math.max(10, rr * 0.92));
|
||||
? Math.min(insidePenetration + rr + slop, maxPush)
|
||||
: Math.min(rr - d + slop, maxPush);
|
||||
t.x += nx * push;
|
||||
t.y += ny * push;
|
||||
if (r.bounce) {
|
||||
|
|
@ -486,50 +745,122 @@
|
|||
}
|
||||
} else if (r.mechanical) {
|
||||
global.TarinaiMechanicalSystem?.applySurfaceVelocityToCircle?.(t, r, nx, ny, preVx, preVy, {
|
||||
damping: r.rotator ? 0.78 : 0.80,
|
||||
surfaceScale: r.rotator ? 0.52 : 0.64,
|
||||
normalBoost: r.rotator ? 42 : 36,
|
||||
maxSpeed: r.rotator ? 680 : 620,
|
||||
impulseVScale: r.rotator ? 0.10 : 0.08,
|
||||
impulseMax: r.rotator ? 360 : 320,
|
||||
damping: r.rotator ? 0.78 : (r.poisonBlock ? 0.74 : 0.80),
|
||||
surfaceScale: r.rotator ? 0.52 : (r.poisonBlock ? 0.38 : 0.64),
|
||||
normalBoost: r.rotator ? 42 : (r.poisonBlock ? 30 : 36),
|
||||
maxSpeed: r.rotator ? 680 : (r.poisonBlock ? 560 : 620),
|
||||
impulseVScale: r.rotator ? 0.10 : (r.poisonBlock ? 0.05 : 0.08),
|
||||
impulseMax: r.rotator ? 360 : (r.poisonBlock ? 260 : 320),
|
||||
passiveImpulseScale: 0.82,
|
||||
spinKick: 0,
|
||||
});
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, r.rotator ? 0.12 : 0.10);
|
||||
const field = r.rotator ? "lastRotatorHitEffectAt" : "lastReciprocatorHitEffectAt";
|
||||
const field = r.rotator ? "lastRotatorHitEffectAt" : (r.poisonBlock ? "lastPoisonBlockPushEffectAt" : "lastReciprocatorHitEffectAt");
|
||||
if ((this.time || 0) >= (t[field] || -999) + 0.10) {
|
||||
t[field] = this.time || 0;
|
||||
this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(14, rr * 0.95), life: 0.16, color: r.rotator ? "rgba(175,116,230,0.42)" : "rgba(90,150,214,0.40)" }));
|
||||
this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(14, rr * 0.95), life: 0.16, color: r.rotator ? "rgba(175,116,230,0.42)" : (r.poisonBlock ? "rgba(92,188,68,0.42)" : "rgba(90,150,214,0.40)") }));
|
||||
}
|
||||
} else if (Math.abs(dx) > Math.abs(dy)) t.vx *= -0.18;
|
||||
else t.vy *= -0.18;
|
||||
return true;
|
||||
},
|
||||
|
||||
resolveSolidObstacleCollision(t) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return;
|
||||
const rr = Math.max(8, t.radius * 0.74);
|
||||
moveTarinaiWithCollision(t, dx, dy, opts = {}) {
|
||||
if (!t || t.dead) return false;
|
||||
const moveX = Number(dx || 0) || 0;
|
||||
const moveY = Number(dy || 0) || 0;
|
||||
const dist = Math.hypot(moveX, moveY);
|
||||
if (dist <= 0.0001) return this.resolveSolidObstacleCollision(t, { maxPasses: 2, reason: opts.reason || "tarinai-stationary" });
|
||||
if (this.isTarinaiHiddenInNestBox?.(t)) {
|
||||
t.x += moveX;
|
||||
t.y += moveY;
|
||||
return false;
|
||||
}
|
||||
const rr = Math.max(8, (Number(t.radius) || 22) * 0.74);
|
||||
const maxStep = Math.max(5, Math.min(12, Number(opts.maxStep || rr * 0.48) || 8));
|
||||
const steps = Math.max(1, Math.min(48, Math.ceil(dist / maxStep)));
|
||||
const stepX = moveX / steps;
|
||||
const stepY = moveY / steps;
|
||||
let collided = false;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const sx = t.x;
|
||||
const sy = t.y;
|
||||
const ex = sx + stepX;
|
||||
const ey = sy + stepY;
|
||||
const hit = firstSweptCircleObstacleHitLocal(this, t, sx, sy, ex, ey, rr, {
|
||||
searchRadius: Math.max(rr + 110, rr + Math.hypot(stepX, stepY) + 170),
|
||||
maxChecks: Math.max(opts.light ? 10 : 32, Number(opts.maxChecks || 0) || CONFIG.maxFenceCollisionChecks || 24),
|
||||
});
|
||||
if (hit && hit.t <= 1) {
|
||||
const safeT = Math.max(0, hit.t - 0.035);
|
||||
t.x = sx + stepX * safeT;
|
||||
t.y = sy + stepY * safeT;
|
||||
const toward = (t.vx || 0) * hit.nx + (t.vy || 0) * hit.ny;
|
||||
if (toward < 0) {
|
||||
t.vx -= toward * hit.nx;
|
||||
t.vy -= toward * hit.ny;
|
||||
}
|
||||
collided = this.resolveSolidObstacleCollision(t, {
|
||||
maxPasses: 3,
|
||||
searchRadius: Math.max(rr + 110, rr + Math.hypot(stepX, stepY) + 170),
|
||||
maxChecks: Math.max(opts.light ? 10 : 32, Number(opts.maxChecks || 0) || CONFIG.maxFenceCollisionChecks || 24),
|
||||
slop: 0.30,
|
||||
reason: opts.reason || "tarinai-swept-hit",
|
||||
}) || true;
|
||||
continue;
|
||||
}
|
||||
t.x = ex;
|
||||
t.y = ey;
|
||||
const passHit = this.resolveSolidObstacleCollision(t, {
|
||||
maxPasses: 2,
|
||||
searchRadius: Math.max(rr + 96, rr + Math.hypot(stepX, stepY) + 140),
|
||||
maxChecks: Math.max(opts.light ? 10 : 28, Number(opts.maxChecks || 0) || CONFIG.maxFenceCollisionChecks || 24),
|
||||
slop: 0.35,
|
||||
reason: opts.reason || "tarinai-swept-move",
|
||||
});
|
||||
collided = passHit || collided;
|
||||
}
|
||||
if (collided) {
|
||||
t.lastSolidObstacleCollisionAt = this.time || 0;
|
||||
this.markSpatialDirty?.("tarinai-moved-collision");
|
||||
}
|
||||
return collided;
|
||||
},
|
||||
|
||||
resolveSolidObstacleCollision(t, opts = {}) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return false;
|
||||
const rr = Math.max(8, (Number(t.radius) || 22) * 0.74);
|
||||
this.resolvePoisonBlockContactDamage(t, rr);
|
||||
let pushed = false;
|
||||
let bounced = false;
|
||||
for (const rect of this.nearbySolidObstacleRects(t.x, t.y, rr + 150)) {
|
||||
if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor(t, rect.item)) continue;
|
||||
if (this.pushTarinaiOutOfRect(t, rect, rr)) {
|
||||
pushed = true;
|
||||
if (rect?.bounce) bounced = true;
|
||||
const maxPasses = Math.max(1, Math.min(4, Number(opts.maxPasses ?? 2) || 2));
|
||||
const searchRadius = Math.max(rr + 40, Number(opts.searchRadius || rr + 150) || (rr + 150));
|
||||
const maxChecks = Math.max(1, Number(opts.maxChecks || CONFIG.maxFenceCollisionChecks || 24) || 24);
|
||||
for (let pass = 0; pass < maxPasses; pass++) {
|
||||
let passPushed = false;
|
||||
for (const rect of this.nearbySolidObstacleRects(t.x, t.y, searchRadius, { maxChecks })) {
|
||||
if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor(t, rect.item)) continue;
|
||||
if (this.pushTarinaiOutOfRect(t, rect, rr, opts)) {
|
||||
pushed = true;
|
||||
passPushed = true;
|
||||
if (rect?.bounce) bounced = true;
|
||||
}
|
||||
}
|
||||
if (!passPushed) break;
|
||||
}
|
||||
if (pushed) {
|
||||
const pad = CONFIG.worldPadding + Math.max(2, t.radius * 0.18);
|
||||
const pad = CONFIG.worldPadding + Math.max(2, (Number(t.radius) || 22) * 0.18);
|
||||
t.x = clamp(t.x, pad, this.w - pad);
|
||||
t.y = clamp(t.y, pad, this.h - pad);
|
||||
const maxV = bounced ? 520 : 130;
|
||||
t.vx = clamp(t.vx || 0, -maxV, maxV);
|
||||
t.vy = clamp(t.vy || 0, -maxV, maxV);
|
||||
}
|
||||
return pushed;
|
||||
},
|
||||
|
||||
resolveFenceCollision(t) {
|
||||
this.resolveSolidObstacleCollision(t);
|
||||
return this.resolveSolidObstacleCollision(t, { maxPasses: 2, reason: "resolve-fence" });
|
||||
},
|
||||
|
||||
segmentIntersectsRect(x1, y1, x2, y2, r) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,34 @@
|
|||
"use strict";
|
||||
(function ensureDeterministicHelpers(global) {
|
||||
if (typeof global.deterministicChance === "function" && typeof global.deterministicRange === "function") return;
|
||||
const clamp01 = v => Math.max(0, Math.min(1, Number(v) || 0));
|
||||
const keyPart = value => {
|
||||
if (value == null) return "null";
|
||||
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(3) : "nan";
|
||||
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
||||
if (typeof value === "object") {
|
||||
const id = value.familyKey || value.id || value.seed || value.liveToken || value.name;
|
||||
if (id) return `${value.type || value.constructor?.name || "entity"}:${id}`;
|
||||
const x = Number.isFinite(value.x) ? value.x.toFixed(1) : "x";
|
||||
const y = Number.isFinite(value.y) ? value.y.toFixed(1) : "y";
|
||||
return `${value.type || value.constructor?.name || "entity"}@${x},${y}`;
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
const hashUnit = (seed, salt = "") => {
|
||||
const str = `${seed}:${salt}`;
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
|
||||
(function (global) {
|
||||
const World = global.World;
|
||||
|
|
@ -539,6 +569,61 @@
|
|||
return true;
|
||||
},
|
||||
|
||||
cancelBirthRitual(actor, options = {}) {
|
||||
const source = actor || null;
|
||||
const partnerId = source?.birthPartnerId || null;
|
||||
const partner = partnerId ? this.liveTarinaiById?.(partnerId) : null;
|
||||
const participants = [];
|
||||
if (source) participants.push(source);
|
||||
if (partner && partner !== source) participants.push(partner);
|
||||
if (!participants.length) return false;
|
||||
const reason = options.reason || "繁殖が中断された";
|
||||
let canceled = false;
|
||||
for (const t of participants) {
|
||||
if (!t || t.dead) continue;
|
||||
if ((t.birthRitualTimer || 0) <= 0.04 && t.state !== "birth_ritual") continue;
|
||||
t.birthRitualTimer = 0;
|
||||
t.birthRitualMax = 0;
|
||||
t.birthPartnerId = null;
|
||||
t.birthRitualLeader = false;
|
||||
t.birthRitualRole = 0;
|
||||
t.postBirthPeaceTimer = Math.max(t.postBirthPeaceTimer || 0, 1.4);
|
||||
t.behaviorLockTimer = Math.max(t.behaviorLockTimer || 0, 0.25);
|
||||
if (typeof clearTarinaiBehavior === "function") clearTarinaiBehavior(t, { reason });
|
||||
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && (e.id === "approach_mate" || e.id === "birth_ritual"));
|
||||
if (options.panic && t === source) {
|
||||
if (t.enterPanic) t.enterPanic({ target: options.target || null, reason, fear: options.fear ?? 0.95, wake: true, cause: options.cause || "birth_ritual_interrupted" });
|
||||
else t.setActionState?.("panic", { target: options.target || null, reason, wake: true });
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, options.fear ?? 0.95);
|
||||
} else {
|
||||
t.goIdle?.(reason);
|
||||
t.thought = reason;
|
||||
}
|
||||
canceled = true;
|
||||
}
|
||||
if (canceled && !options.silentLog && this.time - (this.lastBirthCancelLogAt || -999) > 1.2) {
|
||||
this.log?.(reason, "birth", { participants });
|
||||
this.lastBirthCancelLogAt = this.time || 0;
|
||||
}
|
||||
return canceled;
|
||||
},
|
||||
|
||||
cancelBirthRitualOnForce(tarinai, force = 0, options = {}) {
|
||||
if (!tarinai || tarinai.dead) return false;
|
||||
if ((tarinai.birthRitualTimer || 0) <= 0.04 && tarinai.state !== "birth_ritual") return false;
|
||||
const amount = Number(force) || 0;
|
||||
const threshold = Number(options.threshold || 180) || 180;
|
||||
if (amount < threshold) return false;
|
||||
return this.cancelBirthRitual(tarinai, {
|
||||
reason: options.reason || "強い衝撃で繁殖が中断された",
|
||||
target: options.target || null,
|
||||
fear: options.fear ?? 1.0,
|
||||
cause: options.cause || "external_force",
|
||||
panic: options.panic !== false,
|
||||
silentLog: !!options.silentLog,
|
||||
});
|
||||
},
|
||||
|
||||
finishBirthRitual(a, b) {
|
||||
if (!a || !b || a.dead || b.dead) return null;
|
||||
const reproductionBlockedByDisease = a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease;
|
||||
|
|
@ -725,10 +810,10 @@
|
|||
const actor = aDrug && !bDrug ? a : (!aDrug && bDrug ? b : (a.energy + a.stress * 0.36 >= b.energy + b.stress * 0.36 ? a : b));
|
||||
const target = actor === a ? b : a;
|
||||
if (!this.canFightPair(actor, target)) return false;
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, rand(0.85, 1.35));
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, deterministicRange(this, "fight-mochi-intimidate-actor-timer", 0.85, 1.35, actor, target));
|
||||
actor.intimidateTargetId = target.id;
|
||||
actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28);
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(0.75, 1.25));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "fight-mochi-intimidate-target-timer", 0.75, 1.25, actor, target));
|
||||
target.fearTimer = Math.max(target.fearTimer || 0, 0.72 * target.personalityProfile().fear);
|
||||
actor.adjustRelation(target, -0.05, 0.06, "intimidate");
|
||||
target.adjustRelation(actor, -0.12, 0.30 * target.personalityProfile().fear, "intimidate");
|
||||
|
|
@ -751,8 +836,8 @@
|
|||
if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return false;
|
||||
const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||
const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0;
|
||||
const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + aAggressiveIntent * 8 + ((typeof b.relationTo === "function" ? (b.relationTo(a.id).fear || 0) : 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
||||
const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((typeof a.relationTo === "function" ? (a.relationTo(b.id).fear || 0) : 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5);
|
||||
const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + aAggressiveIntent * 8 + ((typeof b.relationTo === "function" ? (b.relationTo(a.id).fear || 0) : 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + deterministicRange(this, "conflict-score-a", -5, 5, a, b);
|
||||
const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((typeof a.relationTo === "function" ? (a.relationTo(b.id).fear || 0) : 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + deterministicRange(this, "conflict-score-b", -5, 5, a, b);
|
||||
const actor = scoreA >= scoreB ? a : b;
|
||||
const target = actor === a ? b : a;
|
||||
if (!this.canFightPair(actor, target)) return false;
|
||||
|
|
@ -761,7 +846,7 @@
|
|||
const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite();
|
||||
const battleDrug = (actor.fightMochiTimer || 0) > 0.04 || (target.fightMochiTimer || 0) > 0.04;
|
||||
const intimidationChance = clamp(0.40 + (battleDrug ? 0.24 : 0) + (target.isZunchiSlave ? 0.18 : 0), 0.24, 0.90);
|
||||
if (canIntimidate && Math.random() < intimidationChance) {
|
||||
if (canIntimidate && deterministicChance(this, "conflict-intimidation-choice", intimidationChance, actor, target)) {
|
||||
return this.startIntimidation(actor, target, actorScore, targetScore);
|
||||
}
|
||||
return this.startFight(actor, target, { initiator: actor });
|
||||
|
|
@ -780,13 +865,13 @@
|
|||
const targetSize = target.effectiveScale ? target.effectiveScale() : (target.scale || 0.28);
|
||||
const sizeEdge = clamp((actorSize - targetSize) / 0.16, -1, 1);
|
||||
const aggressionEdge = clamp(actorAggression - targetAggression * 0.55, -1, 1);
|
||||
const resistance = target.energy * 0.30 + target.mood * 0.16 + targetAggression * 8 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + rand(-5, 5);
|
||||
const resistance = target.energy * 0.30 + target.mood * 0.16 + targetAggression * 8 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + deterministicRange(this, "intimidation-resistance", -5, 5, actor, target);
|
||||
const successChance = clamp(0.56 + (actorScore - targetScore + fearLoad - resistance) / 82 + aggressionEdge * 0.13 + sizeEdge * 0.16, 0.30, 0.96);
|
||||
actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20);
|
||||
target.surpriseTimer = Math.max(target.surpriseTimer, 0.32);
|
||||
actor.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
||||
target.fightCooldown = CONFIG.fightCooldown + rand(1, 4);
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer, rand(1.05, 1.65));
|
||||
actor.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-actor-cooldown", 1, 4, actor, target);
|
||||
target.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-target-cooldown", 1, 4, actor, target);
|
||||
actor.intimidateTimer = Math.max(actor.intimidateTimer, deterministicRange(this, "intimidation-actor-timer", 1.05, 1.65, actor, target));
|
||||
actor.intimidateTargetId = target.id;
|
||||
actor.setActionState("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" });
|
||||
if (typeof setBehaviorText === "function") setBehaviorText(actor, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "\u5a01\u5687\u3057\u3066\u3044\u308b", reasonText: `${target.name || "\u76f8\u624b"}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b`, target, phase: "perform", source: "behavior" });
|
||||
|
|
@ -796,9 +881,9 @@
|
|||
color: "rgba(145, 106, 55, 0.70)",
|
||||
}));
|
||||
this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)");
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(0.95, 1.55));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-target-prep-timer", 0.95, 1.55, actor, target));
|
||||
target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear);
|
||||
if (Math.random() >= successChance) {
|
||||
if (!deterministicChance(this, "intimidation-success", successChance, actor, target)) {
|
||||
actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation");
|
||||
target.adjustRelation(actor, -0.08, 0.22 * target.personalityProfile().fear, "intimidate");
|
||||
actor.intimidateTimer = 0;
|
||||
|
|
@ -814,13 +899,13 @@
|
|||
actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation");
|
||||
target.defeatedById = actor.id;
|
||||
target.fightWinnerId = actor.id;
|
||||
target.defeatedTimer = Math.max(target.defeatedTimer, rand(1.8, 2.8));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(1.2, 2.0));
|
||||
target.defeatedTimer = Math.max(target.defeatedTimer, deterministicRange(this, "intimidation-defeated-timer", 1.8, 2.8, actor, target));
|
||||
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-success-timer", 1.2, 2.0, actor, target));
|
||||
if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.35, wake: true, cause: "intimidated" });
|
||||
else { target.fearTimer = Math.max(target.fearTimer, 1.35 * target.personalityProfile().fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); }
|
||||
this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
||||
target.vx += (target.x < actor.x ? -1 : 1) * rand(26, 54);
|
||||
target.vy += rand(-18, 18);
|
||||
target.vx += (target.x < actor.x ? -1 : 1) * deterministicRange(this, "intimidation-knockback-x", 26, 54, actor, target);
|
||||
target.vy += deterministicRange(this, "intimidation-knockback-y", -18, 18, actor, target);
|
||||
actor.adjustRelation(target, -0.08, 0.06, "intimidate");
|
||||
target.adjustRelation(actor, -0.22, 0.85 * target.personalityProfile().fear, "intimidate");
|
||||
if (this.time - Math.max(actor.lastLog, target.lastLog) > 5.5) {
|
||||
|
|
@ -862,12 +947,12 @@
|
|||
setBehaviorText(a, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: a === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonA, causeText: causeFor(a, b), target: b, phase: "perform", source: "behavior" });
|
||||
setBehaviorText(b, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: b === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonB, causeText: causeFor(b, a), target: a, phase: "perform", source: "behavior" });
|
||||
}
|
||||
a.fightTimer = Math.max(a.fightTimer, rand(3.2, 4.8));
|
||||
b.fightTimer = Math.max(b.fightTimer, rand(3.2, 4.8));
|
||||
a.nextHeadbutt = this.time + rand(0.08, 0.18);
|
||||
a.fightTimer = Math.max(a.fightTimer, deterministicRange(this, "fight-start-timer-a", 3.2, 4.8, a, b));
|
||||
b.fightTimer = Math.max(b.fightTimer, deterministicRange(this, "fight-start-timer-b", 3.2, 4.8, a, b));
|
||||
a.nextHeadbutt = this.time + deterministicRange(this, "fight-start-headbutt", 0.08, 0.18, a, b);
|
||||
b.nextHeadbutt = a.nextHeadbutt;
|
||||
a.fightCooldown = CONFIG.fightCooldown + rand(1, 5);
|
||||
b.fightCooldown = CONFIG.fightCooldown + rand(1, 5);
|
||||
a.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "fight-start-cooldown-a", 1, 5, a, b);
|
||||
b.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "fight-start-cooldown-b", 1, 5, a, b);
|
||||
if (!a.fightTargetIds) a.fightTargetIds = [];
|
||||
if (!b.fightTargetIds) b.fightTargetIds = [];
|
||||
if (!a.fightTargetIds.includes(b.id)) a.fightTargetIds.push(b.id);
|
||||
|
|
@ -876,22 +961,24 @@
|
|||
b.fightTargetIds = b.fightTargetIds.slice(-4);
|
||||
a.fightTargetId = a.fightTargetIds[0];
|
||||
b.fightTargetId = b.fightTargetIds[0];
|
||||
if (a.addStress) a.addStress(rand(8, 18), { threshold: 8 });
|
||||
if (b.addStress) b.addStress(rand(8, 18), { threshold: 8 });
|
||||
const damageToA = b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7);
|
||||
const damageToB = a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7);
|
||||
if (a.addStress) a.addStress(deterministicRange(this, "fight-start-stress-a", 8, 18, a, b), { threshold: 8 });
|
||||
if (b.addStress) b.addStress(deterministicRange(this, "fight-start-stress-b", 8, 18, a, b), { threshold: 8 });
|
||||
const rawDamageToA = deterministicRange(this, "fight-pulse-damage-a", 2, 7, a, b);
|
||||
const rawDamageToB = deterministicRange(this, "fight-pulse-damage-b", 2, 7, a, b);
|
||||
const damageToA = b.outgoingDamage ? b.outgoingDamage(rawDamageToA) : rawDamageToA;
|
||||
const damageToB = a.outgoingDamage ? a.outgoingDamage(rawDamageToB) : rawDamageToB;
|
||||
a.damage(damageToA, "\u55a7\u5629");
|
||||
b.damage(damageToB, "\u55a7\u5629");
|
||||
this.maybeDefeatFromFightDamage?.(a, b, damageToA);
|
||||
this.maybeDefeatFromFightDamage?.(b, a, damageToB);
|
||||
const dx = b.x - a.x, dy = b.y - a.y;
|
||||
const d = Math.hypot(dx, dy) || 1;
|
||||
a.vx -= dx / d * rand(32, 62); a.vy -= dy / d * rand(32, 62);
|
||||
b.vx += dx / d * rand(32, 62); b.vy += dy / d * rand(32, 62);
|
||||
a.vx -= dx / d * deterministicRange(this, "fight-pulse-knockback-ax", 32, 62, a, b); a.vy -= dy / d * deterministicRange(this, "fight-pulse-knockback-ay", 32, 62, a, b);
|
||||
b.vx += dx / d * deterministicRange(this, "fight-pulse-knockback-bx", 32, 62, a, b); b.vy += dy / d * deterministicRange(this, "fight-pulse-knockback-by", 32, 62, a, b);
|
||||
a.surpriseTimer = 0.24; b.surpriseTimer = 0.24;
|
||||
this.effects.push(new Effect("fight", (a.x + b.x) / 2, (a.y + b.y) / 2 - 8, {
|
||||
size: rand(12, 20),
|
||||
life: rand(0.32, 0.52),
|
||||
size: deterministicRange(this, "fight-pulse-effect-size", 12, 20, a, b),
|
||||
life: deterministicRange(this, "fight-pulse-effect-life", 0.32, 0.52, a, b),
|
||||
color: "rgba(116, 73, 38, 0.82)",
|
||||
}));
|
||||
audio.fight();
|
||||
|
|
|
|||
|
|
@ -161,9 +161,10 @@
|
|||
<button id="rotatorEditorClose" class="rotator-editor-close" type="button" aria-label="閉じる">×</button>
|
||||
</div>
|
||||
<div class="rotator-editor-controls">
|
||||
<label>速度 <input id="rotatorSpeed" type="range" min="-360" max="360" step="5"><input id="rotatorSpeedNum" type="number" min="-720" max="720" step="5"> 度/秒</label>
|
||||
<label>太さ <input id="rotatorThickness" type="range" min="4" max="30" step="1"><input id="rotatorThicknessNum" type="number" min="4" max="34" step="1"></label>
|
||||
<label><input id="rotatorPowered" type="checkbox"> 動力ON</label>
|
||||
<label>速度 <input id="mechMotorSpeed" type="range" min="-360" max="360" step="5"><input id="mechMotorSpeedNum" type="number" min="-720" max="720" step="5"> 度/秒</label>
|
||||
<label>太さ <input id="mechShapeThickness" type="range" min="4" max="30" step="1"><input id="mechShapeThicknessNum" type="number" min="4" max="34" step="1"></label>
|
||||
<label><input id="mechMotorPowered" type="checkbox"> 動力ON</label>
|
||||
<label id="poisonCollisionLabel"><input id="poisonSolidEnabled" type="checkbox"> 当たり判定ON</label>
|
||||
</div>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="描画方法">
|
||||
<button type="button" data-rotator-mode="line" class="active">直線</button>
|
||||
|
|
@ -186,21 +187,32 @@
|
|||
return dialog;
|
||||
}
|
||||
|
||||
function physicsApi() { return global.TarinaiPhysicsBodySystem || null; }
|
||||
function phyScalar(item, key, fallback = 0) { return physicsApi()?.scalar?.(item, key, fallback) ?? fallback; }
|
||||
function phySet(item, key, value, reason = "editor") { return physicsApi()?.setScalar?.(item, key, value, reason) || false; }
|
||||
function cloneSegmentsForRotator(item) {
|
||||
const src = Array.isArray(item?.rotatorSegments) ? item.rotatorSegments : [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||
const fallback = item?.type === "poison_block" ? [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]] : [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||
const src = physicsApi()?.segments?.(item) || fallback;
|
||||
return src.filter(seg => Array.isArray(seg) && seg.length >= 4).map(seg => [Number(seg[0]) || 0, Number(seg[1]) || 0, Number(seg[2]) || 0, Number(seg[3]) || 0]);
|
||||
}
|
||||
function physicsShapeExtent(item, segments = cloneSegmentsForRotator(item), thick = phyScalar(item, "thickness", item?.type === "poison_block" ? 14 : 12)) {
|
||||
return Math.max(...segments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + Math.max(4, thick) + 8;
|
||||
}
|
||||
|
||||
function openRotatorEditor(item, worldRef) {
|
||||
if (!item || item.type !== "rotator") return false;
|
||||
if (!item || (item.type !== "rotator" && item.type !== "poison_block")) return false;
|
||||
const isPoisonBlock = item.type === "poison_block";
|
||||
const dialog = ensureRotatorEditor();
|
||||
const canvas = dialog.querySelector("#rotatorCanvas");
|
||||
const ctx = canvas?.getContext?.("2d");
|
||||
const speed = dialog.querySelector("#rotatorSpeed");
|
||||
const speedNum = dialog.querySelector("#rotatorSpeedNum");
|
||||
const thick = dialog.querySelector("#rotatorThickness");
|
||||
const thickNum = dialog.querySelector("#rotatorThicknessNum");
|
||||
const poweredInput = dialog.querySelector("#rotatorPowered");
|
||||
const speed = dialog.querySelector("#mechMotorSpeed");
|
||||
const speedNum = dialog.querySelector("#mechMotorSpeedNum");
|
||||
const thick = dialog.querySelector("#mechShapeThickness");
|
||||
const thickNum = dialog.querySelector("#mechShapeThicknessNum");
|
||||
const poweredInput = dialog.querySelector("#mechMotorPowered");
|
||||
const poisonCollisionInput = dialog.querySelector("#poisonSolidEnabled");
|
||||
const poisonCollisionLabel = dialog.querySelector("#poisonCollisionLabel");
|
||||
const titleEl = dialog.querySelector("#rotatorEditorTitle");
|
||||
const apply = dialog.querySelector("#rotatorApply");
|
||||
const cancel = dialog.querySelector("#rotatorCancel");
|
||||
const closeBtn = dialog.querySelector("#rotatorEditorClose");
|
||||
|
|
@ -213,13 +225,20 @@
|
|||
let drawing = false;
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
const deg = Math.round((Number(item.rotatorSpeed || 0) || 0) * 180 / Math.PI);
|
||||
const initialThickness = Math.max(4, Math.min(34, Number(item.rotatorThickness || 12) || 12));
|
||||
const deg = Math.round((Number(phyScalar(item, "motorSpeed", 0) || 0) || 0) * 180 / Math.PI);
|
||||
const initialThickness = Math.max(4, Math.min(34, Number(phyScalar(item, "thickness", isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12)));
|
||||
if (titleEl) titleEl.textContent = isPoisonBlock ? "毒ブロック" : "回転体";
|
||||
const speedLabel = speed?.closest?.("label");
|
||||
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 (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);
|
||||
if (thick) thick.value = String(initialThickness);
|
||||
if (thickNum) thickNum.value = String(initialThickness);
|
||||
if (poweredInput) poweredInput.checked = item.rotatorPowered !== false;
|
||||
if (poweredInput) poweredInput.checked = phyScalar(item, "motorOn", true) !== false;
|
||||
const toLocal = (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
|
|
@ -255,7 +274,7 @@
|
|||
const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = "rgba(127,92,190,0.92)";
|
||||
ctx.strokeStyle = isPoisonBlock ? "rgba(92,172,76,0.92)" : "rgba(127,92,190,0.92)";
|
||||
ctx.lineWidth = lw;
|
||||
ctx.beginPath();
|
||||
for (const seg of segments) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); }
|
||||
|
|
@ -272,8 +291,8 @@
|
|||
ctx.beginPath(); ctx.moveTo(draft[0], draft[1]); ctx.lineTo(draft[2], draft[3]); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
ctx.fillStyle = "#f5edff";
|
||||
ctx.strokeStyle = "#6d4ca0";
|
||||
ctx.fillStyle = isPoisonBlock ? "#eefee7" : "#f5edff";
|
||||
ctx.strokeStyle = isPoisonBlock ? "#3c8f32" : "#6d4ca0";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.restore();
|
||||
|
|
@ -382,20 +401,26 @@
|
|||
canvas.onpointerdown = canvas.onpointermove = canvas.onpointerup = null;
|
||||
};
|
||||
if (apply) apply.onclick = () => {
|
||||
global.TarinaiHistory?.capture?.(worldRef, "rotator-edit");
|
||||
global.TarinaiHistory?.capture?.(worldRef, isPoisonBlock ? "poison-block-edit" : "rotator-edit");
|
||||
cleanSegments();
|
||||
item.rotatorSegments = segments.map(seg => seg.slice(0, 4));
|
||||
item.rotatorSpeed = (Number(speedNum?.value || speed?.value || 0) || 0) * Math.PI / 180;
|
||||
item.rotatorThickness = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||
item.rotatorPowered = poweredInput ? Boolean(poweredInput.checked) : item.rotatorPowered !== false;
|
||||
if (item.rotatorPowered) item.rotatorAngularVelocity = 0;
|
||||
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");
|
||||
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");
|
||||
phySet(item, "motorOn", poweredInput ? Boolean(poweredInput.checked) : phyScalar(item, "motorOn", true) !== false, "editor-power");
|
||||
if (phyScalar(item, "motorOn", true)) phySet(item, "spin", 0, "editor-spin-reset");
|
||||
}
|
||||
if (!global.TarinaiPhysicsBodySystem?.invalidateItem?.(item, isPoisonBlock ? "poison-block-edited" : "rotator-edited")) global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
|
||||
const extent = worldRef?.rotatorExtent?.(item);
|
||||
if (Number.isFinite(extent)) item.r = Math.max(32, Math.min(460, extent));
|
||||
item.rotatorEditorOpenAt = worldRef?.time || 0;
|
||||
global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, "rotator-link-remap");
|
||||
worldRef?.markSpatialDirty?.("rotator-edited");
|
||||
global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, isPoisonBlock ? "poison-block-link-remap" : "rotator-link-remap");
|
||||
worldRef?.markSpatialDirty?.(isPoisonBlock ? "poison-block-edited" : "rotator-edited");
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef?.log?.("回転体の設計を変更した。", "observe");
|
||||
worldRef?.log?.(isPoisonBlock ? "毒ブロックの設計を変更した。" : "回転体の設計を変更した。", "observe");
|
||||
global.render?.();
|
||||
close();
|
||||
};
|
||||
|
|
@ -423,10 +448,10 @@
|
|||
</div>
|
||||
<div class="rotator-editor-controls">
|
||||
<label>方向 <input id="reciprocatorAngle" type="range" min="0" max="355" step="5"><input id="reciprocatorAngleNum" type="number" min="0" max="355" step="5"> 度</label>
|
||||
<label>速度 <input id="reciprocatorSpeed" type="range" min="0" max="240" step="5"><input id="reciprocatorSpeedNum" type="number" min="0" max="360" step="5"> px/秒</label>
|
||||
<label>往復幅 <input id="reciprocatorTravel" type="range" min="48" max="360" step="4"><input id="reciprocatorTravelNum" type="number" min="24" max="520" step="4"> px</label>
|
||||
<label>速度 <input id="railMotorSpeedControl" type="range" min="0" max="240" step="5"><input id="railMotorSpeedControlNum" type="number" min="0" max="360" step="5"> px/秒</label>
|
||||
<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="reciprocatorPowered" type="checkbox"> 動力ON</label>
|
||||
<label><input id="railMotorPowered" type="checkbox"> 動力ON</label>
|
||||
</div>
|
||||
<div class="rotator-editor-tools" role="toolbar" aria-label="描画方法">
|
||||
<button type="button" data-reciprocator-mode="line" class="active">直線</button>
|
||||
|
|
@ -456,13 +481,13 @@
|
|||
const ctx = canvas?.getContext?.("2d");
|
||||
const angle = dialog.querySelector("#reciprocatorAngle");
|
||||
const angleNum = dialog.querySelector("#reciprocatorAngleNum");
|
||||
const speed = dialog.querySelector("#reciprocatorSpeed");
|
||||
const speedNum = dialog.querySelector("#reciprocatorSpeedNum");
|
||||
const travel = dialog.querySelector("#reciprocatorTravel");
|
||||
const travelNum = dialog.querySelector("#reciprocatorTravelNum");
|
||||
const speed = dialog.querySelector("#railMotorSpeedControl");
|
||||
const speedNum = dialog.querySelector("#railMotorSpeedControlNum");
|
||||
const travel = dialog.querySelector("#railTravelControl");
|
||||
const travelNum = dialog.querySelector("#railTravelControlNum");
|
||||
const thick = dialog.querySelector("#reciprocatorThickness");
|
||||
const thickNum = dialog.querySelector("#reciprocatorThicknessNum");
|
||||
const powered = dialog.querySelector("#reciprocatorPowered");
|
||||
const powered = dialog.querySelector("#railMotorPowered");
|
||||
const apply = dialog.querySelector("#reciprocatorApply");
|
||||
const cancel = dialog.querySelector("#reciprocatorCancel");
|
||||
const closeBtn = dialog.querySelector("#reciprocatorEditorClose");
|
||||
|
|
@ -475,11 +500,11 @@
|
|||
let drawing = false;
|
||||
const cx = canvas.width / 2;
|
||||
const cy = canvas.height / 2;
|
||||
const axisRad = window.TarinaiMechanicalSystem?.reciprocatorAxisAngle?.(item) ?? (Number.isFinite(Number(item.reciprocatorAxisAngle)) ? Number(item.reciprocatorAxisAngle) : (typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item.angle) || 0)));
|
||||
const axisRad = window.TarinaiMechanicalSystem?.railAxisAngle?.(item) ?? phyScalar(item, "railAxis", (typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item.angle) || 0)));
|
||||
const deg = Math.round((axisRad * 180 / Math.PI) % 360 + 360) % 360;
|
||||
const speedVal = Math.round(Number(item.reciprocatorSpeed || 92) || 0);
|
||||
const travelVal = Math.round(Number(item.reciprocatorTravel || 150) || 150);
|
||||
const initialThickness = Math.max(4, Math.min(34, Number(item.rotatorThickness || 12) || 12));
|
||||
const speedVal = Math.round(Number(phyScalar(item, "railMotorSpeed", 92)) || 0);
|
||||
const travelVal = Math.round(Number(phyScalar(item, "railTravel", 150)) || 150);
|
||||
const initialThickness = Math.max(4, Math.min(34, Number(phyScalar(item, "thickness", 12) || 12) || 12));
|
||||
if (angle) angle.value = String(Math.max(0, Math.min(355, deg)));
|
||||
if (angleNum) angleNum.value = String(deg);
|
||||
if (speed) speed.value = String(Math.max(0, Math.min(240, speedVal)));
|
||||
|
|
@ -488,7 +513,7 @@
|
|||
if (travelNum) travelNum.value = String(travelVal);
|
||||
if (thick) thick.value = String(initialThickness);
|
||||
if (thickNum) thickNum.value = String(initialThickness);
|
||||
if (powered) powered.checked = item.reciprocatorPowered !== false;
|
||||
if (powered) powered.checked = phyScalar(item, "railOn", true) !== false;
|
||||
const toLocal = (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const sx = canvas.width / Math.max(1, rect.width);
|
||||
|
|
@ -660,17 +685,20 @@
|
|||
global.TarinaiHistory?.capture?.(worldRef, "reciprocator-edit");
|
||||
cleanSegments();
|
||||
const angleDeg = Math.max(0, Math.min(355, Number(angleNum?.value || angle?.value || 0) || 0));
|
||||
item.reciprocatorAxisAngle = typeof normalizedItemAngle === "function" ? normalizedItemAngle(angleDeg * Math.PI / 180, 0) : angleDeg * Math.PI / 180;
|
||||
item.reciprocatorSpeed = Math.max(0, Math.min(360, Number(speedNum?.value || speed?.value || 92) || 0));
|
||||
item.reciprocatorTravel = Math.max(24, Math.min(520, Number(travelNum?.value || travel?.value || 150) || 150));
|
||||
item.reciprocatorPowered = powered ? Boolean(powered.checked) : item.reciprocatorPowered !== false;
|
||||
if (item.reciprocatorPowered) item.reciprocatorVelocity = 0;
|
||||
item.rotatorThickness = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12));
|
||||
item.rotatorSegments = segments.map(seg => seg.slice(0, 4));
|
||||
const sa = window.TarinaiMechanicalSystem?.reciprocatorAxisAngle?.(item) ?? (Number.isFinite(Number(item.reciprocatorAxisAngle)) ? Number(item.reciprocatorAxisAngle) : (typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item.angle) || 0)));
|
||||
const halfTravel = Math.max(12, item.reciprocatorTravel * 0.5);
|
||||
item.reciprocatorAnchorX = item.x - Math.cos(sa) * halfTravel * (Number(item.reciprocatorPhase || 0) || 0);
|
||||
item.reciprocatorAnchorY = item.y - Math.sin(sa) * halfTravel * (Number(item.reciprocatorPhase || 0) || 0);
|
||||
const railAxis = typeof normalizedItemAngle === "function" ? normalizedItemAngle(angleDeg * Math.PI / 180, 0) : angleDeg * Math.PI / 180;
|
||||
phySet(item, "railAxis", railAxis, "editor-rail-axis");
|
||||
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");
|
||||
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");
|
||||
const sa = window.TarinaiMechanicalSystem?.railAxisAngle?.(item) ?? phyScalar(item, "railAxis", typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item.angle) || 0));
|
||||
const halfTravel = Math.max(12, phyScalar(item, "railTravel", 150) * 0.5);
|
||||
const phase = Number(phyScalar(item, "railPhase", 0) || 0) || 0;
|
||||
phySet(item, "railAnchorX", item.x - Math.cos(sa) * halfTravel * phase, "editor-rail-anchor");
|
||||
phySet(item, "railAnchorY", item.y - Math.sin(sa) * halfTravel * phase, "editor-rail-anchor");
|
||||
if (!global.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "reciprocator-edited")) global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
|
||||
const extent = worldRef?.rotatorExtent?.(item);
|
||||
if (Number.isFinite(extent)) item.r = Math.max(32, Math.min(460, extent));
|
||||
global.TarinaiLinkRuntime?.remapLinksForEditedMechanicalItem?.(worldRef, item, "reciprocator-link-remap");
|
||||
|
|
@ -700,8 +728,9 @@
|
|||
if (type === "trace" || type === "splat" || type === "ant_corpse") continue;
|
||||
if (type === "rope" || type === "rod") {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const a = runtime?.endpointWorld?.(it.linkA, worldRef);
|
||||
const b = runtime?.endpointWorld?.(it.linkB, worldRef);
|
||||
const pb = physicsApi();
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), worldRef);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), worldRef);
|
||||
const d = runtime?.pointSegmentDistance?.(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y) ?? Infinity;
|
||||
const tolerance = worldRef?.screenSizeToWorld ? worldRef.screenSizeToWorld(12) : 12;
|
||||
if (d <= tolerance && !blockedReason) blockedReason = "link";
|
||||
|
|
@ -754,20 +783,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.rotatorPowered = item.rotatorPowered !== false;
|
||||
data.rotatorSpeed = Number(item.rotatorSpeed || 0) || 0;
|
||||
data.rotatorThickness = Number(item.rotatorThickness || 12) || 12;
|
||||
data.rotatorSegments = cloneSegmentsForRotator(item);
|
||||
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) };
|
||||
}
|
||||
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.reciprocatorPowered = item.reciprocatorPowered !== false;
|
||||
data.reciprocatorSpeed = Number(item.reciprocatorSpeed || 92) || 92;
|
||||
data.reciprocatorTravel = Number(item.reciprocatorTravel || 150) || 150;
|
||||
data.reciprocatorPhase = Number(item.reciprocatorPhase || 0) || 0;
|
||||
data.reciprocatorDirection = Number(item.reciprocatorDirection || 1) || 1;
|
||||
data.reciprocatorAxisAngle = Number.isFinite(Number(item.reciprocatorAxisAngle)) ? Number(item.reciprocatorAxisAngle) : Number(item.angle || 0) || 0;
|
||||
data.rotatorThickness = Number(item.rotatorThickness || 12) || 12;
|
||||
data.rotatorSegments = cloneSegmentsForRotator(item);
|
||||
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) };
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
@ -804,30 +826,46 @@
|
|||
if (item.roles) item.roles.food = Boolean(item.storedFoodType);
|
||||
}
|
||||
if (item.type === "rotator") {
|
||||
item.rotatorPowered = data.rotatorPowered !== false;
|
||||
item.rotatorSpeed = Number(data.rotatorSpeed || 0) || 0;
|
||||
item.rotatorAngularVelocity = 0;
|
||||
item.rotatorThickness = Math.max(4, Math.min(34, Number(data.rotatorThickness || 12) || 12));
|
||||
item.rotatorSegments = Array.isArray(data.rotatorSegments) ? data.rotatorSegments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item);
|
||||
item.r = Math.max(item.r || 64, Math.min(460, Math.max(...item.rotatorSegments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + item.rotatorThickness + 8));
|
||||
const ph = data.physics || {};
|
||||
phySet(item, "motorOn", ph.motorOn !== false, "copy");
|
||||
phySet(item, "motorSpeed", Number(ph.motorSpeed || 0) || 0, "copy");
|
||||
phySet(item, "spin", 0, "copy");
|
||||
phySet(item, "thickness", Math.max(4, Math.min(34, Number(ph.thickness || 12) || 12)), "copy");
|
||||
physicsApi()?.setSegments?.(item, Array.isArray(ph.segments) ? ph.segments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item), "copy");
|
||||
item.r = Math.max(item.r || 64, Math.min(460, physicsShapeExtent(item)));
|
||||
}
|
||||
if (item.type === "poison_block") {
|
||||
const ph = data.physics || {};
|
||||
phySet(item, "solid", ph.solid !== false, "copy");
|
||||
phySet(item, "damage", Math.max(1, Number(ph.damage || 7) || 7), "copy");
|
||||
phySet(item, "spin", 0, "copy");
|
||||
phySet(item, "thickness", Math.max(4, Math.min(34, Number(ph.thickness || 14) || 14)), "copy");
|
||||
physicsApi()?.setSegments?.(item, Array.isArray(ph.segments) ? ph.segments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item), "copy");
|
||||
const extent = physicsShapeExtent(item);
|
||||
item.r = Math.max(item.r || 64, Math.min(460, extent));
|
||||
}
|
||||
if (item.type === "reciprocator") {
|
||||
item.reciprocatorPowered = data.reciprocatorPowered !== false;
|
||||
item.reciprocatorSpeed = Math.max(0, Number(data.reciprocatorSpeed || 92) || 92);
|
||||
item.reciprocatorTravel = Math.max(24, Number(data.reciprocatorTravel || 150) || 150);
|
||||
item.reciprocatorPhase = Math.max(-1, Math.min(1, Number(data.reciprocatorPhase || 0) || 0));
|
||||
item.reciprocatorDirection = Number(data.reciprocatorDirection || 1) || 1;
|
||||
item.reciprocatorAxisAngle = Number.isFinite(Number(data.reciprocatorAxisAngle)) ? Number(data.reciprocatorAxisAngle) : (Number(item.reciprocatorAxisAngle || item.angle || 0) || 0);
|
||||
item.reciprocatorVelocity = 0;
|
||||
item.rotatorThickness = Math.max(4, Math.min(34, Number(data.rotatorThickness || 12) || 12));
|
||||
item.rotatorSegments = Array.isArray(data.rotatorSegments) ? data.rotatorSegments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item);
|
||||
const sa = window.TarinaiMechanicalSystem?.reciprocatorAxisAngle?.(item) ?? (Number.isFinite(Number(item.reciprocatorAxisAngle)) ? Number(item.reciprocatorAxisAngle) : (typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item.angle) || 0)));
|
||||
const halfTravel = Math.max(12, item.reciprocatorTravel * 0.5);
|
||||
item.reciprocatorAnchorX = item.x - Math.cos(sa) * halfTravel * item.reciprocatorPhase;
|
||||
item.reciprocatorAnchorY = item.y - Math.sin(sa) * halfTravel * item.reciprocatorPhase;
|
||||
const extent = Math.max(...item.rotatorSegments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + item.rotatorThickness + 8;
|
||||
const ph = data.physics || {};
|
||||
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");
|
||||
phySet(item, "railPhase", Math.max(-1, Math.min(1, Number(ph.railPhase || 0) || 0)), "copy");
|
||||
phySet(item, "railDir", Number(ph.railDir || 1) || 1, "copy");
|
||||
phySet(item, "railAxis", Number.isFinite(Number(ph.railAxis)) ? Number(ph.railAxis) : (Number(item.angle || 0) || 0), "copy");
|
||||
phySet(item, "slideSpeed", 0, "copy");
|
||||
phySet(item, "thickness", Math.max(4, Math.min(34, Number(ph.thickness || 12) || 12)), "copy");
|
||||
physicsApi()?.setSegments?.(item, Array.isArray(ph.segments) ? ph.segments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item), "copy");
|
||||
const sa = window.TarinaiMechanicalSystem?.railAxisAngle?.(item) ?? phyScalar(item, "railAxis", typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item.angle) || 0));
|
||||
const halfTravel = Math.max(12, phyScalar(item, "railTravel", 150) * 0.5);
|
||||
const phase = Number(phyScalar(item, "railPhase", 0) || 0) || 0;
|
||||
phySet(item, "railAnchorX", item.x - Math.cos(sa) * halfTravel * phase, "copy");
|
||||
phySet(item, "railAnchorY", item.y - Math.sin(sa) * halfTravel * phase, "copy");
|
||||
const extent = physicsShapeExtent(item);
|
||||
item.r = Math.max(item.r || 64, Math.min(460, extent));
|
||||
}
|
||||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) {
|
||||
if (!global.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "item-data-restore")) global.TarinaiMechanicalSystem.invalidateGeometry?.(item);
|
||||
}
|
||||
if (typeof isServingFoodType === "function" && isServingFoodType(item.type)) {
|
||||
item.toolSize = data.toolSize || item.toolSize || "medium";
|
||||
item.foodServingScale = Number(data.foodServingScale || item.foodServingScale || 1) || 1;
|
||||
|
|
@ -855,7 +893,7 @@
|
|||
|
||||
|
||||
function isEditableItem(item) {
|
||||
return item && !item.dead && (item.type === "signboard" || item.type === "gate_fence" || item.type === "rotator" || item.type === "reciprocator");
|
||||
return item && !item.dead && (item.type === "signboard" || item.type === "gate_fence" || item.type === "rotator" || item.type === "poison_block" || item.type === "reciprocator");
|
||||
}
|
||||
|
||||
function findEditableItemAt(worldRef, x, y) {
|
||||
|
|
@ -879,7 +917,7 @@
|
|||
function openEditableItemEditor(item, worldRef) {
|
||||
if (!isEditableItem(item)) return false;
|
||||
if (item.type === "signboard") return openSignboardEditor(item, worldRef);
|
||||
if (item.type === "rotator") return openRotatorEditor(item, worldRef);
|
||||
if (item.type === "rotator" || item.type === "poison_block") return openRotatorEditor(item, worldRef);
|
||||
if (item.type === "reciprocator") return openReciprocatorEditor(item, worldRef);
|
||||
if (item.type === "gate_fence") {
|
||||
item.gateOpen = !item.gateOpen;
|
||||
|
|
@ -960,8 +998,9 @@
|
|||
const r = softItemRadius(softItem);
|
||||
if (physicsItem.type === "rope" || physicsItem.type === "rod") {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const a = runtime?.endpointWorld?.(physicsItem.linkA, worldRef);
|
||||
const b = runtime?.endpointWorld?.(physicsItem.linkB, worldRef);
|
||||
const pb = physicsApi();
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(physicsItem, 0), worldRef);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(physicsItem, 1), worldRef);
|
||||
if (a && b) {
|
||||
const d = runtime?.pointSegmentDistance?.(softItem.x, softItem.y, a.x, a.y, b.x, b.y);
|
||||
return Number.isFinite(d) && d <= r + (physicsItem.type === "rod" ? 9 : 7);
|
||||
|
|
@ -981,7 +1020,7 @@
|
|||
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) {
|
||||
search = Math.max(search, (global.TarinaiMechanicalSystem?.reach?.(item) || item.r || 80) + 96);
|
||||
} else if (item.type === "rope" || item.type === "rod") {
|
||||
search = Math.max(search, Number(item.linkLength || (item.r || 40) * 2 || 80) * 0.55 + 96);
|
||||
search = Math.max(search, Number(physicsApi()?.linkScalar?.(item, "len", (item.r || 40) * 2 || 80)) * 0.55 + 96);
|
||||
}
|
||||
let removed = 0;
|
||||
for (const soft of worldRef.nearbyItems?.(item.x, item.y, search, true) || worldRef.items || []) {
|
||||
|
|
@ -1330,11 +1369,12 @@
|
|||
}
|
||||
if (mechanismTarget) {
|
||||
if (global.TarinaiMechanicalSystem?.applyPokeImpulse?.(mechanismTarget, x, y, this)) {
|
||||
const label = mechanismTarget.type === "rotator" ? "回転体" : "往復体";
|
||||
this.effects?.push(new Effect("ring", x, y, { size: 22, life: 0.18, color: mechanismTarget.type === "rotator" ? "rgba(184,132,230,0.40)" : "rgba(90,150,214,0.40)" }));
|
||||
const label = mechanismTarget.type === "rotator" ? "回転体" : (mechanismTarget.type === "poison_block" ? "毒ブロック" : "往復体");
|
||||
const color = mechanismTarget.type === "rotator" ? "rgba(184,132,230,0.40)" : (mechanismTarget.type === "poison_block" ? "rgba(92,188,68,0.40)" : "rgba(90,150,214,0.40)");
|
||||
this.effects?.push(new Effect("ring", x, y, { size: 22, life: 0.18, color }));
|
||||
this.drawListDirty = true;
|
||||
audio.poke?.();
|
||||
this.log(`動力OFFの${label}をつついて動かした。`, "observe", { eventType: "mechanism_poke", hiddenFromObservation: true });
|
||||
this.log(`${label}をつついて動かした。`, "observe", { eventType: "mechanism_poke", hiddenFromObservation: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1377,7 +1417,7 @@
|
|||
const rawItem = this.applyToolSize(new Item(itemType, px, py));
|
||||
if (typeof isRotatableItemType === "function" && isRotatableItemType(itemType)) rawItem.angle = this.toolAngleFor ? this.toolAngleFor(itemType) : (typeof defaultItemAngle === "function" ? defaultItemAngle(itemType) : 0);
|
||||
if (itemType === "reciprocator") {
|
||||
rawItem.reciprocatorAxisAngle = rawItem.angle;
|
||||
global.TarinaiPhysicsBodySystem?.setScalar?.(rawItem, "railAxis", rawItem.angle, "raw-item-axis");
|
||||
rawItem.angle = typeof defaultItemAngle === "function" ? defaultItemAngle(itemType) : 0;
|
||||
}
|
||||
// Placement uses the same footprint that the preview validates.
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@
|
|||
bedConflictRuns: 0,
|
||||
bedConflictSkips: 0,
|
||||
};
|
||||
this.spatialDirtyReasonCountsThisFrame = {};
|
||||
this.spatialRebuildReasonCountsThisFrame = {};
|
||||
this.deferredSpatialReadsThisFrame = 0;
|
||||
this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} };
|
||||
this.nearbyQueryStatsThisFrame = { obstacleRectHits: 0, obstacleRectMisses: 0, obstacleRectBypass: 0, obstacleRectBuilt: 0 };
|
||||
this.constraintDirtyStatsThisFrame = { marked: 0, coalesced: 0 };
|
||||
this._solidObstacleRectQueryCache = new Map();
|
||||
},
|
||||
|
||||
spendScheduledWork(kind = "ai", urgent = false) {
|
||||
|
|
@ -77,6 +84,9 @@
|
|||
rebuildsTotal: this.spatialRebuildsTotal || 0,
|
||||
partialRebuildsTotal: this.spatialPartialRebuildsTotal || 0,
|
||||
lastRebuildReason: this.lastSpatialRebuildReason || "",
|
||||
dirtyReasons: this.spatialDirtyReasonCountsThisFrame || {},
|
||||
rebuildReasons: this.spatialRebuildReasonCountsThisFrame || {},
|
||||
deferredReads: this.deferredSpatialReadsThisFrame || 0,
|
||||
},
|
||||
render: {
|
||||
drawList: (this.drawList || []).length,
|
||||
|
|
@ -105,6 +115,10 @@
|
|||
stats: this.workStats || null,
|
||||
},
|
||||
scheduler: this._itemUpdateSchedulerStats || null,
|
||||
creatures: this._creatureUpdateStats || null,
|
||||
terrain: this.terrainDirtyStatsThisFrame || null,
|
||||
nearby: this.nearbyQueryStatsThisFrame || null,
|
||||
constraintDirty: this.constraintDirtyStatsThisFrame || null,
|
||||
perf: window.TarinaiPerf?.snapshot?.() || null,
|
||||
};
|
||||
},
|
||||
|
|
@ -146,10 +160,32 @@
|
|||
ensureSpatial(reason = "read") {
|
||||
if (!this.spatial) return false;
|
||||
if (!this.spatialDirty && (this.spatialVersion || 0) > 0) return false;
|
||||
// Rebuild at most once per frame. Queries after movement may use the previous
|
||||
// frame's grid until the next update-start; this is acceptable for this game
|
||||
// and avoids repeated full-grid rebuilds under dense scenes.
|
||||
if ((this.spatialRebuildsThisFrame || 0) > 0 && (this.spatialVersion || 0) > 0) return false;
|
||||
|
||||
// During the creature pass, Tarinai movement marks only the Tarinai bucket
|
||||
// dirty many times. Obstacle/static item cells are still valid, so spatial
|
||||
// reads can use the frame-start Tarinai grid and collapse those invalidations
|
||||
// into one post-creature rebuild. Do not defer static/dynamic/ant dirtiness:
|
||||
// those buckets are used by collision and contact queries and need fresh data.
|
||||
const inDeferredPhase = (this.deferSpatialRebuildDepth || 0) > 0 && (this.spatialVersion || 0) > 0;
|
||||
const canDeferTarinaiOnly = inDeferredPhase
|
||||
&& this.spatialTarinaiDirty
|
||||
&& !this.spatialStaticItemsDirty
|
||||
&& !this.spatialDynamicItemsDirty
|
||||
&& !this.spatialAntDirty;
|
||||
const canDeferMobileOnly = inDeferredPhase
|
||||
&& String(this.deferSpatialRebuildMode || "") === "mobile"
|
||||
&& !this.spatialStaticItemsDirty
|
||||
&& (this.spatialDynamicItemsDirty || this.spatialTarinaiDirty || this.spatialAntDirty);
|
||||
if (canDeferTarinaiOnly || canDeferMobileOnly) {
|
||||
this.deferredSpatialDirtyReason = reason;
|
||||
this.deferredSpatialReadsThisFrame = (this.deferredSpatialReadsThisFrame || 0) + 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collision/contact queries must not read a previous-grid position after
|
||||
// moving static/dynamic obstacles in the same frame. Partial rebuilds are
|
||||
// already split by static/dynamic/tarinai/ant buckets, so keep those reads
|
||||
// correct here and let dirty flags decide how much work is required.
|
||||
return this.rebuildSpatial(true, reason);
|
||||
},
|
||||
|
||||
|
|
@ -231,6 +267,8 @@
|
|||
this.spatialRebuildsThisFrame = (this.spatialRebuildsThisFrame || 0) + 1;
|
||||
this.spatialRebuildsTotal = (this.spatialRebuildsTotal || 0) + 1;
|
||||
this.lastSpatialRebuildReason = reason;
|
||||
if (!this.spatialRebuildReasonCountsThisFrame) this.spatialRebuildReasonCountsThisFrame = {};
|
||||
this.spatialRebuildReasonCountsThisFrame[String(reason || "manual")] = (this.spatialRebuildReasonCountsThisFrame[String(reason || "manual")] || 0) + 1;
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@
|
|||
let d = distXY(x, y, it.x, it.y);
|
||||
if (it.type === "rope" || it.type === "rod") {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const a = runtime?.endpointWorld?.(it.linkA, this);
|
||||
const b = runtime?.endpointWorld?.(it.linkB, this);
|
||||
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;
|
||||
if (d < 18 && d < bestD) { best = it; bestD = d; }
|
||||
continue;
|
||||
|
|
@ -79,8 +80,9 @@
|
|||
let d = distXY(x, y, it.x, it.y);
|
||||
if (it.type === "rope" || it.type === "rod") {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const a = runtime?.endpointWorld?.(it.linkA, this);
|
||||
const b = runtime?.endpointWorld?.(it.linkB, this);
|
||||
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;
|
||||
if (d <= radius + 8) candidates.push(it);
|
||||
continue;
|
||||
|
|
@ -137,7 +139,7 @@
|
|||
const hit = Math.max(18, (t.radius || 18) * 1.12);
|
||||
const d = distXY(x, y, t.x, t.y);
|
||||
if (d <= hit && d < bestD) {
|
||||
best = { kind: "tarinai", id: t.id, type: t.type || "tarinai", label: t.name || "たりない", x: t.x, y: t.y, localX: 0, localY: 0, center: true };
|
||||
best = { kind: "tarinai", id: t.id || "", liveToken: t.liveToken ?? null, _ref: t, type: t.type || "tarinai", label: t.name || "たりない", x: t.x, y: t.y, localX: 0, localY: 0, center: true };
|
||||
bestD = d;
|
||||
}
|
||||
}
|
||||
|
|
@ -152,8 +154,9 @@
|
|||
let d = distXY(x, y, it.x, it.y);
|
||||
if (isConstraint) {
|
||||
const runtime = global.TarinaiLinkRuntime;
|
||||
const a = runtime?.endpointWorld?.(it.linkA, this);
|
||||
const b = runtime?.endpointWorld?.(it.linkB, this);
|
||||
const pb = global.TarinaiPhysicsBodySystem;
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), this);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), this);
|
||||
if (!a || !b) continue;
|
||||
d = runtime?.pointSegmentDistance?.(x, y, a.x, a.y, b.x, b.y) ?? d;
|
||||
if (d > 20 || d >= bestD) continue;
|
||||
|
|
@ -208,14 +211,17 @@
|
|||
const constraintType = this.tool === "rod" ? "rod" : "rope";
|
||||
const item = new Item(constraintType, (p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
|
||||
item.world = this;
|
||||
item.linkA = { ...first, x: p1.x, y: p1.y };
|
||||
item.linkB = { ...endpoint, x: p2.x, y: p2.y };
|
||||
item.linkLength = Math.max(24, distXY(p1.x, p1.y, p2.x, p2.y));
|
||||
item.linkMidX = (p1.x + p2.x) * 0.5;
|
||||
item.linkMidY = (p1.y + p2.y) * 0.5;
|
||||
item.linkMidVX = 0;
|
||||
item.linkMidVY = 0;
|
||||
item.r = Math.max(18, item.linkLength * 0.5);
|
||||
const pb = global.TarinaiPhysicsBodySystem;
|
||||
pb?.ensureConstraint?.(item, this, { syncFromLegacy: false });
|
||||
pb?.setEndpoint?.(item, 0, { ...first, x: p1.x, y: p1.y });
|
||||
pb?.setEndpoint?.(item, 1, { ...endpoint, x: p2.x, y: p2.y });
|
||||
const linkLen = Math.max(24, distXY(p1.x, p1.y, p2.x, p2.y));
|
||||
pb?.setLinkScalar?.(item, "len", linkLen);
|
||||
pb?.setLinkScalar?.(item, "midX", (p1.x + p2.x) * 0.5);
|
||||
pb?.setLinkScalar?.(item, "midY", (p1.y + p2.y) * 0.5);
|
||||
pb?.setLinkScalar?.(item, "midVx", 0);
|
||||
pb?.setLinkScalar?.(item, "midVy", 0);
|
||||
item.r = Math.max(18, linkLen * 0.5);
|
||||
item.amount = 999;
|
||||
const placed = this.addItem?.(item, constraintType);
|
||||
this.pendingLinkEndpoint = null;
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@
|
|||
markSpatialDirty(reason = "manual") {
|
||||
this.spatialDirty = true;
|
||||
this.spatialDirtyReason = reason;
|
||||
if (!this.spatialDirtyReasonCountsThisFrame) this.spatialDirtyReasonCountsThisFrame = {};
|
||||
this.spatialDirtyReasonCountsThisFrame[String(reason || "manual")] = (this.spatialDirtyReasonCountsThisFrame[String(reason || "manual")] || 0) + 1;
|
||||
this.spatialDirtyMarksThisFrame = (this.spatialDirtyMarksThisFrame || 0) + 1;
|
||||
this.spatialDirtyMarksTotal = (this.spatialDirtyMarksTotal || 0) + 1;
|
||||
this.lastSpatialDirtyReason = reason;
|
||||
|
|
@ -50,7 +52,7 @@
|
|||
this.spatialTarinaiDirty = true;
|
||||
} else if (/ants?-moved|post-ant|ant/i.test(r) && !/ant_nest|ant-nest/i.test(r)) {
|
||||
this.spatialAntDirty = true;
|
||||
} else if (/items?-moved|ball-ball|mobile|kinematic|pushpin|oshibyo|firecracker|genkotsu|zunchi/i.test(r)) {
|
||||
} else if (/items?-moved|ball-ball|mobile|kinematic|pushpin|oshibyo|firecracker|genkotsu|zunchi|mechanical|rotator|reciprocator|poison-block|constraint|rod-/i.test(r)) {
|
||||
this.spatialDynamicItemsDirty = true;
|
||||
} else {
|
||||
this.spatialStaticItemsDirty = true;
|
||||
|
|
@ -61,27 +63,52 @@
|
|||
},
|
||||
|
||||
markTerrainDirty(reason = "manual") {
|
||||
const reasonKey = String(reason || "manual");
|
||||
if (!this.terrainDirtyStatsThisFrame) this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} };
|
||||
this.terrainDirtyStatsThisFrame.raw += 1;
|
||||
this.terrainDirtyStatsThisFrame.global += 1;
|
||||
this.terrainDirtyStatsThisFrame.reasons[reasonKey] = (this.terrainDirtyStatsThisFrame.reasons[reasonKey] || 0) + 1;
|
||||
if (this.terrainDirtyGlobal && this.terrainDirtyReason === reasonKey) {
|
||||
this.terrainDirtyStatsThisFrame.coalesced += 1;
|
||||
this.terrainDirty = true;
|
||||
this.terrainLastDirtyAt = this.time || 0;
|
||||
return;
|
||||
}
|
||||
this.terrainDirty = true;
|
||||
this.terrainDirtyGlobal = true;
|
||||
this.terrainDirtyReason = reason;
|
||||
this.terrainDirtyReason = reasonKey;
|
||||
this.terrainVersion = (this.terrainVersion || 0) + 1;
|
||||
this.terrainLastDirtyAt = this.time || 0;
|
||||
this.emit("terrain:dirty", { reason, terrainVersion: this.terrainVersion, global: true });
|
||||
this.emit("terrain:dirty", { reason: reasonKey, terrainVersion: this.terrainVersion, global: true });
|
||||
},
|
||||
|
||||
markTerrainDirtyAt(x, y, radius = 48, reason = "manual") {
|
||||
const reasonKey = String(reason || "manual");
|
||||
const cx = Math.floor((Number(x) || 0) / 256);
|
||||
const cy = Math.floor((Number(y) || 0) / 256);
|
||||
const cr = Math.max(0, Math.ceil((Number(radius) || 0) / 256));
|
||||
if (!this.terrainDirtyStatsThisFrame) this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} };
|
||||
this.terrainDirtyStatsThisFrame.raw += 1;
|
||||
this.terrainDirtyStatsThisFrame.chunk += 1;
|
||||
this.terrainDirtyStatsThisFrame.reasons[reasonKey] = (this.terrainDirtyStatsThisFrame.reasons[reasonKey] || 0) + 1;
|
||||
if (!this.terrainDirtyChunks) this.terrainDirtyChunks = new Set();
|
||||
let added = 0;
|
||||
for (let yy = cy - cr; yy <= cy + cr; yy++) {
|
||||
for (let xx = cx - cr; xx <= cx + cr; xx++) this.terrainDirtyChunks.add(`${xx}:${yy}`);
|
||||
for (let xx = cx - cr; xx <= cx + cr; xx++) {
|
||||
const key = `${xx}:${yy}`;
|
||||
if (!this.terrainDirtyChunks.has(key)) added += 1;
|
||||
this.terrainDirtyChunks.add(key);
|
||||
}
|
||||
}
|
||||
this.terrainDirty = true;
|
||||
this.terrainDirtyReason = reason;
|
||||
this.terrainVersion = (this.terrainVersion || 0) + 1;
|
||||
this.terrainDirtyReason = reasonKey;
|
||||
this.terrainLastDirtyAt = this.time || 0;
|
||||
this.emit("terrain:dirty", { reason, terrainVersion: this.terrainVersion, global: false, x, y, radius });
|
||||
if (added <= 0) {
|
||||
this.terrainDirtyStatsThisFrame.coalesced += 1;
|
||||
return true;
|
||||
}
|
||||
this.terrainVersion = (this.terrainVersion || 0) + 1;
|
||||
this.emit("terrain:dirty", { reason: reasonKey, terrainVersion: this.terrainVersion, global: false, x, y, radius });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -194,6 +194,26 @@ def check_legacy_behavior_refs() -> None:
|
|||
|
||||
|
||||
|
||||
def check_legacy_physics_props_removed() -> None:
|
||||
terms = [
|
||||
"rotatorThickness", "rotatorSegments", "rotatorPowered", "rotatorSpeed", "rotatorAngularVelocity",
|
||||
"poisonCollisionEnabled", "poisonDamage", "poisonMass", "poisonInertia",
|
||||
"reciprocatorPowered", "reciprocatorSpeed", "reciprocatorTravel", "reciprocatorPhase",
|
||||
"reciprocatorDirection", "reciprocatorAxisAngle", "reciprocatorVelocity", "reciprocatorAnchorX", "reciprocatorAnchorY",
|
||||
"linkA", "linkB", "linkLength", "linkMidX", "linkMidY", "linkMidVX", "linkMidVY",
|
||||
"ropeParticles", "_physicsAwakeUntil", "_linkAwakeUntil",
|
||||
]
|
||||
offenders = []
|
||||
for path in (ROOT / "js").glob("*.js"):
|
||||
text = read_text(path)
|
||||
for term in terms:
|
||||
if term in text:
|
||||
offenders.append(f"{path.relative_to(ROOT).as_posix()}:{term}")
|
||||
if offenders:
|
||||
fail("legacy physics property names still present: " + ", ".join(offenders[:20]))
|
||||
ok("legacy physics property names removed")
|
||||
|
||||
|
||||
def check_basic_action_spec_runtime() -> None:
|
||||
source = "\n".join(read_text(ROOT / rel) for rel in ["js/tarinai_action_definitions.js", "js/tarinai_needs_items.js"])
|
||||
required = {
|
||||
|
|
@ -1232,6 +1252,7 @@ def main() -> None:
|
|||
check_processing_split()
|
||||
command_dispatcher_smoke()
|
||||
check_legacy_behavior_refs()
|
||||
check_legacy_physics_props_removed()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "39.00.00";
|
||||
const APP_VERSION = "39.15.18";
|
||||
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.
|
||||
const coreScriptNames = [
|
||||
"version", "event_bus", "registry_base", "disease_registry", "sound_pack", "item_registry", "data", "ground_types", "input_mode_manager", "math", "physics_helpers", "collision_footprint_system", "mechanical_system", "constraint_system", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_support", "item_lifecycle_runtime", "item_update_policy", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_forced_action_planner_system", "tarinai_action_planner_system", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "world_update_phases", "system_order", "simulation", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "patch_helpers", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
"version", "event_bus", "registry_base", "disease_registry", "sound_pack", "item_registry", "data", "ground_types", "input_mode_manager", "math", "physics_helpers", "collision_footprint_system", "mechanical_system", "constraint_system", "physics_world_system", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_support", "item_lifecycle_runtime", "item_update_policy", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_forced_action_planner_system", "tarinai_action_planner_system", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "world_update_phases", "system_order", "simulation", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "patch_helpers", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
];
|
||||
const CORE_ASSETS = [
|
||||
"./",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue