removed unused processesseessess

This commit is contained in:
33333-33333 2026-06-29 16:21:58 +09:00
commit 4fdc567e08
158 changed files with 1317 additions and 3351 deletions

View file

@ -32,7 +32,7 @@
},
{
"id": "core",
"patterns": ["js/version.js", "js/event_bus.js", "js/registry_base.js", "js/disease_registry.js", "js/sound_pack.js", "js/data.js", "js/ground_types.js", "js/health.js", "js/input_mode_manager.js", "js/main.js", "js/math.js", "js/patch_helpers.js", "js/perf_profiler.js", "js/debug_tools.js"],
"patterns": ["js/version.js", "js/event_bus.js", "js/registry_base.js", "js/disease_registry.js", "js/sound_pack.js", "js/data.js", "js/ground_types.js", "js/health.js", "js/input_mode_manager.js", "js/main.js", "js/math.js", "js/perf_profiler.js", "js/debug_tools.js"],
"meaning": "Bootstrap, shared constants/helpers, registries, config, profiling/debug."
},
{

View file

@ -1,5 +1,5 @@
{
"version": "39.15.63",
"version": "39.15.65",
"css": [
"css/base.css",
"css/layout.css",
@ -25,7 +25,6 @@
"js/math.js",
"js/collision_footprint_system.js",
"js/physics_helpers.js",
"js/placement_collision_system.js",
"js/placement_preview_system.js",
"js/pin_attachment_system.js",
"js/physics_shape_editor_system.js",
@ -44,7 +43,6 @@
"js/items.js",
"js/item_type_initializers.js",
"js/item_lifecycle_support.js",
"js/item_lifecycle_runtime.js",
"js/item_update_policy.js",
"js/item_dynamic_tool_system.js",
"js/item_dynamic_ball_system.js",
@ -81,8 +79,6 @@
"js/tarinai_building_behavior.js",
"js/tarinai_action_definitions.js",
"js/tarinai_needs_items.js",
"js/tarinai_forced_action_planner_system.js",
"js/tarinai_action_planner_system.js",
"js/tarinai_need_planner_system.js",
"js/tarinai_item_interaction_context.js",
"js/tarinai_food_interaction_system.js",
@ -122,7 +118,6 @@
"js/simulation_maintenance_system.js",
"js/simulation_ambient_system.js",
"js/simulation_systems.js",
"js/world_update_phases.js",
"js/system_order.js",
"js/simulation.js",
"js/colony_situation_system.js",
@ -143,7 +138,6 @@
"js/save_codec.js",
"js/save_storage.js",
"js/save_system.js",
"js/patch_helpers.js",
"js/ui_tooltips.js",
"js/ui_layout_dialogs.js",
"js/ui_ground.js",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -74,25 +74,6 @@ button {
line-height: 1.55;
min-height: 1.55em;
}
.loading-bar {
height: 12px;
margin: 18px 0 8px;
border-radius: 999px;
overflow: hidden;
background: rgba(90, 70, 48, 0.12);
}
.loading-bar-fill {
width: 0%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #9ce84b, #f0d35b);
transition: width 160ms ease;
}
.loading-percent {
color: var(--muted);
font-size: 12px;
font-variant-numeric: tabular-nums;
}
#app {
width: min(1480px, 100vw);
@ -137,13 +118,6 @@ h1 {
white-space: nowrap;
}
.subtitle {
margin: 0;
color: var(--muted);
font-size: clamp(11px, 1.1vw, 13px);
line-height: 1.35;
white-space: nowrap;
}
.top-actions {
display: flex;
@ -429,7 +403,6 @@ h1 {
z-index: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool-sprite-watermark { display: none !important; }
.tool[data-tool="giant_drug"] .tool-sprite-preview { width: 39px; height: 39px; left: -4px; }
.tool[data-tool="dwarf_drug"] .tool-sprite-preview,
.tool[data-tool="oshibyo"] .tool-sprite-preview { width: 22px; height: 22px; left: 5px; }

View file

@ -246,12 +246,6 @@
font-size: 12px;
color: rgba(88, 73, 60, 0.82);
}
.loading-hint {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.55;
color: rgba(111, 92, 73, 0.78);
}
.collapsible-card {
padding-top: 10px;

View file

@ -74,8 +74,6 @@
margin: 10px 0 0;
}
.tool-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.tool-head h2 { margin-bottom: 0; }
.btn.mini { padding: 6px 10px; font-size: 12px; }
.tool-body { margin-top: 10px; }
.tool-palette { display: flex; flex-direction: column; gap: 8px; }
@ -168,14 +166,6 @@
.stat span { display: block; font-size: 11px; color: var(--muted); }
.stat strong { font-size: 18px; }
.meter-row {
display: grid;
grid-template-columns: 72px 1fr;
align-items: center;
gap: 8px;
font-size: 12px;
margin: 6px 0;
}
meter {
width: 100%;
height: 12px;
@ -340,16 +330,6 @@ meter {
.tree-note {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
margin: 0 0 10px;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255,255,255,0.54);
border: 1px solid rgba(84,68,48,0.10);
}
/* Clearer tool category grouping. */

View file

@ -1,41 +1,3 @@
.sound-category-controls,
.log-push-controls {
margin-top: 10px;
padding: 10px;
border-radius: 14px;
border: 1px solid rgba(84,68,48,.12);
background: rgba(255,255,255,.46);
font-size: 12px;
}
.control-group-title { font-weight: 700; margin-bottom: 7px; color: rgba(58,48,39,.88); }
.sound-category-controls { display: grid; grid-template-columns: 1fr; gap: 6px; }
.sound-category-controls label,
.push-master,
.push-kind-grid label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
.sound-category-controls input,
.push-master input,
.push-kind-grid input { accent-color: #82bf66; }
.log-push-controls { display: flex; flex-direction: column; gap: 8px; }
.push-kind-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px 8px; }
.log-push-overlay { position: fixed; left: 18px; bottom: 18px; z-index: 9998; width: min(320px, calc(100vw - 36px)); display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
.log-push-toast { position: relative; overflow: hidden; padding: 10px 12px 10px 17px; border-radius: 15px; background: rgba(255,252,246,.94); border: 1px solid rgba(84,68,48,.16); box-shadow: 0 12px 32px rgba(44,34,25,.18); animation: logPushIn .18s ease-out; }
.log-push-toast::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 5px; background: #a5c77a; }
.log-push-toast .log-push-tag { display: inline-block; margin-right: 6px; font-size: 11px; font-weight: 700; color: rgba(54,45,38,.88); }
.log-push-toast .log-push-time { font-size: 10px; color: rgba(104,88,72,.72); }
.log-push-toast .log-push-text { margin-top: 3px; font-size: 12px; line-height: 1.35; color: rgba(48,40,33,.92); }
.log-push-toast.log-push-death::before { background: #8f817a; }
.log-push-toast.log-push-accident::before { background: #ee9e5d; }
.log-push-toast.log-push-birth::before { background: #98cf7a; }
.log-push-toast.log-push-fight::before { background: #e47b80; }
.log-push-toast.log-push-relation::before { background: #b99be0; }
.log-push-toast.log-push-food::before { background: #e9bd61; }
.log-push-toast.log-push-weather::before { background: #7bbbdc; }
.log-push-toast.log-push-grass::before { background: #8cc77a; }
.log-push-toast.log-push-event::before { background: #8b82d6; }
.log-push-toast.leaving { opacity: 0; transform: translateY(8px); transition: opacity .22s ease, transform .22s ease; }
@keyframes logPushIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
/* v15.12.1: top-right sound settings panel */
.sound-menu { position: relative; display: inline-block; }
.sound-menu-btn.active { background: linear-gradient(180deg, #f3ffe9, #e7f8db); border-color: rgba(103,178,44,.46); }
@ -71,9 +33,6 @@
}
/* v15.12.4: sound settings live only in the top-right sound menu */
.log-card .sound-category-controls {
display: none !important;
}
/* v15.17.0: mobile layout isolation and canvas touch gestures */
#app { overflow-x: clip; }

View file

@ -1,9 +1,4 @@
/* CSS split file; preserve manifest load order. */
.lineage-tree {
overflow: auto;
padding: 8px 10px 18px;
contain: content;
}
.lineage-main-card {
margin-top: 16px;
@ -52,11 +47,6 @@
gap: 12px;
}
.lineage-main-content .lineage-tree {
min-width: 360px;
overflow: visible;
padding-right: 2px;
}
body::before {
@ -192,14 +182,6 @@ body::before {
text-overflow: ellipsis;
white-space: nowrap;
}
.family-copy .death-reason {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.12;
margin-top: 1px;
color: rgba(126, 80, 64, 0.92);
}
.family-personality-tags {
display: grid !important;
grid-template-columns: repeat(2, minmax(0, 1fr));
@ -314,20 +296,6 @@ body::before {
background: rgba(255,253,248,0.82);
border-radius: 8px;
}
.lineage-tree-hint {
position: sticky;
left: 0;
z-index: 3;
width: max-content;
max-width: 100%;
margin: -2px 0 8px;
padding: 5px 8px;
border-radius: 9px;
color: var(--muted);
font-size: 11px;
background: rgba(255,255,255,0.62);
border: 1px solid rgba(84,68,48,0.08);
}
.lineage-family-tree .lineage-tree-panel {
position: relative;
overflow: visible;

View file

@ -7,11 +7,11 @@
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
<link rel="stylesheet" href="css/base.css?v=39.15.63" />
<link rel="stylesheet" href="css/layout.css?v=39.15.63" />
<link rel="stylesheet" href="css/panel.css?v=39.15.63" />
<link rel="stylesheet" href="css/components.css?v=39.15.63" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.63" />
<link rel="stylesheet" href="css/base.css?v=39.15.65" />
<link rel="stylesheet" href="css/layout.css?v=39.15.65" />
<link rel="stylesheet" href="css/panel.css?v=39.15.65" />
<link rel="stylesheet" href="css/components.css?v=39.15.65" />
<link rel="stylesheet" href="css/mobile.css?v=39.15.65" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -201,158 +201,152 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=39.15.63" defer></script>
<script src="js/event_bus.js?v=39.15.63" defer></script>
<script src="js/registry_base.js?v=39.15.63" defer></script>
<script src="js/disease_registry.js?v=39.15.63" defer></script>
<script src="js/sound_pack.js?v=39.15.63" defer></script>
<script src="js/deterministic_helpers.js?v=39.15.63" defer></script>
<script src="js/item_tool_definitions.js?v=39.15.63" defer></script>
<script src="js/item_visual_definitions.js?v=39.15.63" defer></script>
<script src="js/item_food_definitions.js?v=39.15.63" defer></script>
<script src="js/item_effect_definitions.js?v=39.15.63" defer></script>
<script src="js/item_registry.js?v=39.15.63" defer></script>
<script src="js/data.js?v=39.15.63" defer></script>
<script src="js/ground_types.js?v=39.15.63" defer></script>
<script src="js/input_mode_manager.js?v=39.15.63" defer></script>
<script src="js/math.js?v=39.15.63" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.63" defer></script>
<script src="js/physics_helpers.js?v=39.15.63" defer></script>
<script src="js/placement_collision_system.js?v=39.15.63" defer></script>
<script src="js/placement_preview_system.js?v=39.15.63" defer></script>
<script src="js/pin_attachment_system.js?v=39.15.63" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.15.63" defer></script>
<script src="js/mechanical_system.js?v=39.15.63" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.15.63" defer></script>
<script src="js/constraint_system.js?v=39.15.63" defer></script>
<script src="js/physics_world_system.js?v=39.15.63" defer></script>
<script src="js/physics_projection_system.js?v=39.15.63" defer></script>
<script src="js/assets.js?v=39.15.63" defer></script>
<script src="js/audio.js?v=39.15.63" defer></script>
<script src="js/perf_profiler.js?v=39.15.63" defer></script>
<script src="js/render.js?v=39.15.63" defer></script>
<script src="js/sim_core.js?v=39.15.63" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.63" defer></script>
<script src="js/structures.js?v=39.15.63" defer></script>
<script src="js/items.js?v=39.15.63" defer></script>
<script src="js/item_type_initializers.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_runtime.js?v=39.15.63" defer></script>
<script src="js/item_update_policy.js?v=39.15.63" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.63" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.63" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.63" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.63" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.63" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.63" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.63" defer></script>
<script src="js/item_runtime.js?v=39.15.63" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.63" defer></script>
<script src="js/item_render_runtime.js?v=39.15.63" defer></script>
<script src="js/ants.js?v=39.15.63" defer></script>
<script src="js/health.js?v=39.15.63" defer></script>
<script src="js/tarinai.js?v=39.15.63" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.63" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.63" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.63" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.63" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.63" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.63" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.63" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.63" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.63" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.63" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.63" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.63" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.63" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.63" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.63" defer></script>
<script src="js/tarinai_forced_action_planner_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_action_planner_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.63" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.63" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.63" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.63" defer></script>
<script src="js/tarinai_render.js?v=39.15.63" defer></script>
<script src="js/world.js?v=39.15.63" defer></script>
<script src="js/world_view.js?v=39.15.63" defer></script>
<script src="js/family_graph.js?v=39.15.63" defer></script>
<script src="js/world_family_social.js?v=39.15.63" defer></script>
<script src="js/impact_core_system.js?v=39.15.63" defer></script>
<script src="js/impact_response_system.js?v=39.15.63" defer></script>
<script src="js/collision_response_system.js?v=39.15.63" defer></script>
<script src="js/world_combat_effects.js?v=39.15.63" defer></script>
<script src="js/world_environment.js?v=39.15.63" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.63" defer></script>
<script src="js/world_ants_system.js?v=39.15.63" defer></script>
<script src="js/weather_system.js?v=39.15.63" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.63" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.63" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.63" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.63" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.63" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.63" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.63" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.63" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.63" defer></script>
<script src="js/simulation_systems.js?v=39.15.63" defer></script>
<script src="js/world_update_phases.js?v=39.15.63" defer></script>
<script src="js/system_order.js?v=39.15.63" defer></script>
<script src="js/simulation.js?v=39.15.63" defer></script>
<script src="js/colony_situation_system.js?v=39.15.63" defer></script>
<script src="js/world_update.js?v=39.15.63" defer></script>
<script src="js/world_tool_actions.js?v=39.15.63" defer></script>
<script src="js/world_placement_log.js?v=39.15.63" defer></script>
<script src="js/world_event_effects.js?v=39.15.63" defer></script>
<script src="js/command_dispatcher.js?v=39.15.63" defer></script>
<script src="js/text_catalog.js?v=39.15.63" defer></script>
<script src="js/ui.js?v=39.15.63" defer></script>
<script src="js/ui_log.js?v=39.15.63" defer></script>
<script src="js/ui_helpers.js?v=39.15.63" defer></script>
<script src="js/ui_selected.js?v=39.15.63" defer></script>
<script src="js/save_schema.js?v=39.15.63" defer></script>
<script src="js/snapshot_system.js?v=39.15.63" defer></script>
<script src="js/restore_coordinator.js?v=39.15.63" defer></script>
<script src="js/history_system.js?v=39.15.63" defer></script>
<script src="js/save_codec.js?v=39.15.63" defer></script>
<script src="js/save_storage.js?v=39.15.63" defer></script>
<script src="js/save_system.js?v=39.15.63" defer></script>
<script src="js/patch_helpers.js?v=39.15.63" defer></script>
<script src="js/ui_tooltips.js?v=39.15.63" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.63" defer></script>
<script src="js/ui_ground.js?v=39.15.63" defer></script>
<script src="js/ui_tools.js?v=39.15.63" defer></script>
<script src="js/ui_input_shared.js?v=39.15.63" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.15.63" defer></script>
<script src="js/ui_input_touch.js?v=39.15.63" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.63" defer></script>
<script src="js/ui_bind.js?v=39.15.63" defer></script>
<script src="js/ui_charts.js?v=39.15.63" defer></script>
<script src="js/ui_family_data.js?v=39.15.63" defer></script>
<script src="js/ui_family_async.js?v=39.15.63" defer></script>
<script src="js/ui_family_layout.js?v=39.15.63" defer></script>
<script src="js/ui_family_paths.js?v=39.15.63" defer></script>
<script src="js/ui_family_render.js?v=39.15.63" defer></script>
<script src="js/main.js?v=39.15.63" defer></script>
<script src="js/debug_tools.js?v=39.15.63" defer></script>
<script src="js/version.js?v=39.15.65" defer></script>
<script src="js/event_bus.js?v=39.15.65" defer></script>
<script src="js/registry_base.js?v=39.15.65" defer></script>
<script src="js/disease_registry.js?v=39.15.65" defer></script>
<script src="js/sound_pack.js?v=39.15.65" defer></script>
<script src="js/deterministic_helpers.js?v=39.15.65" defer></script>
<script src="js/item_tool_definitions.js?v=39.15.65" defer></script>
<script src="js/item_visual_definitions.js?v=39.15.65" defer></script>
<script src="js/item_food_definitions.js?v=39.15.65" defer></script>
<script src="js/item_effect_definitions.js?v=39.15.65" defer></script>
<script src="js/item_registry.js?v=39.15.65" defer></script>
<script src="js/data.js?v=39.15.65" defer></script>
<script src="js/ground_types.js?v=39.15.65" defer></script>
<script src="js/input_mode_manager.js?v=39.15.65" defer></script>
<script src="js/math.js?v=39.15.65" defer></script>
<script src="js/collision_footprint_system.js?v=39.15.65" defer></script>
<script src="js/physics_helpers.js?v=39.15.65" defer></script>
<script src="js/placement_preview_system.js?v=39.15.65" defer></script>
<script src="js/pin_attachment_system.js?v=39.15.65" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.15.65" defer></script>
<script src="js/mechanical_system.js?v=39.15.65" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.15.65" defer></script>
<script src="js/constraint_system.js?v=39.15.65" defer></script>
<script src="js/physics_world_system.js?v=39.15.65" defer></script>
<script src="js/physics_projection_system.js?v=39.15.65" defer></script>
<script src="js/assets.js?v=39.15.65" defer></script>
<script src="js/audio.js?v=39.15.65" defer></script>
<script src="js/perf_profiler.js?v=39.15.65" defer></script>
<script src="js/render.js?v=39.15.65" defer></script>
<script src="js/sim_core.js?v=39.15.65" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.15.65" defer></script>
<script src="js/structures.js?v=39.15.65" defer></script>
<script src="js/items.js?v=39.15.65" defer></script>
<script src="js/item_type_initializers.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_support.js?v=39.15.65" defer></script>
<script src="js/item_update_policy.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.15.65" defer></script>
<script src="js/item_dynamic_system.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.15.65" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.15.65" defer></script>
<script src="js/item_runtime.js?v=39.15.65" defer></script>
<script src="js/structure_lifecycle.js?v=39.15.65" defer></script>
<script src="js/item_render_runtime.js?v=39.15.65" defer></script>
<script src="js/ants.js?v=39.15.65" defer></script>
<script src="js/health.js?v=39.15.65" defer></script>
<script src="js/tarinai.js?v=39.15.65" defer></script>
<script src="js/tarinai_action_spec.js?v=39.15.65" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.15.65" defer></script>
<script src="js/tarinai_identity_social.js?v=39.15.65" defer></script>
<script src="js/tarinai_action_state.js?v=39.15.65" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_effects.js?v=39.15.65" defer></script>
<script src="js/tarinai_needs_core.js?v=39.15.65" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.15.65" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.15.65" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.15.65" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.15.65" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.15.65" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.15.65" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.15.65" defer></script>
<script src="js/tarinai_needs_items.js?v=39.15.65" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.15.65" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.15.65" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.15.65" defer></script>
<script src="js/tarinai_runtime.js?v=39.15.65" defer></script>
<script src="js/tarinai_render.js?v=39.15.65" defer></script>
<script src="js/world.js?v=39.15.65" defer></script>
<script src="js/world_view.js?v=39.15.65" defer></script>
<script src="js/family_graph.js?v=39.15.65" defer></script>
<script src="js/world_family_social.js?v=39.15.65" defer></script>
<script src="js/impact_core_system.js?v=39.15.65" defer></script>
<script src="js/impact_response_system.js?v=39.15.65" defer></script>
<script src="js/collision_response_system.js?v=39.15.65" defer></script>
<script src="js/world_combat_effects.js?v=39.15.65" defer></script>
<script src="js/world_environment.js?v=39.15.65" defer></script>
<script src="js/world_spatial_budget.js?v=39.15.65" defer></script>
<script src="js/world_ants_system.js?v=39.15.65" defer></script>
<script src="js/weather_system.js?v=39.15.65" defer></script>
<script src="js/item_update_scheduler.js?v=39.15.65" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.15.65" defer></script>
<script src="js/tarinai_update_policy.js?v=39.15.65" defer></script>
<script src="js/simulation_environment_system.js?v=39.15.65" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.15.65" defer></script>
<script src="js/simulation_effects_system.js?v=39.15.65" defer></script>
<script src="js/simulation_creature_system.js?v=39.15.65" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.15.65" defer></script>
<script src="js/simulation_ambient_system.js?v=39.15.65" defer></script>
<script src="js/simulation_systems.js?v=39.15.65" defer></script>
<script src="js/system_order.js?v=39.15.65" defer></script>
<script src="js/simulation.js?v=39.15.65" defer></script>
<script src="js/colony_situation_system.js?v=39.15.65" defer></script>
<script src="js/world_update.js?v=39.15.65" defer></script>
<script src="js/world_tool_actions.js?v=39.15.65" defer></script>
<script src="js/world_placement_log.js?v=39.15.65" defer></script>
<script src="js/world_event_effects.js?v=39.15.65" defer></script>
<script src="js/command_dispatcher.js?v=39.15.65" defer></script>
<script src="js/text_catalog.js?v=39.15.65" defer></script>
<script src="js/ui.js?v=39.15.65" defer></script>
<script src="js/ui_log.js?v=39.15.65" defer></script>
<script src="js/ui_helpers.js?v=39.15.65" defer></script>
<script src="js/ui_selected.js?v=39.15.65" defer></script>
<script src="js/save_schema.js?v=39.15.65" defer></script>
<script src="js/snapshot_system.js?v=39.15.65" defer></script>
<script src="js/restore_coordinator.js?v=39.15.65" defer></script>
<script src="js/history_system.js?v=39.15.65" defer></script>
<script src="js/save_codec.js?v=39.15.65" defer></script>
<script src="js/save_storage.js?v=39.15.65" defer></script>
<script src="js/save_system.js?v=39.15.65" defer></script>
<script src="js/ui_tooltips.js?v=39.15.65" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.15.65" defer></script>
<script src="js/ui_ground.js?v=39.15.65" defer></script>
<script src="js/ui_tools.js?v=39.15.65" defer></script>
<script src="js/ui_input_shared.js?v=39.15.65" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.15.65" defer></script>
<script src="js/ui_input_touch.js?v=39.15.65" defer></script>
<script src="js/ui_input_mouse.js?v=39.15.65" defer></script>
<script src="js/ui_bind.js?v=39.15.65" defer></script>
<script src="js/ui_charts.js?v=39.15.65" defer></script>
<script src="js/ui_family_data.js?v=39.15.65" defer></script>
<script src="js/ui_family_async.js?v=39.15.65" defer></script>
<script src="js/ui_family_layout.js?v=39.15.65" defer></script>
<script src="js/ui_family_paths.js?v=39.15.65" defer></script>
<script src="js/ui_family_render.js?v=39.15.65" defer></script>
<script src="js/main.js?v=39.15.65" defer></script>
<script src="js/debug_tools.js?v=39.15.65" defer></script>
</body>
</html>

View file

@ -13,14 +13,6 @@ const ANT_WORKER_RETURN_WEAK_SPEED = 9.0;
const ANT_WORKER_DRAG_SPEED = 6.1875;
const ANT_QUEEN_SPEED = 13.5;
const ANT_EXTENSION_HOOKS = Object.freeze({
nestHunger: "reserved",
nestGrowth: "reserved",
queenReproduction: "existing-queen-founding",
workerLifecycle: "worker-pool-hp",
carryingSuccessRate: "current-deterministic-haul",
territorialBehavior: "reserved",
});
function clampAntHp(v) {
return clamp(Number(v) || 0, 0, ANT_WORKER_HP);
@ -84,7 +76,6 @@ class AntActor {
this.seed = opts.seed || Math.random() * 1000;
this.wanderAngle = opts.wanderAngle || rand(0, Math.PI * 2);
this.nextTurn = opts.nextTurn || rand(0.4, 1.6);
this.returnTimer = opts.returnTimer || 0;
this.foundingX = opts.foundingX;
this.foundingY = opts.foundingY;
this.foundingDelayUntil = Number.isFinite(opts.foundingDelayUntil) ? opts.foundingDelayUntil : -Infinity;
@ -292,7 +283,6 @@ class AntActor {
target.target = null;
if (targetWasSleeping) {
target.startSleeping?.(null, "\u7720\u3063\u305f\u307e\u307e\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b");
target.sleepTimer = Math.max(target.sleepTimer || 0, 0.8);
} else {
if (target.enterPanic) target.enterPanic({ threat: this, reason: "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b", fear: 0.36, wake: true, cause: "ant_drag" });
else { target.setActionState?.("panic", { target: target.panicDestination ? target.panicDestination(this) : null, reason: "\u30a2\u30ea\u306b\u5de3\u3078\u904b\u3070\u308c\u3066\u3044\u308b", wake: true }); target.fearTimer = Math.max(target.fearTimer || 0, 0.36); }
@ -428,7 +418,7 @@ class AntActor {
draw(ctx, time = 0, lighting = null) {
if (this.dead) return;
const id = this.spriteId();
const img = typeof getRenderableImage === "function" ? getRenderableImage(id, id) : images.get(id);
const img = getRenderableImage(id, id);
const lightState = lighting || getLightingState(this.world);
ctx.save();
const shadow = projectedShadowParams(lightState, this.kind === "queen" ? 0.78 : 0.26);
@ -479,13 +469,3 @@ class AntActor {
}
}
window.AntActor = AntActor;
window.ANT_NEST_START_COUNT = ANT_NEST_START_COUNT;
window.ANT_NEST_MAX_COUNT = ANT_NEST_MAX_COUNT;
window.ANT_NEST_MAX_OUTSIDE = ANT_NEST_MAX_OUTSIDE;
window.ANT_WORKER_HP = ANT_WORKER_HP;
window.ANT_QUEEN_HP = ANT_QUEEN_HP;
window.ANT_EXTENSION_HOOKS = ANT_EXTENSION_HOOKS;
window.buildAntWorkerPool = buildAntWorkerPool;
window.ensureAntNestWorkerPool = ensureAntNestWorkerPool;
window.syncAntNestCount = syncAntNestCount;

View file

@ -1,16 +1,70 @@
"use strict";
const images = new Map();
const imageBoundsCache = new WeakMap();
const imageMetrics = new Map();
const imagePromises = new Map();
const imageAssetIndex = new Map();
const DECOR_ASSETS = [
{ id: "zunchi", path: "assets/objects/zunchi.webp" },
{ id: "zunchi_02", path: "assets/objects/zunchi_02.webp" },
{ id: "ant_queen", path: "assets/objects/ant_queen.webp" },
{ id: "ant_worker", path: "assets/objects/ant_worker.webp" },
{ id: "genkotsu", path: "assets/objects/genkotsu.webp" },
{ id: "pushpin", path: "assets/objects/pushpin.webp" },
{ id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" },
{ id: "oshibyo", path: "assets/objects/oshibyo.webp" },
{ id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" },
{ id: "plushie_bear", path: "assets/objects/plushie_bear.png" },
];
const ALPHA_BOUNDS = {
smile: { x: 18, y: 18, w: 476, h: 432, imageW: 512, imageH: 468 },
angry: { x: 16, y: 16, w: 480, h: 432, imageW: 512, imageH: 464 },
teary: { x: 17, y: 17, w: 478, h: 438, imageW: 512, imageH: 472 },
jito: { x: 18, y: 18, w: 476, h: 451, imageW: 512, imageH: 487 },
drool: { x: 18, y: 18, w: 476, h: 425, imageW: 512, imageH: 461 },
cry: { x: 15, y: 15, w: 482, h: 448, imageW: 512, imageH: 478 },
sleep: { x: 18, y: 18, w: 475, h: 439, imageW: 511, imageH: 475 },
pokan: { x: 18, y: 18, w: 476, h: 433, imageW: 512, imageH: 469 },
stretch: { x: 15, y: 15, w: 482, h: 354, imageW: 512, imageH: 384 },
hurt: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
weak: { x: 53, y: 18, w: 405, h: 435, imageW: 512, imageH: 468 },
back: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
flee: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
zunda_eat: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
hurt2: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
normal_smirk: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
normal_tongue: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
fear: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
flee_fear2: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
sleep2: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
normal_happy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
fear_blue: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
fear_cry: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
stress_dizzy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 },
zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 },
hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 },
zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 },
zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 },
zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 },
ant_queen: { x: 0, y: 0, w: 300, h: 155, imageW: 300, imageH: 155 },
ant_worker: { x: 0, y: 0, w: 140, h: 72, imageW: 140, imageH: 72 },
genkotsu: { x: 8, y: 8, w: 220, h: 283, imageW: 236, imageH: 299 },
sunbath_1: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
sunbath_2: { x: 69, y: 18, w: 374, h: 432, imageW: 512, imageH: 468 },
sunbath_3: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
plushie_bear: { x: 0, y: 0, w: 150, h: 192, imageW: 150, imageH: 192 },
};
const scaledSpriteCache = new Map();
const stainOverlayCache = new Map();
const SPRITE_CACHE_LIMIT = 128;
const STAIN_CACHE_LIMIT = 64;
const SUNBATH_IMAGE_IDS = new Set((typeof SPRITES !== "undefined" ? SPRITES : [])
const SUNBATH_IMAGE_IDS = new Set(SPRITES
.map(asset => asset?.id || "")
.filter(id => String(id).startsWith("sunbath")));
const INITIAL_IMAGE_IDS = new Set(["smile", "hungry_70", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
@ -35,13 +89,6 @@ function getCachedSpriteCanvas(id, img, w, h) {
const qw = quantizedCanvasSize(w);
const qh = quantizedCanvasSize(h);
const key = `${id}:${qw}x${qh}`;
if (window.TarinaiPhysics?.createCachedCanvas) {
return window.TarinaiPhysics.createCachedCanvas(scaledSpriteCache, key, qw, qh, (c) => {
if ("imageSmoothingQuality" in c) c.imageSmoothingQuality = "high";
c.clearRect(0, 0, qw, qh);
c.drawImage(img, 0, 0, qw, qh);
}, { limit: SPRITE_CACHE_LIMIT });
}
let canvas = scaledSpriteCache.get(key);
if (canvas) return canvas;
canvas = document.createElement("canvas");
@ -86,7 +133,6 @@ function getCachedStainOverlayCanvas(id, img, w, h, stain = 0, seed = 0.5) {
c.globalAlpha = 1;
c.globalCompositeOperation = "source-over";
};
if (window.TarinaiPhysics?.createCachedCanvas) return window.TarinaiPhysics.createCachedCanvas(stainOverlayCache, key, qw, qh, buildOverlay, { limit: STAIN_CACHE_LIMIT });
let canvas = stainOverlayCache.get(key);
if (canvas) return canvas;
canvas = document.createElement("canvas");
@ -109,7 +155,6 @@ function primeImageMetrics(asset) {
w,
h,
ratio: h / Math.max(w, 1),
visibleBottomRatio: bounds ? (bounds.y + bounds.h) / Math.max(bounds.imageH, 1) : 1,
});
}
@ -129,7 +174,6 @@ function markImageLoaded(asset, img) {
w,
h,
ratio: h / Math.max(w, 1),
visibleBottomRatio: (bounds.y + bounds.h) / Math.max(bounds.imageH, 1),
});
}
@ -154,7 +198,7 @@ function loadImageAsset(asset, priority = "auto") {
img.onerror = () => { resolve(null); };
});
imagePromises.set(asset.id, promise);
if (!img.src) img.src = (window.TarinaiVersion?.withStaticVersion ? window.TarinaiVersion.withStaticVersion(asset.path) : asset.path);
if (!img.src) img.src = window.TARINAI_APP?.withVersion ? window.TARINAI_APP.withVersion(asset.path) : asset.path;
return promise;
}
@ -176,10 +220,9 @@ function getRenderableImage(id, fallbackId = "smile") {
return null;
}
function loadImages(ids = null, onProgress = null) {
function loadImages(onProgress = null) {
setupAssetIndex();
const requested = ids ? new Set(ids) : INITIAL_IMAGE_IDS;
const assets = [...requested].map(id => imageAssetIndex.get(id)).filter(Boolean);
const assets = [...INITIAL_IMAGE_IDS].map(id => imageAssetIndex.get(id)).filter(Boolean);
const total = Math.max(assets.length, 1);
let done = 0;
if (typeof onProgress === "function") onProgress(done, total, null);
@ -227,14 +270,9 @@ function startBackgroundImageLoading() {
function imageAlphaBounds(img) {
if (!img) return null;
if (img.alphaBounds) return img.alphaBounds;
if (imageBoundsCache.has(img)) return imageBoundsCache.get(img);
const w = img.naturalWidth || img.width || 0;
const h = img.naturalHeight || img.height || 0;
if (!w || !h) return null;
const bounds = { x: 0, y: 0, w, h, imageW: w, imageH: h };
imageBoundsCache.set(img, bounds);
return bounds;
return w && h ? { x: 0, y: 0, w, h, imageW: w, imageH: h } : null;
}
function getImageMetrics(idOrImg) {
@ -250,9 +288,8 @@ function getImageMetrics(idOrImg) {
}
const w = idOrImg.naturalWidth || idOrImg.width || 0;
const h = idOrImg.naturalHeight || idOrImg.height || 0;
return w && h ? { w, h, ratio: h / w, visibleBottomRatio: 1 } : null;
return w && h ? { w, h, ratio: h / w } : null;
}
setupAssetIndex();
window.TarinaiAssets = { images, imageBoundsCache, imageMetrics, imagePromises, scaledSpriteCache, stainOverlayCache, loadImages, startBackgroundImageLoading, trimImageMemory, ensureImage, isImageReady, getRenderableImage, imageAlphaBounds, getImageMetrics, getCachedSpriteCanvas, getCachedStainOverlayCanvas };

View file

@ -10,12 +10,8 @@ const audio = {
sampleLast: {},
activeVoices: 0,
maxConcurrent: 96,
failedSamples: {},
lastPlayedId: "",
lastPlayedAt: 0,
soundPackVersion: "inline",
masterGainNode: null,
compressorNode: null,
_synthIdGain: 1,
idGain: {},
idMinGap: {},
@ -26,7 +22,6 @@ const audio = {
soundMap: {},
applySoundPack(pack = window.TARINAI_SOUND_PACK) {
if (!pack || typeof pack !== "object") return false;
this.soundPackVersion = String(pack.version || "custom");
if (pack.categoryGain && typeof pack.categoryGain === "object") this.categoryGain = { ...this.categoryGain, ...pack.categoryGain };
if (pack.idGain && typeof pack.idGain === "object") this.idGain = { ...this.idGain, ...pack.idGain };
if (pack.idMinGap && typeof pack.idMinGap === "object") this.idMinGap = { ...this.idMinGap, ...pack.idMinGap };
@ -46,13 +41,13 @@ const audio = {
this.masterGainNode = ctx.createGain();
this.masterGainNode.gain.setValueAtTime(1.85, ctx.currentTime);
if (typeof ctx.createDynamicsCompressor === "function") {
this.compressorNode = ctx.createDynamicsCompressor();
this.compressorNode.threshold.value = -18;
this.compressorNode.knee.value = 24;
this.compressorNode.ratio.value = 8;
this.compressorNode.attack.value = 0.002;
this.compressorNode.release.value = 0.18;
this.masterGainNode.connect(this.compressorNode).connect(ctx.destination);
const compressor = ctx.createDynamicsCompressor();
compressor.threshold.value = -18;
compressor.knee.value = 24;
compressor.ratio.value = 8;
compressor.attack.value = 0.002;
compressor.release.value = 0.18;
this.masterGainNode.connect(compressor).connect(ctx.destination);
} else {
this.masterGainNode.connect(ctx.destination);
}
@ -95,8 +90,7 @@ const audio = {
a.preload = "auto";
a.volume = this.sampleVolumeFor(key);
a.addEventListener("error", () => {
this.failedSamples[key] = path;
window.TarinaiEvents?.emit("audio:sample-error", { id: key, path });
window.TarinaiEvents.emit("audio:sample-error", { id: key, path });
}, { once: true });
this.samples[key] = a;
}
@ -147,8 +141,7 @@ const audio = {
const cat = category || this.categoryFor(key, key.startsWith("voice") ? "voice" : "ops");
if (!this.canPlay(key, cat, minGap)) return false;
this.lastPlayedId = key;
this.lastPlayedAt = performance.now() / 1000;
window.TarinaiEvents?.emit("audio:play", { id: key, category: cat, synthetic: false });
window.TarinaiEvents.emit("audio:play", { id: key, category: cat, synthetic: false });
this.ensure();
const node = base.cloneNode(true);
node.volume = Math.min(1, base.volume * (this.categoryGain?.[cat] ?? 1) * (this.idGain?.[key] ?? 1));
@ -203,8 +196,7 @@ const audio = {
const minGap = opts.minGap ?? 0.10;
if (!this.canPlay(id, category, minGap)) return false;
this.lastPlayedId = id;
this.lastPlayedAt = performance.now() / 1000;
window.TarinaiEvents?.emit("audio:play", { id, category, synthetic: !this.samples[id] });
window.TarinaiEvents.emit("audio:play", { id, category, synthetic: !this.samples[id] });
if (this.samples[id]) {
const base = this.samples[id];
this.ensure();
@ -227,7 +219,7 @@ const audio = {
}, ms);
try {
switch (id) {
case "sfx_notify": case "sfx_log_push":
case "sfx_notify":
this.tone(880, 0.055, "sine", 0.022, 80, category); t(62, () => this.tone(1180, 0.075, "triangle", 0.020, -40, category)); break;
case "sfx_ui_click": this.tone(520, 0.035, "square", 0.010, 80, category); break;
case "sfx_ui_fold": this.tone(420, 0.045, "triangle", 0.012, -130, category); break;
@ -237,7 +229,6 @@ const audio = {
case "sfx_grab": this.tone(300, 0.050, "triangle", 0.018, 90, category); break;
case "sfx_drop": this.tone(220, 0.060, "triangle", 0.018, -80, category); break;
case "sfx_water_hose": this.noise(0.13, 0.018, category, 2400); break;
case "sfx_water_clean": this.noise(0.08, 0.014, category, 3200); this.tone(1120, 0.040, "sine", 0.009, 180, category); break;
case "sfx_drink": this.tone(720, 0.045, "sine", 0.012, -120, category); t(42, () => this.tone(620, 0.040, "sine", 0.010, -80, category)); break;
case "sfx_ball_hit": this.tone(260, 0.065, "triangle", 0.020, 120, category); break;
case "sfx_stone_impact": this.tone(110, 0.080, "sawtooth", 0.032, -50, category); this.noise(0.05, 0.018, category, 600); break;
@ -319,5 +310,4 @@ const audio = {
audio.applySoundPack?.(window.TARINAI_SOUND_PACK);
audio.loadSettings();
window.TarinaiEvents?.on?.("audio:test", ev => audio.play(ev.detail?.id || "sfx_notify", ev.detail || {}));
window.TarinaiAudio = audio;

View file

@ -169,7 +169,7 @@
function placementRectsFor(worldRef, item) {
const solid = worldRef?.solidObstacleRects?.(item) || [];
if (solid.length) return solid;
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(item?.type)) return global.TarinaiMechanicalSystem?.placementRects?.(item) || [];
if (global.TarinaiMechanicalSystem.isMechanicalType(item?.type)) return global.TarinaiMechanicalSystem.placementRects(item) || [];
return solid;
}
@ -177,7 +177,7 @@
if (!item || item.dead) return { hit: false, distance: Infinity };
const padding = Math.max(0, num(opts.padding, 0));
let rects = opts.rects || worldRef?.solidObstacleRects?.(item) || [];
if ((!rects || !rects.length) && item?.type === "poison_block") rects = global.TarinaiMechanicalSystem?.poisonHazardRects?.(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) {
@ -186,9 +186,9 @@
bestDistance = Math.min(bestDistance, Math.hypot(num(x) - num(rect.cx, item.x), num(y) - num(rect.cy, item.y)));
}
}
const mechHit = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type);
const mechHit = global.TarinaiMechanicalSystem.isMechanicalType(item.type);
if (mechHit) {
const hub = Math.max(18, Math.min(40, (global.TarinaiPhysicsBodySystem?.scalar?.(item, "thickness", 12) ?? 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);
@ -201,7 +201,7 @@
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
if (!worldRef || !item || item.dead) return true;
const important = opts.important || new Set(["zunchi", "grass", "grass_bed", "water", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box"]);
const isPhysicsItem = (global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || item.type === "rope" || item.type === "rod" || (typeof isFenceItemType === "function" && isFenceItemType(item.type)));
const isPhysicsItem = (global.TarinaiMechanicalSystem.isMechanicalType(item.type) || item.type === "rope" || item.type === "rod" || isFenceItemType(item.type));
if (!important.has(item.type) && !isPhysicsItem) return false;
const itemRects = placementRectsFor(worldRef, item);
const itemRadius = itemFootprintRadius(worldRef, item, itemRects);
@ -212,7 +212,7 @@
if (!important.has(other.type)) continue;
const otherRects = placementRectsFor(worldRef, other);
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
const hasMechanism = global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type) || global.TarinaiMechanicalSystem?.isMechanicalType?.(other.type);
const hasMechanism = global.TarinaiMechanicalSystem.isMechanicalType(item.type) || global.TarinaiMechanicalSystem.isMechanicalType(other.type);
const itemIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(item.type) : /fence/.test(String(item.type || ""));
const otherIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(other.type) : /fence/.test(String(other.type || ""));
const margin = (itemIsFence || otherIsFence) ? (item.type === "gate_fence" || other.type === "gate_fence" ? 1.5 : 2.5) : (hasMechanism ? 1.25 : Math.max(4, Math.min(itemRadius, otherRadius) * 0.18));
@ -237,15 +237,9 @@
rectLocalPoint,
rectWorldVector,
pointInRect,
rectCorners,
rectAxes,
projectPoints,
aabbOverlap,
rectOverlapInfo,
rectsOverlap,
rectCircleOverlap,
itemFootprintRadius,
isSoftPlacementIgnoredType,
placementRectsFor,
hitTestItem,
itemPlacementOverlapBlocked,

View file

@ -1,42 +1,18 @@
"use strict";
(function (global) {
const ImpactCore = global.TarinaiImpactCoreSystem || {};
const ImpactCore = global.TarinaiImpactCoreSystem;
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = ImpactCore.PHYSICAL_DAMAGE_SPEED_THRESHOLD || 165;
const physicalDamageFromImpactSpeed = ImpactCore.physicalDamageFromImpactSpeed;
const itemHasAttachedLink = ImpactCore.itemHasAttachedLink;
function pairKey(a, b) {
const aid = String(a?.id ?? "a");
const bid = String(b?.id ?? "b");
return aid < bid ? `${aid}:${bid}` : `${bid}:${aid}`;
}
function normalizeVector(x, y, fallback = null) {
const d = Math.hypot(Number(x) || 0, Number(y) || 0);
if (d > 0.0001) return { x: x / d, y: y / d, d };
if (fallback) return { x: fallback.x || 1, y: fallback.y || 0, d: 1 };
return { x: 1, y: 0, d: 1 };
}
function separatePair(worldRef, a, b, nx, ny, overlap, aWeight = 0.5, bWeight = 0.5) {
const amount = Math.max(0, Number(overlap || 0) || 0);
if (!worldRef || amount <= 0 || !a || !b) return false;
a.x = clamp((a.x || 0) - nx * amount * aWeight, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
a.y = clamp((a.y || 0) - ny * amount * aWeight, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
b.x = clamp((b.x || 0) + nx * amount * bWeight, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
b.y = clamp((b.y || 0) + ny * amount * bWeight, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
return true;
}
function resolveBallBallCollisions(worldRef, dt) {
if (!worldRef) return undefined;
return (function resolveBallBallCollisionsUnified(dt) {
if ((this.itemCounts?.ball || 0) < 2) return;
const now = this.time || 0;
if (!this.ballCollisionMemo) this.ballCollisionMemo = new Map();
const balls = this.itemsOfType?.("ball") || (this.items || []).filter(it => it && !it.dead && it.type === "ball");
if ((worldRef.itemCounts?.ball || 0) < 2) return;
const now = worldRef.time || 0;
if (!worldRef.ballCollisionMemo) worldRef.ballCollisionMemo = new Map();
const balls = worldRef.itemsOfType?.("ball") || (worldRef.items || []).filter(it => it && !it.dead && it.type === "ball");
for (const a of balls) {
if (!a || a.dead || a.type !== "ball") continue;
const ar = a.r || 18;
@ -44,10 +20,10 @@
const avy = a.vy || 0;
const aSpeed = Math.hypot(avx, avy);
const range = ar * 2 + aSpeed * Math.max(0.016, dt || 0.016) + 52;
for (const b of this.nearbyItems(a.x, a.y, range) || []) {
for (const b of worldRef.nearbyItems(a.x, a.y, range) || []) {
if (!b || b === a || b.dead || b.type !== "ball") continue;
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
if (now - (this.ballCollisionMemo.get(key) || -999) < 0.055) continue;
if (now - (worldRef.ballCollisionMemo.get(key) || -999) < 0.055) continue;
const br = b.r || 18;
const hitRadius = ar + br;
let dx = b.x - a.x;
@ -90,7 +66,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 = deterministicAngle(this, "ball-ball-overlap-normal", a, b); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; }
else { const ang = deterministicAngle(worldRef, "ball-ball-overlap-normal", a, b); dx = Math.cos(ang); dy = Math.sin(ang); d = 1; }
}
const nx = dx / d;
const ny = dy / d;
@ -98,7 +74,7 @@
const rvy = (b.vy || 0) - (a.vy || 0);
const relNormal = rvx * nx + rvy * ny;
if (relNormal > 0 && d >= hitRadius - 0.5) continue;
this.ballCollisionMemo.set(key, now);
worldRef.ballCollisionMemo.set(key, now);
const restitution = 0.86;
const impulse = -(1 + restitution) * relNormal / 2;
if (Number.isFinite(impulse)) {
@ -117,19 +93,17 @@
const tangent = nx * ((a.vy || 0) - (b.vy || 0)) - ny * ((a.vx || 0) - (b.vx || 0));
a.spinVelocity = clamp((a.spinVelocity || 0) - tangent / Math.max(14, ar) * 1.8, -38, 38);
b.spinVelocity = clamp((b.spinVelocity || 0) + tangent / Math.max(14, br) * 1.8, -38, 38);
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(13, hitRadius * 0.38), life: 0.16, color: "rgba(255,255,255,0.44)" }));
worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(13, hitRadius * 0.38), life: 0.16, color: "rgba(255,255,255,0.44)" }));
}
}
}).call(worldRef, dt);
}
function resolveBallInteractions(worldRef, dt) {
if (!worldRef) return undefined;
return (function resolveBallInteractionsUnified(dt) {
if (!this.itemCounts?.ball) return;
for (const ball of (this.itemsOfType ? this.itemsOfType("ball") : this.items)) {
if (!worldRef.itemCounts?.ball) return;
for (const ball of (worldRef.itemsOfType ? worldRef.itemsOfType("ball") : worldRef.items)) {
if (!ball || ball.dead || ball.type !== "ball") continue;
const ballSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
const sweep = ballSpeed * Math.max(0.016, dt || 0.016) + 84;
@ -139,8 +113,8 @@
const sx = ball.x - px;
const sy = ball.y - py;
const segLenSq = sx * sx + sy * sy;
for (const t of this.nearbyTarinai(ball.x, ball.y, range)) {
if (!t || t.dead || t.state === "sleep" || this.isTarinaiHiddenInNestBox(t)) continue;
for (const t of worldRef.nearbyTarinai(ball.x, ball.y, range)) {
if (!t || t.dead || t.state === "sleep" || worldRef.isTarinaiHiddenInNestBox(t)) continue;
let hitX = ball.x;
let hitY = ball.y;
if (segLenSq > 1) {
@ -151,7 +125,7 @@
let d = Math.max(0.001, distXY(hitX, hitY, t.x, t.y));
const hitDistance = (ball.r || 18) + t.radius * 0.74;
if (d > hitDistance) continue;
const now = this.time || 0;
const now = worldRef.time || 0;
const repeat = ball.lastKickerId === t.id && now - (ball.lastKickedAt || -999) < 0.18;
if (repeat && ballSpeed < 300) continue;
let nx = (hitX - t.x) / d;
@ -162,25 +136,25 @@
ny = (ball.vy || 0) / ballSpeed;
} else {
nx = t.facingDir ? t.facingDir() : 1;
ny = deterministicRange(this, "ball-interaction-fallback-ny", -0.22, 0.22, ball, t);
ny = deterministicRange(worldRef, "ball-interaction-fallback-ny", -0.22, 0.22, ball, t);
}
}
const transferredPins = globalThis.TarinaiItemDynamicPinSystem?.transferBallPinsToTarinai?.(ball, t, this) || 0;
const transferredPins = globalThis.TarinaiItemDynamicPinSystem.transferBallPinsToTarinai(ball, t, worldRef) || 0;
if (transferredPins > 0) {
ball.vx = (ball.vx || 0) * 0.72 - nx * Math.min(78, Math.max(18, ballSpeed * 0.06));
ball.vy = (ball.vy || 0) * 0.72 - ny * Math.min(78, Math.max(18, ballSpeed * 0.06));
ball.spinVelocity = clamp((ball.spinVelocity || 0) * 0.72 + (nx >= 0 ? -1 : 1) * 5.2, -38, 38);
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, this.w - CONFIG.worldPadding);
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, this.h - CONFIG.worldPadding);
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
ball.lastKickedAt = now;
ball.lastKickerId = t.id;
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(18, ball.r * 1.05), life: 0.20, color: "rgba(196,65,72,0.46)" }));
if (this.relationNotice(t.id, ball.id || "ball", "ball-pin-transfer", 1.2)) this.log(`${t.name}にボールから鋲が移った。`, "accident", { participants: [t] });
worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(18, ball.r * 1.05), life: 0.20, color: "rgba(196,65,72,0.46)" }));
if (worldRef.relationNotice(t.id, ball.id || "ball", "ball-pin-transfer", 1.2)) worldRef.log(`${t.name}にボールから鋲が移った。`, "accident", { participants: [t] });
continue;
}
const constrainedBall = itemHasAttachedLink(this, ball);
const constrainedBall = itemHasAttachedLink(worldRef, ball);
const frameDt = Math.max(0.016, Number(dt || 0.016) || 0.016);
const moveVx = sx / frameDt;
const moveVy = sy / frameDt;
@ -195,10 +169,10 @@
t.vy = clamp((t.vy || 0) - ny * pushImpulse + effectiveVy * 0.08, -360, 360);
if (overlapPush > 0.4) {
const sep = Math.min(14, overlapPush * 0.58 + 0.8);
t.x = clamp((t.x || 0) - nx * sep, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
t.y = clamp((t.y || 0) - ny * sep, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
ball.x = clamp((ball.x || 0) + nx * sep * 0.28, CONFIG.worldPadding, this.w - CONFIG.worldPadding);
ball.y = clamp((ball.y || 0) + ny * sep * 0.28, CONFIG.worldPadding, this.h - CONFIG.worldPadding);
t.x = clamp((t.x || 0) - nx * sep, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
t.y = clamp((t.y || 0) - ny * sep, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
ball.x = clamp((ball.x || 0) + nx * sep * 0.28, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
ball.y = clamp((ball.y || 0) + ny * sep * 0.28, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
}
ball.vx = clamp((ball.vx || effectiveVx) * 0.62 + nx * Math.min(42, pushImpulse * 0.22), -720, 720);
ball.vy = clamp((ball.vy || effectiveVy) * 0.62 + ny * Math.min(42, pushImpulse * 0.22), -720, 720);
@ -207,7 +181,7 @@
ball.lastKickerId = t.id;
t.target = null;
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.16);
this.markSpatialDirty?.("constrained-ball-push");
worldRef.markSpatialDirty?.("constrained-ball-push");
continue;
}
@ -217,13 +191,13 @@
const bvy = ballSpeed > 0.01 ? (ball.vy || 0) / ballSpeed : ny;
const damage = physicalDamageFromImpactSpeed(ballSpeed, { min: 3.5, max: 38, scale: 18 });
t.lastBallDamageAt = now;
this.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, {
worldRef.applyImpulse(t, (ball.vx || 0) * 0.20, (ball.vy || 0) * 0.20, {
panic: true,
target: { x: ball.x, y: ball.y },
fearTimer: 0.95,
thought: "\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u3076\u3064\u304b\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
});
this.applyImpactDamage(t, damage, "\u885d\u7a81", { source: ball });
worldRef.applyImpactDamage(t, damage, "\u885d\u7a81", { source: ball });
const sensitiveHit = t.shouldApplyPersonalityBehavior?.("neuroticism", 1) ? 1.35 : (t.shouldApplyPersonalityBehavior?.("neuroticism", -1) ? 0.82 : 1.0);
const stressGain = clamp(damage * 0.55 * sensitiveHit, 3, sensitiveHit > 1 ? 24 : 18);
if (t.addStress) t.addStress(stressGain, { threshold: 8 });
@ -236,9 +210,9 @@
t.fearTimer = Math.max(t.fearTimer, 0.95);
t.blastSpinTimer = Math.max(t.blastSpinTimer || 0, damage > 14 ? 1.0 : 0.44);
t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer);
this.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.8));
this.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" }));
if (this.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) this.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [t] });
worldRef.spawnFallEffect(t.x, t.y + t.radius * 0.55, clamp(damage / 18, 0.55, 1.8));
worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(20, ball.r * 1.25), life: 0.22, color: "rgba(210,75,65,0.50)" }));
if (worldRef.relationNotice(t.id, ball.id || "ball", "fast-ball-hit", 2.8)) worldRef.log(`${t.name}\u306f\u9ad8\u901f\u306e\u30dc\u30fc\u30eb\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [t] });
ball.vx = (ball.vx || 0) * 0.58 - nx * Math.min(110, ballSpeed * 0.10);
ball.vy = (ball.vy || 0) * 0.58 - ny * Math.min(110, ballSpeed * 0.10);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38);
@ -255,8 +229,8 @@
ball.vy = (ball.vy || 0) * (isDangerous ? 0.78 : 0.92) + ny * impulse + (t.vy || 0) * 0.52;
const kickedSpeed = Math.hypot(ball.vx || 0, ball.vy || 0);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx * (t.vy || 0) - ny * (t.vx || 0)) / 16 + impulse / Math.max(12, ball.r || 18) * (nx >= 0 ? 1 : -1), -38, 38);
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, this.w - CONFIG.worldPadding);
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, this.h - CONFIG.worldPadding);
ball.x = clamp(t.x + nx * (hitDistance + 2), CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
ball.y = clamp(t.y + ny * (hitDistance + 2), CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
ball.lastKickedAt = now;
ball.lastKickerId = t.id;
t.vx -= nx * Math.min(22, impulse * 0.10);
@ -271,26 +245,24 @@
}
t.goodMode = playIntent || playful ? "smile" : t.goodMode;
if (playIntent && now > (t.nextPlayBubbleAt || 0)) {
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)");
t.nextPlayBubbleAt = now + deterministicRange(worldRef, "ball-play-bubble-delay", 2.0, 3.8, ball, t);
worldRef.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)");
}
if (playful && 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] });
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: "rgba(170,210,95,0.50)" }));
if (playful && worldRef.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) worldRef.log(`${t.name}\u306f\u30dc\u30fc\u30eb\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] });
worldRef.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: "rgba(170,210,95,0.50)" }));
}
}
}).call(worldRef, dt);
}
function resolveTarinaiHighSpeedCollisions(worldRef, dt) {
if (!worldRef) return undefined;
return (function resolveTarinaiHighSpeedCollisionsUnified(dt) {
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
const now = this.time || 0;
if (!this.tarinaiCollisionMemo) this.tarinaiCollisionMemo = new Map();
const live = Array.from(this.tarinai || [])
.filter(t => t && !t.dead && !this.isTarinaiHiddenInNestBox(t))
const now = worldRef.time || 0;
if (!worldRef.tarinaiCollisionMemo) worldRef.tarinaiCollisionMemo = new Map();
const live = Array.from(worldRef.tarinai || [])
.filter(t => t && !t.dead && !worldRef.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));
@ -310,7 +282,7 @@
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;
if (now - (worldRef.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
const bvx = b.vx || 0;
const bvy = b.vy || 0;
const relVx = avx - bvx;
@ -341,14 +313,14 @@
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));
const maxImpacts = Math.max(8, Math.min(64, Number(worldRef.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);
if (now - (worldRef.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
worldRef.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);
@ -365,7 +337,7 @@
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;
const angle = (stableUnit(key, "tarinai-collision-normal")) * Math.PI * 2;
nx = Math.cos(angle);
ny = Math.sin(angle);
}
@ -373,14 +345,14 @@
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");
impactor.x = clamp((impactor.x || 0) - nx * sep * 0.45, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
impactor.y = clamp((impactor.y || 0) - ny * sep * 0.45, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
target.x = clamp((target.x || 0) + nx * sep * 0.55, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
target.y = clamp((target.y || 0) + ny * sep * 0.55, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
worldRef.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, {
worldRef.applyImpulse(target, ivx * 0.20 + nx * 28, ivy * 0.20 + ny * 28, {
panic: true,
target: { x: impactor.x, y: impactor.y },
fearTimer: 0.55,
@ -388,8 +360,8 @@
});
impactor.vx = ivx * 0.72 - nx * Math.min(56, relSpeed * 0.10);
impactor.vy = ivy * 0.72 - ny * Math.min(56, relSpeed * 0.10);
this.applyImpactDamage(target, damage, "\u885d\u7a81", { source: impactor });
if (!impactor.dead) this.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81", { source: target });
worldRef.applyImpactDamage(target, damage, "\u885d\u7a81", { source: impactor });
if (!impactor.dead) worldRef.applyImpactDamage(impactor, damage * 0.35, "\u885d\u7a81", { source: target });
for (const t of [target, impactor]) {
if (!t || t.dead) continue;
if (t.enterPanic) t.enterPanic({ target: { x: impactor.x, y: impactor.y }, reason: "\u9ad8\u901f\u3067\u3076\u3064\u304b\u3063\u3066\u6df7\u4e71\u3057\u3066\u3044\u308b", fear: 0.75, wake: true, cause: "tarinai_highspeed_collision" });
@ -400,22 +372,15 @@
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] });
worldRef.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 (worldRef.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) worldRef.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] });
}
if (solved && this.spatialDirty) this.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
if (solved && worldRef.spatialDirty) worldRef.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
}).call(worldRef, dt);
}
global.TarinaiCollisionResponseSystem = Object.freeze({
PHYSICAL_DAMAGE_SPEED_THRESHOLD,
physicalDamageFromImpactSpeed,
itemHasAttachedLink,
pairKey,
normalizeVector,
separatePair,
resolveBallBallCollisions,
resolveBallInteractions,
resolveTarinaiHighSpeedCollisions,

View file

@ -87,9 +87,8 @@
const id = choose(worldRef, metrics);
const next = worldRef.colonyMoodDefinition?.(id) || { id, label: id, effects: { personality: {} } };
worldRef.colonyMood = next;
worldRef.colonySituationMetrics = metrics;
worldRef.nextColonyMoodEvalAt = now + 1.25;
return next;
}
global.TarinaiColonySituationSystem = Object.freeze({ collect, choose, evaluate });
global.TarinaiColonySituationSystem = Object.freeze({ evaluate });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -26,7 +26,6 @@
if (worldRef.tool === id) return ok({ toolId: id, changed: false });
worldRef.tool = id;
if (id !== "shoot") {
worldRef.hoverShootTarget = null;
worldRef.shootCursorActive = false;
worldRef.shootCursorOffsetX = 0;
worldRef.shootCursorOffsetY = 0;
@ -131,7 +130,7 @@
const py = Number(y);
if (!Number.isFinite(px) || !Number.isFinite(py)) return fail("invalid_point", { x, y });
if (typeof worldRef.handleClick !== "function") return fail("missing_pointer_handler");
if (worldRef.tool && worldRef.tool !== "observe") global.TarinaiHistory?.capture?.(worldRef, `tool:${worldRef.tool}`);
if (worldRef.tool && worldRef.tool !== "observe") global.TarinaiHistory.capture(worldRef, `tool:${worldRef.tool}`);
worldRef.handleClick(px, py);
emit(worldRef, "pointer:primary", { x: px, y: py, toolId: worldRef.tool || "" });
return ok({ x: px, y: py, toolId: worldRef.tool || "" });
@ -162,11 +161,11 @@
case "pointer.primary":
return primaryPointer(worldRef, command.x, command.y);
case "history.undo": {
const result = global.TarinaiHistory?.undo?.(worldRef) || { ok: false, reason: "missing_history" };
const result = global.TarinaiHistory.undo(worldRef);
return result.ok ? ok({ action: "undo" }) : fail(result.reason || "undo_failed");
}
case "history.redo": {
const result = global.TarinaiHistory?.redo?.(worldRef) || { ok: false, reason: "missing_history" };
const result = global.TarinaiHistory.redo(worldRef);
return result.ok ? ok({ action: "redo" }) : fail(result.reason || "redo_failed");
}
default:
@ -175,35 +174,8 @@
}
function installWorldFacade() {
const World = global.World;
if (!World?.prototype || World.prototype.dispatchCommand) return;
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
dispatchCommand(command = {}) { return dispatch(this, command); },
selectToolCommand(toolId) { return selectTool(this, toolId); },
setSelectionCommand(target) { return setSelection(this, target); },
clearSelectionCommand() { return clearSelection(this); },
focusSelectionCommand(target, options = {}) { return focusSelection(this, target, options); },
primaryPointerCommand(x, y) { return primaryPointer(this, x, y); },
}));
}
installWorldFacade();
global.TarinaiCommands = Object.freeze({
dispatch,
selectTool,
setSelection,
clearSelection,
focusSelection,
toggleSelectedFavorite,
setToolSize,
cycleToolSize,
setPaused,
togglePaused,
cycleSpeed,
primaryPointer,
undo(worldRef = global.world) { return global.TarinaiHistory?.undo?.(worldRef) || fail("missing_history"); },
redo(worldRef = global.world) { return global.TarinaiHistory?.redo?.(worldRef) || fail("missing_history"); },
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -10,7 +10,6 @@
function ls(item, key, fallback = 0) { return bodyApi()?.linkScalar?.(item, key, fallback) ?? fallback; }
function lset(item, key, value) { return bodyApi()?.setLinkScalar?.(item, key, value) || false; }
function ep(item, index) { return bodyApi()?.endpoint?.(item, index) || null; }
function setEp(item, index, value) { return bodyApi()?.setEndpoint?.(item, index, value) || false; }
function parts(item) { return bodyApi()?.particles?.(item) || null; }
function setParts(item, list) { return bodyApi()?.setParticles?.(item, list) || false; }
function ensureConstraint(item, worldRef = null, opts = {}) {
@ -26,7 +25,7 @@
return c;
}
function commitMechanicalBody(item, reason = "constraint-moved-body") {
if (!item || !global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) return false;
if (!item || !global.TarinaiMechanicalSystem.isMechanicalType(item.type)) return false;
// Constraint pulls sometimes mutate item.x/y as a scratch pose. Preserve
// that pose in physicsBody before normalizing, otherwise normalizeBody()
// can restore the old body pose and silently cancel the correction.
@ -74,7 +73,7 @@
item._lastConstraintSpatialDirtyY = y;
item._lastConstraintSpatialDirtyR = r;
item._nextConstraintSpatialDirtyAt = now + Math.max(0.016, Number(minInterval || 0.065) || 0.065);
const marked = global.TarinaiPhysicsProjectionSystem?.markSpatialDirty?.(world, item, reason, threshold, minInterval) ?? false;
const marked = global.TarinaiPhysicsProjectionSystem.markSpatialDirty(world, item, reason, threshold, minInterval) ?? false;
if (!marked) world.markSpatialDirty?.(reason);
if (world.constraintDirtyStatsThisFrame) world.constraintDirtyStatsThisFrame.marked = (world.constraintDirtyStatsThisFrame.marked || 0) + 1;
return true;
@ -84,11 +83,11 @@
}
function mechanicalPowered(item) {
return Boolean(global.TarinaiMechanicalSystem?.isPowered?.(item));
return Boolean(global.TarinaiMechanicalSystem.isPowered(item));
}
function commitConstraintMovedMechanical(item, reason = "constraint-projection") {
if (!item || !global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) return false;
if (!item || !global.TarinaiMechanicalSystem.isMechanicalType(item.type)) return false;
bodyApi()?.syncPoseFromItem?.(item);
bodyApi()?.normalizeBodyState?.(item, item.physicsBody);
bodyApi()?.markBodyChanged?.(item, reason);
@ -97,9 +96,9 @@
return true;
}
function reciprocatorAxis(item) {
if (global.TarinaiMechanicalSystem?.axis) return global.TarinaiMechanicalSystem.axis(item);
const fallback = typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0);
const a = typeof normalizedItemAngle === "function" ? normalizedItemAngle(ps(item, "railAxis", fallback), fallback) : ps(item, "railAxis", fallback);
if (global.TarinaiMechanicalSystem.axis) return global.TarinaiMechanicalSystem.axis(item);
const fallback = itemAngleFor(item);
const a = normalizedItemAngle(ps(item, "railAxis", fallback), fallback);
return { x: Math.cos(a), y: Math.sin(a), angle: a };
}
@ -112,15 +111,6 @@
if (!Number.isFinite(Number(ps(item, "railAnchorY", NaN)))) pset(item, "railAnchorY", item.y || 0, "constraint-anchor-init");
}
function railPhaseFromPosition(item) {
resetReciprocatorAnchorIfMissing(item);
const axis = reciprocatorAxis(item);
const travel = reciprocatorHalfTravel(item);
const dx = (Number(item.x || 0) || 0) - ps(item, "railAnchorX", 0);
const dy = (Number(item.y || 0) || 0) - ps(item, "railAnchorY", 0);
return clamp((dx * axis.x + dy * axis.y) / Math.max(10, travel), -1, 1);
}
function resolveEndpointObject(endpoint, worldRef) {
if (!endpoint || !worldRef) return null;
const allowFuzzy = endpoint.fuzzyResolve === true || endpoint.fuzzyFallback === true;
@ -164,11 +154,11 @@
}
function itemAngle(item) {
return typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0);
return itemAngleFor(item);
}
function isMechanicalLinkTarget(item) {
return Boolean(item && !item.dead && global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type));
return Boolean(item && !item.dead && global.TarinaiMechanicalSystem.isMechanicalType(item.type));
}
function localToWorld(item, lx, ly) {
@ -190,7 +180,7 @@
function nearestMechanicalLocalPoint(item, lx, ly) {
if (!isMechanicalLinkTarget(item)) return null;
const segments = global.TarinaiMechanicalSystem?.sanitizeSegments?.(item) || [];
const segments = global.TarinaiMechanicalSystem.sanitizeSegments(item) || [];
let best = null;
for (const seg of segments) {
if (!Array.isArray(seg) || seg.length < 4) continue;
@ -215,7 +205,7 @@
function mechanicalLocalPointSupported(item, lx, ly, endpoint = null) {
if (!isMechanicalLinkTarget(item)) return true;
const version = Number(item?._mechanicalShapeVersion || item?._segmentsVersion || 0) || 0;
const version = Number(item?._mechanicalShapeVersion || 0) || 0;
if (endpoint && endpoint._supportItemId === item.id && endpoint._supportCheckedVersion === version) return endpoint._supportAlive !== false;
const nearest = nearestMechanicalLocalPoint(item, lx, ly);
const alive = Boolean(nearest && nearest.d <= mechanicalSupportTolerance(item));
@ -339,7 +329,7 @@
if (obj.type === "ball") return 0.7;
if (obj.type === "zunchi") return 0.55;
if (obj.type === "rope" || obj.type === "rod") return 1.6;
if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
if (global.TarinaiMechanicalSystem.isMechanicalType(obj.type)) {
if (obj.type === "rotator") return Infinity; // anchored pivot: links may torque it, not translate it
if (obj.type === "reciprocator") return mechanicalPowered(obj) ? 18.0 : 5.8;
if (obj.type === "poison_block") return Math.max(0.35, ps(obj, "mass", 0.45));
@ -371,7 +361,7 @@
obj.x += moveX;
obj.y += moveY;
if (obj.state === "sleep" || obj.sleeping) {
global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(obj, dt, `constraint-${mode}`);
global.TarinaiMovementUpdateStep.markSleepExternalMotion(obj, dt, `constraint-${mode}`);
obj._sleepExternalMotionUntil = Math.max(Number(obj._sleepExternalMotionUntil || 0) || 0, (Number(obj.world?.time || 0) || 0) + (mode === "rod" ? 1.1 : 0.6));
}
obj.target = null;
@ -417,7 +407,7 @@
obj.x = beforeX + px;
obj.y = beforeY + py;
commitConstraintMovedMechanical(obj, "constraint-poison-project");
global.TarinaiMechanicalSystem?.wakeItem?.(obj, "constraint-poison-project");
global.TarinaiMechanicalSystem.wakeItem(obj, "constraint-poison-project");
return true;
}
if (obj.type === "rotator") {
@ -441,51 +431,6 @@
return true;
}
function updateFlexibleMidParticle(item, a, b, dt, length) {
if (!a || !b || item.type !== "rope") return false;
const ax = a.x, ay = a.y, bx = b.x, by = b.y;
const dx = bx - ax, dy = by - ay;
const d = Math.max(0.001, Math.hypot(dx, dy));
if (!Number.isFinite(ls(item, "midX", NaN)) || !Number.isFinite(ls(item, "midY", NaN))) {
lset(item, "midX", (ax + bx) * 0.5);
lset(item, "midY", (ay + by) * 0.5 + Math.max(4, (length - d) * 0.25));
lset(item, "midVx", 0);
lset(item, "midVy", 0);
}
const prevX = ls(item, "midX", item.x || 0);
const prevY = ls(item, "midY", item.y || 0);
const step = Math.max(0.008, Math.min(0.05, Number(dt || 0.016) || 0.016));
lset(item, "midVx", ls(item, "midVx", 0) * Math.pow(0.62, step * 60));
lset(item, "midVy", ls(item, "midVy", 0) * Math.pow(0.62, step * 60) + 90 * step);
lset(item, "midX", ls(item, "midX", 0) + ls(item, "midVx", 0) * step);
lset(item, "midY", ls(item, "midY", 0) + ls(item, "midVy", 0) * step);
const half = Math.max(12, length * 0.5);
if (d >= length * 0.985) {
lset(item, "midX", (ax + bx) * 0.5);
lset(item, "midY", (ay + by) * 0.5);
lset(item, "midVx", ls(item, "midVx", 0) * 0.25);
lset(item, "midVy", ls(item, "midVy", 0) * 0.25);
} else {
for (let i = 0; i < 4; i += 1) {
for (const pnt of [[ax, ay], [bx, by]]) {
const vx = ls(item, "midX", 0) - pnt[0];
const vy = ls(item, "midY", 0) - pnt[1];
const dd = Math.max(0.001, Math.hypot(vx, vy));
if (dd > half) {
lset(item, "midX", pnt[0] + vx / dd * half);
lset(item, "midY", pnt[1] + vy / dd * half);
}
}
}
const minY = Math.min(ay, by) - length * 0.35;
const maxY = Math.max(ay, by) + length * 0.70;
lset(item, "midY", clamp(ls(item, "midY", 0), minY, maxY));
}
lset(item, "midVx", clamp((ls(item, "midX", 0) - prevX) / step, -220, 220));
lset(item, "midVy", clamp((ls(item, "midY", 0) - prevY) / step, -220, 220));
return Math.hypot(ls(item, "midX", 0) - prevX, ls(item, "midY", 0) - prevY) > 0.01;
}
function applyDistanceConstraint(item, a, b, dt, length, mode) {
const solveOnce = (pa, pb, strength, maxFraction) => {
if (!pa || !pb || !pa.obj || !pb.obj) return false;
@ -541,7 +486,7 @@
}
function ropeLodSettings(worldRef, length) {
const tier = global.TarinaiPerf?.renderQualityTier?.() || "high";
const tier = global.TarinaiPerf.renderQualityTier() || "high";
const ropeCount = Number(worldRef?.itemCounts?.rope || 0) || 0;
let spacing = 34;
let maxParticles = 24;
@ -594,13 +539,7 @@
}
function rectLocalPointForConstraint(r, x, y) {
const shared = global.TarinaiCollisionFootprints?.rectLocalPoint;
if (typeof shared === "function") return shared(r, x, y);
const dx = x - (r.cx ?? 0);
const dy = y - (r.cy ?? 0);
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
const s = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0);
return { x: dx * c + dy * s, y: -dx * s + dy * c };
return global.TarinaiCollisionFootprints.rectLocalPoint(r, x, y);
}
function rectWorldNormalForConstraint(r, nx, ny) {
@ -932,9 +871,9 @@
}
let stamp = `${endpoint?.kind || "item"}:${obj.id || "?"}:${obj.type || (obj.name ? "tarinai" : "entity")}:${q(obj.x)}:${q(obj.y)}`;
if (obj.radius && obj.name) stamp += `:${q(obj.vx, 4)}:${q(obj.vy, 4)}:${obj.state || ""}`;
else if (global.TarinaiMechanicalSystem?.isMechanicalType?.(obj.type)) {
const a = typeof itemAngleFor === "function" ? itemAngleFor(obj) : (Number(obj.angle) || 0);
const shape = Number(obj._mechanicalShapeVersion || obj._segmentsVersion || 0) || 0;
else if (global.TarinaiMechanicalSystem.isMechanicalType(obj.type)) {
const a = itemAngleFor(obj);
const shape = Number(obj._mechanicalShapeVersion || 0) || 0;
stamp += `:${q(a, 1000)}:${shape}:${ps(obj, "motorOn", true) !== false ? 1 : 0}:${ps(obj, "railOn", true) !== false ? 1 : 0}:${q(obj.vx, 4)}:${q(obj.vy, 4)}:${q(ps(obj, "spin", 0), 1000)}:${q(ps(obj, "slideSpeed", 0), 4)}`;
} else stamp += `:${q(obj.vx, 4)}:${q(obj.vy, 4)}:${Number(obj.amount || 0) > 0 ? 1 : 0}`;
if (Number.isFinite(Number(endpoint?.localX)) || Number.isFinite(Number(endpoint?.localY))) stamp += `:l${q(endpoint.localX)}:${q(endpoint.localY)}`;
@ -1004,7 +943,7 @@
function updateWorld(worldRef, dt = 0.016, opts = {}) {
if (!worldRef?.itemsOfType) return 0;
const profiler = global.TarinaiPerf;
const end = profiler?.begin?.("update.constraints") || null;
const end = profiler.begin("update.constraints") || null;
try {
worldRef.ensureItemBuckets?.("constraint-world");
const maxLinks = Math.max(8, Number(opts.maxLinks || 160) || 160);
@ -1069,17 +1008,9 @@
}
const api = Object.freeze({
endpointWorld,
resolveEndpointObject,
endpointMass,
moveEndpointObject,
snapEndpointToTarget,
remapLinksForEditedMechanicalItem,
updateFlexibleLink,
updateRigidLink,
updateLink,
updateWorld,
pointSegmentDistance,
});
global.TarinaiConstraintSystem = api;

View file

@ -34,65 +34,8 @@ const SPRITES = [
{ id: "sunbath_2", label: "\u65e5\u5149\u6d74\u305f\u308a\u306a\u3044 2", path: "assets/sprites/tarinai_32_sunbath_02.webp", actionOnly: true },
{ id: "sunbath_3", label: "\u65e5\u5149\u6d74\u305f\u308a\u306a\u3044 3", path: "assets/sprites/tarinai_33_sunbath_03.webp", actionOnly: true },
];
const DECOR_ASSETS = [
{ id: "zunchi", path: "assets/objects/zunchi.webp" },
{ id: "zunchi_02", path: "assets/objects/zunchi_02.webp" },
{ id: "ant_queen", path: "assets/objects/ant_queen.webp" },
{ id: "ant_worker", path: "assets/objects/ant_worker.webp" },
{ id: "genkotsu", path: "assets/objects/genkotsu.webp" },
{ id: "pushpin", path: "assets/objects/pushpin.webp" },
{ id: "pushpin_stuck", path: "assets/objects/pushpin_stuck.webp" },
{ id: "oshibyo", path: "assets/objects/oshibyo.webp" },
{ id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" },
{ id: "plushie_bear", path: "assets/objects/plushie_bear.png" },
];
const ALPHA_BOUNDS = {
smile: { x: 18, y: 18, w: 476, h: 432, imageW: 512, imageH: 468 },
angry: { x: 16, y: 16, w: 480, h: 432, imageW: 512, imageH: 464 },
teary: { x: 17, y: 17, w: 478, h: 438, imageW: 512, imageH: 472 },
jito: { x: 18, y: 18, w: 476, h: 451, imageW: 512, imageH: 487 },
drool: { x: 18, y: 18, w: 476, h: 425, imageW: 512, imageH: 461 },
cry: { x: 15, y: 15, w: 482, h: 448, imageW: 512, imageH: 478 },
sleep: { x: 18, y: 18, w: 475, h: 439, imageW: 511, imageH: 475 },
pokan: { x: 18, y: 18, w: 476, h: 433, imageW: 512, imageH: 469 },
stretch: { x: 15, y: 15, w: 482, h: 354, imageW: 512, imageH: 384 },
hurt: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
weak: { x: 53, y: 18, w: 405, h: 435, imageW: 512, imageH: 468 },
back: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
flee: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
zunda_eat: { x: 17, y: 21, w: 478, h: 432, imageW: 512, imageH: 468 },
hurt2: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
normal_smirk: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
normal_tongue: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
fear: { x: 16, y: 20, w: 480, h: 434, imageW: 512, imageH: 468 },
flee_fear2: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
sleep2: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
normal_happy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
fear_blue: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
fear_cry: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
stress_dizzy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 },
zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 },
hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 },
zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 },
zunchi: { x: 0, y: 0, w: 236, h: 178, imageW: 236, imageH: 178 },
zunchi_02: { x: 8, y: 63, w: 220, h: 105, imageW: 236, imageH: 178 },
ant_queen: { x: 0, y: 0, w: 300, h: 155, imageW: 300, imageH: 155 },
ant_worker: { x: 0, y: 0, w: 140, h: 72, imageW: 140, imageH: 72 },
genkotsu: { x: 8, y: 8, w: 220, h: 283, imageW: 236, imageH: 299 },
sunbath_1: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
sunbath_2: { x: 69, y: 18, w: 374, h: 432, imageW: 512, imageH: 468 },
sunbath_3: { x: 71, y: 18, w: 369, h: 432, imageW: 512, imageH: 468 },
plushie_bear: { x: 0, y: 0, w: 150, h: 192, imageW: 150, imageH: 192 },
};
const NEEDS = ["\u98df\u3079\u7269", "\u4ef2\u9593", "\u7720\u308a", "\u52c7\u6c17", "\u6c34", "\u9759\u3051\u3055", "\u3042\u305f\u305f\u304b\u3055", "\u610f\u5473", "\u7a7a\u9593", "\u81ea\u4fe1"];
const CONFIG = {
initialPopulation: 18,
maxPopulation: Number.MAX_SAFE_INTEGER,
maxFenceCollisionChecks: 18,
effectLimit: 78,
grassLimit: 99,
@ -101,7 +44,6 @@ const CONFIG = {
hungerPerMinute: 36.0,
lonelinessPerMinute: 7.6,
energyPerMinute: 8.2,
stressDecayPerMinute: 10.2,
reproductionCooldown: 18,
dayLength: 120,
childGrowTime: 58,
@ -110,13 +52,17 @@ const CONFIG = {
weatherChangeMin: 46,
weatherChangeMax: 96,
bubbleLimit: 32,
highPopulationMode: 70,
ultraPopulationMode: 120,
};
const FIELD_LAYOUT = Object.freeze({
height: "calc(100vh - 140px)",
minHeight: "620px",
mobileHeight: "66vh",
mobileMinHeight: "460px",
});
const FIELD_TYPES = {
cage: { id: "cage", label: "\u30b1\u30fc\u30b8", sizeLabel: "\u5c0f", height: "calc(100vh - 140px)", minHeight: "620px", mobileHeight: "66vh", mobileMinHeight: "460px", worldScale: 0.46, population: 12, grass: 16 },
garden: { id: "garden", label: "\u5ead", sizeLabel: "\u4e2d", height: "calc(100vh - 140px)", minHeight: "620px", mobileHeight: "66vh", mobileMinHeight: "460px", worldScale: 1.0, population: CONFIG.initialPopulation, grass: 34 },
park: { id: "park", label: "\u516c\u5712", sizeLabel: "\u5927", height: "calc(100vh - 140px)", minHeight: "620px", mobileHeight: "66vh", mobileMinHeight: "460px", worldScale: 2.35, population: 24, grass: 62 },
cage: { id: "cage", worldScale: 0.46, population: 12, grass: 16 },
garden: { id: "garden", worldScale: 1.0, population: CONFIG.initialPopulation, grass: 34 },
park: { id: "park", worldScale: 2.35, population: 24, grass: 62 },
};
const LIGHTING_TUNING = {
rayAlpha: 0.040,

View file

@ -82,8 +82,8 @@
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; });
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);
@ -134,11 +134,11 @@
const DEBUG_REFRESH_MS = 750;
window.setInterval(() => {
const w = window.world;
const input = window.TarinaiInputMode?.snapshot?.() || {};
const input = window.TarinaiInputMode.snapshot() || {};
const diag = w?.lastRuntimeDiagnostics || w?.runtimeDiagnostics?.() || {};
const behaviorCounts = diag.behavior?.counts || {};
const topBehaviors = topMap(behaviorCounts, 5);
const perf = diag.perf || window.TarinaiPerf?.snapshot?.() || {};
const perf = diag.perf || window.TarinaiPerf.snapshot() || {};
const metrics = perf.metrics || {};
const currentDpr = (typeof uiCache !== "undefined" && uiCache) ? uiCache.canvasDpr : "?";
const creature = diag.creatures || {};
@ -165,7 +165,7 @@
`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 || "-"}`,
`audio ${lastAudio.join(", ") || window.TarinaiAudio.lastPlayedId || "-"}`,
`events ${lastEvents.join(", ")}`,
];
lastText = lines.join("\n");

View file

@ -32,7 +32,6 @@
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));
global.TarinaiDeterministicHelpers = Object.freeze({ keyPart, hashUnit });
})(typeof window !== "undefined" ? window : globalThis);
// Keep the historical bare helper names available to legacy scripts while the

View file

@ -5,59 +5,34 @@
disease_zunchi: {
id: "disease_zunchi",
flag: "zunchiDisease",
label: "\u305a\u3093\u3061\u75c5",
symptoms: ["stress", "energy_loss", "infection"],
displayText: "\u305a\u3093\u3061\u75c5",
color: "rgba(113,93,59,0.74)",
mysteryEligible: true,
cureRules: ["water", "natural_recovery"]
},
disease_sleep: {
id: "disease_sleep",
flag: "sleepDisease",
label: "\u306d\u3080\u308a\u75c5",
symptoms: ["sleep_lock", "no_counterattack"],
displayText: "\u306d\u3080\u308a\u75c5",
color: "rgba(104,84,168,0.78)",
mysteryEligible: true,
cureRules: ["damage"]
},
disease_explosion: {
id: "disease_explosion",
flag: "explosionDisease",
label: "\u7206\u767a\u75c5",
symptoms: ["explodes_after_timer", "water_seeking"],
displayText: "\u7206\u767a\u75c5",
color: "rgba(210,90,44,0.78)",
baseInfectionChance: 0.15,
mysteryEligible: true,
cureRules: ["water", "zunda_mochi"]
},
disease_fight: {
id: "disease_fight",
flag: "fightDisease",
label: "\u304d\u305a\u3064\u304d\u75c5",
symptoms: ["fight_lash_out", "zunda_seeking"],
displayText: "\u304d\u305a\u3064\u304d\u75c5",
color: "rgba(180,86,52,0.80)",
mysteryEligible: true,
cureRules: ["zunda_mochi", "natural_recovery"]
}
};
class DiseaseRegistry extends global.TarinaiRegistryBase {
byFlag(flag = "") { return [...this.defs.values()].find(def => def.flag === flag) || null; }
label(id = "") { return super.label(id, id || ""); }
color(id = "") { return super.color(id, "rgba(255,242,160,0.85)"); }
infectionChance(id = "", fallback = 0) {
const value = this.get(id)?.baseInfectionChance;
return Number.isFinite(value) ? value : fallback;
}
mysteryPool() { return [...this.defs.values()].filter(def => def.mysteryEligible).map(def => def.id); }
debugSnapshot() { return super.debugSnapshot((id, def) => ({ id: def.id || id, label: def.label, flag: def.flag, cures: def.cureRules || [] })); }
}
const registry = new DiseaseRegistry(rawDefinitions);
global.TarinaiDiseaseRegistry = registry;
global.TARINAI_DISEASE_DEFINITIONS = Object.freeze(rawDefinitions);
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,10 +1,8 @@
"use strict";
class TarinaiEventBus {
constructor({ historyLimit = 180 } = {}) {
constructor() {
this.handlers = new Map();
this.history = [];
this.historyLimit = historyLimit;
}
on(type, handler) {
@ -28,8 +26,6 @@ class TarinaiEventBus {
emit(type, detail = {}) {
if (!type) return null;
const event = { type, detail: detail || {}, at: performance?.now?.() ?? Date.now() };
this.history.unshift(event);
if (this.history.length > this.historyLimit) this.history.length = this.historyLimit;
const direct = this.handlers.get(type);
if (direct) for (const handler of [...direct]) this.safeCall(handler, event);
const wildcard = this.handlers.get("*");
@ -41,12 +37,6 @@ class TarinaiEventBus {
try { handler(event); }
catch (err) { console.error("event handler failed", event.type, err); }
}
recent(filter = null, limit = 40) {
const list = filter ? this.history.filter(ev => ev.type === filter) : this.history;
return list.slice(0, limit);
}
}
window.TarinaiEventBus = TarinaiEventBus;
window.TarinaiEvents = window.TarinaiEvents || new TarinaiEventBus();

View file

@ -45,5 +45,5 @@
return comps;
}
global.TarinaiFamilyGraph = { buildGraph, connectedComponents };
global.TarinaiFamilyGraph = { connectedComponents };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -24,10 +24,6 @@ function groundLabel(id = "soil") {
return groundDefinition(id).label || "\u7802";
}
function groundDescription(id = "soil") {
return groundDefinition(id).description || "";
}
function normalizedGroundStressMultiplier(id = "soil") {
const mult = Number(groundDefinition(id).stressMultiplier);
return Number.isFinite(mult) && mult > 0 ? mult : 1;
@ -72,12 +68,9 @@ function groundTooltipText(id = "soil") {
}
window.TarinaiGround = Object.freeze({
definitions: GROUND_TYPES,
order: GROUND_TYPE_ORDER,
definition: groundDefinition,
exists: groundExists,
label: groundLabel,
description: groundDescription,
stressMultiplier: normalizedGroundStressMultiplier,
grassMultiplier: normalizedGroundGrassMultiplier,
grassGrowthMultiplier: normalizedGroundGrassGrowthMultiplier,

View file

@ -78,7 +78,6 @@ const HEALTH = (() => {
t.lastDamageAt = now;
if (t.lastDamageAmount >= 6 || t.lastDamageAmount >= Math.max(4, (t.energy || 0) * 0.22)) {
t.lastMajorDamageCause = cause;
t.lastMajorDamageAmount = t.lastDamageAmount;
t.lastMajorDamageAt = now;
}
return cause;
@ -207,7 +206,6 @@ const HEALTH = (() => {
if (total < maxEnergy * 0.30) return false;
const majorCause = causeLabel(dominant.cause || cause) || causeLabel(cause) || CAUSES.accident;
t.lastMajorDamageCause = majorCause;
t.lastMajorDamageAmount = total;
t.lastMajorDamageAt = now;
if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false;
t.nextMajorDamageVoiceAt = now + 1.0;
@ -298,10 +296,6 @@ const HEALTH = (() => {
}
return Object.freeze({
MAJOR_DAMAGE_WINDOW,
WEAKENED_TRACE_WINDOW,
CAUSES,
causeLabel,
recentDamageCause,
rememberDamage,
weakenedDeathReasonFor,
@ -309,8 +303,6 @@ const HEALTH = (() => {
normalizeDeathReason,
applyDamage,
briefDeathReason,
maybeReleaseZunchiFromDamage,
maybeTriggerMajorDamageReaction,
});
})();

View file

@ -30,7 +30,7 @@
function capture(worldRef = global.world, label = "action") {
const world = ensure(worldRef);
if (!world || world._historyRestoring) return false;
const snap = global.TarinaiSnapshot?.createSnapshot?.(world);
const snap = global.TarinaiSnapshot.createSnapshot(world);
if (!snap) return false;
const key = snapshotKey(snap);
if (key && key === world._lastUndoSnapshotKey) return false;
@ -42,11 +42,11 @@
}
function restore(worldRef, entry) {
if (!worldRef || !entry?.snapshot || !global.TarinaiRestoreCoordinator?.restoreSnapshot) return false;
if (!worldRef || !entry?.snapshot) return false;
worldRef._historyRestoring = true;
try {
global.TarinaiRestoreCoordinator.restoreSnapshot(cloneSnapshot(entry.snapshot), worldRef, { syncUi: true });
worldRef._lastUndoSnapshotKey = snapshotKey(global.TarinaiSnapshot?.createSnapshot?.(worldRef));
worldRef._lastUndoSnapshotKey = snapshotKey(global.TarinaiSnapshot.createSnapshot(worldRef));
return true;
} finally {
worldRef._historyRestoring = false;
@ -56,7 +56,7 @@
function undo(worldRef = global.world) {
const world = ensure(worldRef);
if (!world || !world._undoStack.length) return { ok: false, reason: "empty" };
const current = global.TarinaiSnapshot?.createSnapshot?.(world);
const current = global.TarinaiSnapshot.createSnapshot(world);
const prev = world._undoStack.pop();
if (current) world._redoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "redo", time: world.time || 0 });
if (world._redoStack.length > MAX_HISTORY) world._redoStack.splice(0, world._redoStack.length - MAX_HISTORY);
@ -67,7 +67,7 @@
function redo(worldRef = global.world) {
const world = ensure(worldRef);
if (!world || !world._redoStack.length) return { ok: false, reason: "empty" };
const current = global.TarinaiSnapshot?.createSnapshot?.(world);
const current = global.TarinaiSnapshot.createSnapshot(world);
const next = world._redoStack.pop();
if (current) world._undoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "undo", time: world.time || 0 });
if (world._undoStack.length > MAX_HISTORY) world._undoStack.splice(0, world._undoStack.length - MAX_HISTORY);
@ -75,17 +75,6 @@
return { ok, label: next.label || "" };
}
function clear(worldRef = global.world) {
const world = ensure(worldRef);
if (!world) return false;
world._undoStack.length = 0;
world._redoStack.length = 0;
world._lastUndoSnapshotKey = "";
return true;
}
function canUndo(worldRef = global.world) { return !!worldRef?._undoStack?.length; }
function canRedo(worldRef = global.world) { return !!worldRef?._redoStack?.length; }
global.TarinaiHistory = Object.freeze({ capture, undo, redo, clear, canUndo, canRedo });
global.TarinaiHistory = Object.freeze({ capture, undo, redo });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -46,7 +46,6 @@
global.TarinaiImpactCoreSystem = Object.freeze({
PHYSICAL_DAMAGE_SPEED_THRESHOLD,
physicalDamageFromImpactSpeed,
linkEndpointMatchesItem,
itemHasAttachedLink,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,14 +1,13 @@
"use strict";
(function (global) {
const ImpactCore = global.TarinaiImpactCoreSystem || {};
const ImpactCore = global.TarinaiImpactCoreSystem;
const PHYSICAL_DAMAGE_SPEED_THRESHOLD = ImpactCore.PHYSICAL_DAMAGE_SPEED_THRESHOLD || 165;
const physicalDamageFromImpactSpeed = ImpactCore.physicalDamageFromImpactSpeed;
const itemHasAttachedLink = ImpactCore.itemHasAttachedLink;
function itemDropImpact(worldRef, it) {
if (!worldRef) return undefined;
return (function itemDropImpactUnified(it) {
if (!it || it.dead || it.type === "water") return;
const isStone = it.type === "stone";
const isGenkotsu = it.type === "genkotsu";
@ -19,17 +18,17 @@
const damage = physicalDamageFromImpactSpeed(impactSpeed, { min: 1.2, max: isGenkotsu ? 58 : 34, scale: 8.6 });
const push = clamp((impactSpeed - 120) * 0.62, 42, isGenkotsu ? 1040 : 260);
const reason = isGenkotsu ? "\u3052\u3093\u3053\u3064" : (isStone ? "\u77f3\u306b\u6f70\u3055\u308c\u305f" : (impactSpeed >= PHYSICAL_DAMAGE_SPEED_THRESHOLD ? "\u9ad8\u901f\u3067\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u885d\u7a81" : "\u843d\u3061\u3066\u304d\u305f\u9053\u5177\u306b\u5f53\u305f\u3063\u305f"));
if (isGenkotsu) { this.lastGenkotsuAt = this.time || 0; it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); this.triggerGroundShake?.(2.0, 10); }
if (isGenkotsu) { worldRef.lastGenkotsuAt = worldRef.time || 0; it.impactFlash = 1; it.lingerTimer = Math.max(it.lingerDuration || 2, 2); worldRef.triggerGroundShake?.(2.0, 10); }
let hit = 0;
for (const t of this.nearbyTarinai(it.x, it.y, radius + 54)) {
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue;
for (const t of worldRef.nearbyTarinai(it.x, it.y, radius + 54)) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue;
const d = Math.max(1, distXY(t.x, t.y, it.x, it.y));
const p = clamp(1 - d / (radius + t.radius), 0, 1);
if (p <= 0) continue;
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 = deterministicAngle(this, "drop-impact-overlap-normal", it, t);
const a = deterministicAngle(worldRef, "drop-impact-overlap-normal", it, t);
nx = Math.cos(a);
ny = Math.sin(a);
}
@ -52,9 +51,9 @@
hit += 1;
}
let ballHit = 0;
if (isGenkotsu && this.itemCounts?.ball) {
if (isGenkotsu && worldRef.itemCounts?.ball) {
const ballRange = radius + Math.max(72, (it.r || 88) * 0.78);
const balls = this.nearbyItems?.(it.x, it.y, ballRange + 42, true) || this.items || [];
const balls = worldRef.nearbyItems?.(it.x, it.y, ballRange + 42, true) || worldRef.items || [];
for (const ball of balls) {
if (!ball || ball.dead || ball.type !== "ball") continue;
const d = Math.max(1, distXY(ball.x, ball.y, it.x, it.y));
@ -63,7 +62,7 @@
let nx = (ball.x - it.x) / d;
let ny = (ball.y - it.y) / d;
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
const a = deterministicAngle(this, "genkotsu-ball-impact-normal", it, ball);
const a = deterministicAngle(worldRef, "genkotsu-ball-impact-normal", it, ball);
nx = Math.cos(a);
ny = Math.sin(a);
}
@ -73,28 +72,27 @@
ball.vx = clamp((ball.vx || 0) + nx * ballPush, -1180, 1180);
ball.vy = clamp((ball.vy || 0) + ny * ballPush - Math.min(180, ballPush * 0.12) * p, -1180, 1180);
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * (10 + p * 12), -48, 48);
ball.lastPokedAt = this.time || 0;
ball.lastPokedAt = worldRef.time || 0;
ballHit += 1;
this.effects?.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, (ball.r || 18) * 1.1), life: 0.20, color: "rgba(255,255,255,0.52)" }));
worldRef.effects?.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, (ball.r || 18) * 1.1), life: 0.20, color: "rgba(255,255,255,0.52)" }));
}
if (ballHit > 0) this.markSpatialDirty?.("genkotsu-ball-impact");
if (ballHit > 0) worldRef.markSpatialDirty?.("genkotsu-ball-impact");
}
if (damage > 0 && typeof damageNearbyStructures === "function") damageNearbyStructures(this, it.x, it.y, radius, damage, it);
const antHit = damage > 0 ? this.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
const antHit = damage > 0 ? worldRef.damageAntsInRadius(it.x, it.y, radius + 54, p => damage * (0.35 + p * 0.65), reason, {
push: p => push * (0.62 + p * 1.25),
}) : 0;
if (isGenkotsu) {
this.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75, { limit: 96, effectLimit: 6 });
this.blastLoosePinsFrom?.(it.x, it.y, radius + 92, it, 2.75, "genkotsu-impact");
const existingEffects = (this.effects || []).length;
worldRef.blastZunchiFrom?.(it.x, it.y, radius + 92, 2.75, { limit: 96, effectLimit: 6 });
worldRef.blastLoosePinsFrom?.(it.x, it.y, radius + 92, it, 2.75, "genkotsu-impact");
const existingEffects = (worldRef.effects || []).length;
const ringCount = existingEffects > 180 ? 2 : 3;
const burstCount = existingEffects > 180 ? 4 : 7;
for (let r = 0; r < ringCount; r++) this.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 42, life: 0.32 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.68)" : "rgba(74,57,42,0.38)" }));
for (let i = 0; i < burstCount; 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.22, 0.42, it, i), color: "rgba(255,205,46,0.78)" }));
for (let r = 0; r < ringCount; r++) worldRef.effects.push(new Effect("ring", it.x, it.y, { size: 32 + r * 42, life: 0.32 + r * 0.07, color: r % 2 ? "rgba(255,203,54,0.68)" : "rgba(74,57,42,0.38)" }));
for (let i = 0; i < burstCount; i++) worldRef.effects.push(new Effect("fight", it.x + deterministicRange(worldRef, "genkotsu-impact-effect-x", -38, 38, it, i), it.y + deterministicRange(worldRef, "genkotsu-impact-effect-y", -22, 28, it, i), { size: deterministicRange(worldRef, "genkotsu-impact-effect-size", 12, 22, it, i), life: deterministicRange(worldRef, "genkotsu-impact-effect-life", 0.22, 0.42, it, i), color: "rgba(255,205,46,0.78)" }));
} 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)" }));
worldRef.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)" }));
}
if (hit > 0 || antHit > 0 || isGenkotsu) this.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
if (hit > 0 || antHit > 0 || isGenkotsu) worldRef.spawnFallEffect(it.x, it.y + (it.r || 12) * 0.6, isGenkotsu ? 1.32 : (isStone ? 0.9 : 0.45));
if (isGenkotsu) audio.genkotsuImpact?.();
else if (isStone) audio.stoneImpact?.();
else if (hit > 0 || antHit > 0) audio.poke?.();
@ -104,15 +102,12 @@
const victims = [];
if (hit > 0) victims.push(`${hit}\u5339\u306e\u305f\u308a\u306a\u3044`);
if (antHit > 0) victims.push(`${antHit}\u5339\u306e\u30a2\u30ea`);
this.log(`${label}\u304c\u843d\u3061\u3001${victims.join("\u3068")}\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident");
worldRef.log(`${label}\u304c\u843d\u3061\u3001${victims.join("\u3068")}\u304c\u5dfb\u304d\u8fbc\u307e\u308c\u305f\u3002`, "accident");
}
}).call(worldRef, it);
}
global.TarinaiImpactResponseSystem = Object.freeze({
PHYSICAL_DAMAGE_SPEED_THRESHOLD,
physicalDamageFromImpactSpeed,
itemDropImpact,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -6,7 +6,6 @@
class InputModeManager {
constructor() {
this.storageKey = STORAGE_KEY;
this.currentMode = this.loadMode();
this.classification = this.classify();
}
@ -71,6 +70,5 @@
}
}
global.InputModeManager = InputModeManager;
global.TarinaiInputMode = global.TarinaiInputMode || new InputModeManager();
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -18,14 +18,14 @@
if (item.y < p) { item.y = p; item.vy = Math.abs(item.vy || 0) * 0.72; item.spinVelocity *= -0.72; }
if (item.y > worldRef.h - p) { item.y = worldRef.h - p; item.vy = -Math.abs(item.vy || 0) * 0.72; item.spinVelocity *= -0.72; }
resolveObstacleCollisions(item, dt, worldRef);
global.TarinaiItemDynamicPinSystem?.tryBallPickup?.(item, worldRef);
global.TarinaiItemDynamicPinSystem.tryBallPickup(item, worldRef);
item.x = clamp(item.x, p, worldRef.w - p);
item.y = clamp(item.y, p, worldRef.h - p);
const groundFriction = Math.pow(0.72, dt);
item.vx *= groundFriction;
item.vy *= groundFriction;
}
global.TarinaiItemDynamicPinSystem?.tryBallPickup?.(item, worldRef);
global.TarinaiItemDynamicPinSystem.tryBallPickup(item, worldRef);
item.spin = (item.spin || 0) + (item.spinVelocity || 0) * dt + (item.vx || 0) * dt / Math.max(8, item.r || 18);
item.spinVelocity *= Math.pow(0.65, dt);
if (Math.hypot(item.vx || 0, item.vy || 0) < 0.18) { item.vx = 0; item.vy = 0; }
@ -42,8 +42,8 @@
// \u4e2d\u5fc3\u3067\u306f\u306a\u304f\u5916\u63a5\u534a\u5f84\u3067\u8ffd\u52a0\u8d70\u67fb\u3057\u3066\u3001\u9577\u3044\u68d2\u30fb\u5f80\u5fa9\u5e45\u306b\u3082\u885d\u7a81\u3055\u305b\u308b\u3002
if (Array.isArray(worldRef.items)) {
for (const it of worldRef.items) {
if (!it || it.dead || it === item || !global.TarinaiMechanicalSystem?.isMechanicalType?.(it.type) || seen.has(it)) continue;
const reach = global.TarinaiMechanicalSystem?.reach?.(it) || worldRef.rotatorExtent?.(it) || Math.max(64, it.r || 64);
if (!it || it.dead || it === item || !global.TarinaiMechanicalSystem.isMechanicalType(it.type) || seen.has(it)) continue;
const reach = global.TarinaiMechanicalSystem.reach(it) || worldRef.rotatorExtent?.(it) || Math.max(64, it.r || 64);
if (distXY(item.x, item.y, it.x, it.y) <= searchRadius + reach + 36) {
items.push(it);
seen.add(it);
@ -134,7 +134,7 @@
const restitution = Number.isFinite(rect.restitution) ? rect.restitution : 0.78;
const velocityDamp = rect.bounce ? 1.0 : 0.94;
const minBounceSpeed = Math.max(0, Number(rect.minBounceSpeed || 0) || 0);
if (rect.oriented && global.TarinaiPhysics?.bounceCircleOffRect) {
if (rect.oriented && global.TarinaiPhysics.bounceCircleOffRect) {
const bounced = global.TarinaiPhysics.bounceCircleOffRect(item, rect, hitRadius, restitution, worldRef, { minBounceSpeed });
if (!bounced) return;
item.vx *= velocityDamp;
@ -243,9 +243,5 @@
global.TarinaiItemDynamicBallSystem = Object.freeze({
update,
resolveObstacleCollisions,
applyHayDrag,
resolveCircleBounce,
resolveRectBounce,
emitBounce,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,9 +1,7 @@
"use strict";
// Layer: entity-runtime/items/dynamic-system
// Owns duplicator item loading/reloading behavior. Direct stored-type helpers
// remain exposed from TarinaiDuplicatorRuntime for UI/tool commands and
// save/restore stability.
// Owns duplicator item loading/reloading behavior.
(function (global) {
function support() {
return global.TarinaiItemLifecycleRuntimeSupport || {};
@ -12,7 +10,7 @@
function update(item, dt, worldRef) {
if (!item || item.dead || item.type !== "duplicator") return false;
const s = support();
const syncRoles = s.syncDuplicatorRoles || global.TarinaiDuplicatorRuntime?.syncRoles;
const syncRoles = s.syncDuplicatorRoles;
const applyStoredPin = s.duplicatorApplyStoredPin;
if (typeof syncRoles !== "function") return false;
@ -20,12 +18,6 @@
syncRoles(item);
if (typeof applyStoredPin === "function") applyStoredPin(item, worldRef);
item.duplicatorPinFlash = Math.max(0, (item.duplicatorPinFlash || 0) - dt);
// 39.15.34: auto-loading by contact is intentionally disabled.
// A duplicator accepts a stored item only through the explicit player
// click path (World.directSetDuplicatorAt / direct:true). This prevents
// dropped or pushed items from accidentally changing its setup.
item._duplicatorCandidateKey = "";
item._duplicatorCandidateSince = 0;
return true;
}

View file

@ -6,7 +6,7 @@
// behavior. Prototype methods delegate here.
(function (global) {
function isPin(item) {
return Boolean(global.TarinaiPinAttachmentSystem?.isPin?.(item) || (item && typeof global.isPinType === "function" && global.isPinType(item.type)));
return Boolean(global.TarinaiPinAttachmentSystem.isPin(item) || (item && typeof global.isPinType === "function" && global.isPinType(item.type)));
}
function ballById(worldRef, id) {
@ -28,7 +28,7 @@
detach(item, worldRef, "ball-lost");
return;
}
if (global.TarinaiPinAttachmentSystem?.applyBallPose?.(item, ball, worldRef)) return;
if (global.TarinaiPinAttachmentSystem.applyBallPose(item, ball, worldRef)) return;
const angle = (ball.spin || 0) + (item.pinBallOffsetAngle || 0);
const distance = Math.max(4, Number(item.pinBallOffsetDistance || (ball.r || 18) * 0.55) || (ball.r || 18) * 0.55);
item.prevX = item.x;
@ -134,9 +134,9 @@
updateBallLodged(item, dt, worldRef);
return true;
}
if (global.TarinaiPhysics?.applyKinematicItemMotion) {
if (global.TarinaiPhysics.applyKinematicItemMotion) {
global.TarinaiPhysics.applyKinematicItemMotion(item, worldRef, dt, { padding: 26, bounce: 0.44, spinBounce: -0.68, frictionBase: 0.18, frictionRate: 0.85, spinFrictionBase: 0.38, spinRestDamp: 0.08, stopSpeed: 0.05, zeroBelow: 7.5 });
global.TarinaiItemDynamicBallSystem?.resolveObstacleCollisions?.(item, dt, worldRef);
global.TarinaiItemDynamicBallSystem.resolveObstacleCollisions(item, dt, worldRef);
} else {
item.prevX = item.x;
item.prevY = item.y;
@ -168,7 +168,7 @@
}
function oshibyoAnchorFor(t, visualSide, item = { type: "oshibyo" }) {
return global.TarinaiPinAttachmentSystem?.tarinaiAnchor?.(item, t, { visualSide }) || {
return global.TarinaiPinAttachmentSystem.tarinaiAnchor(item, t, { visualSide }) || {
x: t.x + visualSide * (t?.radius || 16) * 0.96,
y: t.y + (t?.radius || 16) * 0.50,
angle: visualSide * 0.48,
@ -181,7 +181,7 @@
function attach(item, t, worldRef, d = null) {
if (!t || t.dead) return false;
if (t.stuckPushpinId && t.stuckPushpinId !== item.id) return false;
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(item.type) : null;
const behavior = pinBehaviorFor(item.type);
const isOshibyo = Boolean(behavior?.blocksZunchi);
const dx = item.x - t.x;
const dy = item.y - t.y;
@ -190,7 +190,7 @@
item.deletable = false;
item.pinTargetId = t.id;
removeFromBall(item, worldRef);
const sharedAnchor = global.TarinaiPinAttachmentSystem?.tarinaiAnchor?.(item, t, { dx, dy, distance: distToTarget });
const sharedAnchor = global.TarinaiPinAttachmentSystem.tarinaiAnchor(item, t, { dx, dy, distance: distToTarget });
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
const faceSide = faceDir >= 0 ? 1 : -1;
const visualSide = sharedAnchor?.visualSide ?? (isOshibyo ? -faceSide : faceSide);
@ -238,11 +238,11 @@
detach(item, worldRef, "owner-lost");
return;
}
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(item.type) : null;
const behavior = pinBehaviorFor(item.type);
const isOshibyo = Boolean(behavior?.blocksZunchi);
t.stuckPushpinId = item.id;
item.deletable = false;
const sharedPose = global.TarinaiPinAttachmentSystem?.applyTarinaiPose?.(item, t, worldRef);
const sharedPose = global.TarinaiPinAttachmentSystem.applyTarinaiPose(item, t, worldRef);
if (!sharedPose) {
const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1);
const faceSide = faceDir >= 0 ? 1 : -1;
@ -290,7 +290,7 @@
function detach(item, worldRef, reason = "released") {
const carriedBall = removeFromBall(item, worldRef);
const t = worldRef?.liveTarinaiById?.(item.pinTargetId) || null;
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(item.type) : null;
const behavior = pinBehaviorFor(item.type);
const wasOshibyo = Boolean(behavior?.blocksZunchi);
const storedZunchi = wasOshibyo && t ? Math.max(0, Math.floor(t.oshiriByoZunchiStock || 0)) : 0;
if (t && t.stuckPushpinId === item.id) t.stuckPushpinId = null;
@ -352,14 +352,9 @@
global.TarinaiItemDynamicPinSystem = Object.freeze({
update,
isPin,
tryStick,
attach,
updateLodged,
detach,
attachToBall,
transferFromBallToTarinai,
transferBallPinsToTarinai,
tryBallPickup,
updateBallLodged,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -16,15 +16,15 @@
case "sprinkler":
case "rope":
case "rod":
return global.TarinaiItemDynamicToolSystem?.update(item, dt, worldRef) !== false;
return global.TarinaiItemDynamicToolSystem.update(item, dt, worldRef) !== false;
case "ball":
return global.TarinaiItemDynamicBallSystem?.update(item, dt, worldRef) !== false;
return global.TarinaiItemDynamicBallSystem.update(item, dt, worldRef) !== false;
case "duplicator":
return global.TarinaiItemDynamicDuplicatorSystem?.update(item, dt, worldRef) !== false;
return global.TarinaiItemDynamicDuplicatorSystem.update(item, dt, worldRef) !== false;
case "zunchi":
return global.TarinaiItemDynamicZunchiSystem?.updateMotion(item, dt, worldRef) !== false;
return global.TarinaiItemDynamicZunchiSystem.updateMotion(item, dt, worldRef) !== false;
default:
if (global.TarinaiItemDynamicPinSystem?.isPin(item)) {
if (global.TarinaiItemDynamicPinSystem.isPin(item)) {
return global.TarinaiItemDynamicPinSystem.update(item, dt, worldRef) !== false;
}
return false;

View file

@ -5,9 +5,6 @@
// links, and static gate-fence ticking. Item.prototype methods delegate here for
// direct Item.prototype calls; lifecycle dispatch also calls this boundary directly.
(function (global) {
function support() {
return global.TarinaiItemLifecycleRuntimeSupport || {};
}
function updateFirecracker(item, dt, worldRef) {
item.fuseTimer = Math.max(0, (item.fuseTimer ?? 5) - dt);
@ -28,29 +25,25 @@
}
function updateRotator(item, dt, worldRef) {
return Boolean(global.TarinaiMechanicalSystem?.updateRotator?.(item, dt, worldRef));
return Boolean(global.TarinaiMechanicalSystem.updateRotator(item, dt, worldRef));
}
function updateReciprocator(item, dt, worldRef) {
return Boolean(global.TarinaiMechanicalSystem?.updateReciprocator?.(item, dt, worldRef));
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));
return Boolean(global.TarinaiMechanicalSystem.updatePoisonBlock(item, dt, worldRef));
}
function updateFlexibleLink(item, dt, worldRef) {
return Boolean(global.TarinaiConstraintSystem?.updateFlexibleLink?.(item, dt, worldRef));
return Boolean(global.TarinaiConstraintSystem.updateFlexibleLink(item, dt, worldRef));
}
function updateRigidLink(item, dt, worldRef) {
return Boolean(global.TarinaiConstraintSystem?.updateRigidLink?.(item, dt, worldRef));
return Boolean(global.TarinaiConstraintSystem.updateRigidLink(item, dt, worldRef));
}
@ -140,16 +133,6 @@
global.TarinaiItemDynamicToolSystem = Object.freeze({
update,
updateFirecracker,
updateGenkotsu,
updateRotator,
updateReciprocator,
updatePoisonBlock,
updateSprinkler,
sprinklerRange,
updateFlexibleLink,
updateRigidLink,
resolveMechanicalInteractions,
updateGateFence,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -9,10 +9,10 @@
const speed = Math.hypot(item.vx || 0, item.vy || 0);
if (speed < 0.08) {
item.vx = 0; item.vy = 0;
global.TarinaiItemDynamicBallSystem?.resolveObstacleCollisions?.(item, dt, worldRef);
global.TarinaiItemDynamicBallSystem.resolveObstacleCollisions(item, dt, worldRef);
return true;
}
if (global.TarinaiPhysics?.applyKinematicItemMotion) {
if (global.TarinaiPhysics.applyKinematicItemMotion) {
global.TarinaiPhysics.applyKinematicItemMotion(item, worldRef, dt, { bounce: 0.34, frictionBase: 0.68, frictionRate: 2.4, spin: false, stopSpeed: 0.08 });
} else {
item.prevX = item.x;
@ -29,7 +29,7 @@
item.vy *= slow;
worldRef.drawListDirty = true;
}
global.TarinaiItemDynamicBallSystem?.resolveObstacleCollisions?.(item, dt, worldRef);
global.TarinaiItemDynamicBallSystem.resolveObstacleCollisions(item, dt, worldRef);
resolveHighSpeedTarinaiCollision(item, dt, worldRef, speed);
item.spin = (item.spin || 0) + speed * dt / Math.max(8, item.r || 14);
return true;
@ -78,8 +78,8 @@
const rainy = worldRef.weather === "light_rain";
const rainBoost = rainy ? 1.28 : 1;
const groundId = worldRef?.groundType || "soil";
const stageMul = Math.max(0.1, Number(global.TarinaiGround?.zunchiStageMultiplier?.(groundId) || 1) || 1);
const groundDecayMul = Math.max(0.1, Number(global.TarinaiGround?.zunchiDecayMultiplier?.(groundId) || 1) || 1);
const stageMul = Math.max(0.1, Number(global.TarinaiGround.zunchiStageMultiplier(groundId) || 1) || 1);
const groundDecayMul = Math.max(0.1, Number(global.TarinaiGround.zunchiDecayMultiplier(groundId) || 1) || 1);
const decayBoost = (rainy ? 1.35 : 1) * groundDecayMul;
let waterBoost = 0;
const nearby = worldRef.nearbyItems ? worldRef.nearbyItems(item.x, item.y, 84) : (worldRef.items || []);
@ -108,5 +108,5 @@
return true;
}
global.TarinaiItemDynamicZunchiSystem = Object.freeze({ updateMotion, resolveHighSpeedTarinaiCollision, updateLifecycle });
global.TarinaiItemDynamicZunchiSystem = Object.freeze({ updateMotion, updateLifecycle });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -20,7 +20,7 @@
tags: ["item", "behavior", "power"], display: () => "\u6c38\u7d9a / \u4e0e\u30c0\u30e1\u30fc\u30b80.5\u500d / \u75c5\u6c17\u78ba\u73872\u500d",
},
mercury: {
label: "\u6c34\u9280", color: "rgba(96, 146, 166, 0.82)", kind: "permanent", stack: "multiply",
label: "\u6c34\u9280", color: "rgba(96, 146, 166, 0.82)", kind: "permanent",
tags: ["item", "life"], display: (_timer, t) => `\u6c38\u7d9a / \u5bff\u547dx${((t && t.mercuryLifeMultiplier) || 1.3).toFixed(2)}\uff08\u98df\u3079\u308b\u305f\u3073\u4e57\u7b97\uff09`,
},
giant_drug: {
@ -258,25 +258,17 @@
detail: this.display(id, this.timerValue(tarinai, id), tarinai),
}));
}
labels() { return super.labels(); }
colors() { return super.colors(); }
randomPool() {
const itemEffects = [...this.defs.entries()]
.filter(([id, def]) => Boolean(def.mysteryEligible && id !== "mystery_drug" && id !== "zunda_juice" && !(def.tags || []).includes("disease")))
.map(([id]) => id)
.filter(id => id !== "mystery_drug" && id !== "zunda_juice");
const diseases = global.TarinaiDiseaseRegistry?.mysteryPool?.() || [];
const diseases = global.TarinaiDiseaseRegistry.mysteryPool() || [];
return [...new Set([...itemEffects, ...diseases])];
}
permanentIds() { return [...this.defs.entries()].filter(([, def]) => def.kind === "permanent").map(([id]) => id); }
timedIds() { return [...this.defs.entries()].filter(([, def]) => def.kind === "timed" || def.kind === "timer").map(([id]) => id); }
debugSnapshot() {
return [...this.defs.entries()].map(([id, def]) => ({ id, label: def.label, kind: def.kind, group: def.exclusiveGroup || "" }));
}
}
const registry = new EffectRegistry(rawDefinitions);
global.TarinaiEffectRegistry = registry;
global.TarinaiEffectDefinitions = Object.freeze(rawDefinitions);
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -4,23 +4,23 @@
(function (global) {
const rawDefinitions = {
food: { label: "\u98df\u3079\u7269", nutrition: 18.0, hungerRelief: 1.0, interest: "normal", decayRateMultiplier: 1.0, hygienePenalty: 0.055 },
sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 16.5, hungerRelief: 0.95, interest: "zunda_high", decayRateMultiplier: 0.82, hygienePenalty: 0.055, cures: ["disease_explosion", "disease_fight"] },
love_mochi: { label: "\u3078\u3053\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "love_mochi" },
fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "fight_mochi" },
sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "sleep_drug" },
laxative: { label: "\u4e0b\u5264", nutrition: 6.8, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "laxative", passThrough: true },
protein: { label: "\u30d7\u30ed\u30c6\u30a4\u30f3", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "protein" },
niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "niteropu" },
ammo: { label: "\u5f3e\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "ammo" },
mystery_drug: { label: "\u602a\u3057\u3044\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mystery_drug" },
mercury: { label: "\u6c34\u9280", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "mercury" },
giant_drug: { label: "\u5de8\u5927\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "giant_drug" },
dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, hygienePenalty: 0.055, effectId: "dwarf_drug" },
zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.12, hygienePenalty: 0.055, effectId: "zunda_juice", removesItemEffects: true },
grass: { label: "\u8349", nutrition: 2.6, hungerRelief: 0.55, interest: "grass_like", decayRateMultiplier: 1.0, hygienePenalty: 0 },
water: { label: "\u6c34\u6ef4", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0, hygienePenalty: 0 },
ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 4.2, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0, hygienePenalty: 0.065 }
food: { label: "\u98df\u3079\u7269", nutrition: 18.0, hungerRelief: 1.0, interest: "normal", decayRateMultiplier: 1.0 },
sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 16.5, hungerRelief: 0.95, interest: "zunda_high", decayRateMultiplier: 0.82 },
love_mochi: { label: "\u3078\u3053\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "love_mochi" },
fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "fight_mochi" },
sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "sleep_drug" },
laxative: { label: "\u4e0b\u5264", nutrition: 6.8, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "laxative" },
protein: { label: "\u30d7\u30ed\u30c6\u30a4\u30f3", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "protein" },
niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "niteropu" },
ammo: { label: "\u5f3e\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "ammo" },
mystery_drug: { label: "\u602a\u3057\u3044\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "mystery_drug" },
mercury: { label: "\u6c34\u9280", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "mercury" },
giant_drug: { label: "\u5de8\u5927\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "giant_drug" },
dwarf_drug: { label: "\u77ee\u5c0f\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "dwarf_drug" },
zunda_juice: { label: "\u305a\u3093\u3060\u6c41", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.12, effectId: "zunda_juice", removesItemEffects: true },
grass: { label: "\u8349", nutrition: 2.6, hungerRelief: 0.55, interest: "grass_like", decayRateMultiplier: 1.0 },
water: { label: "\u6c34\u6ef4", nutrition: 0, hungerRelief: 0, interest: "water", decayRateMultiplier: 1.0 },
ant_corpse: { label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", nutrition: 4.2, hungerRelief: 0.62, interest: "corpse", decayRateMultiplier: 1.0 }
};
class FoodRegistry extends global.TarinaiRegistryBase {
@ -29,12 +29,6 @@
.filter(([, def]) => def.interest && !["grass_like", "water", "corpse"].includes(def.interest))
.map(([id]) => id);
}
typesByInterest(...interests) {
const wanted = new Set(interests.flat().filter(Boolean));
return [...this.defs.entries()]
.filter(([, def]) => wanted.has(def.interest))
.map(([id]) => id);
}
paramEffectTypes() {
return [...this.defs.entries()].filter(([, def]) => def.effectId && !def.effectId.endsWith("_mochi") && def.effectId !== "sleep_drug").map(([id]) => id);
}
@ -46,14 +40,10 @@
const value = this.get(type)?.hungerRelief;
return Number.isFinite(value) ? value : fallback;
}
effectId(type = "") { return this.get(type)?.effectId || ""; }
passiveDecayMultiplier(type = "") { return this.get(type)?.decayRateMultiplier ?? 1; }
hygienePenalty(type = "") { return this.get(type)?.hygienePenalty ?? 0.055; }
debugSnapshot() { return super.debugSnapshot((id, def) => ({ id, label: def.label, interest: def.interest, effectId: def.effectId || "" })); }
}
const registry = new FoodRegistry(rawDefinitions);
global.TarinaiFoodRegistry = registry;
global.TarinaiFoodDefinitions = Object.freeze(rawDefinitions);
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,8 +1,8 @@
"use strict";
// Layer: entity-runtime/items/decay-system
// Owns passive amount decay and spoilage side effects for water, food,
// transient traces, splats, and ant corpses. The lifecycle step is now only a
// Owns passive amount decay for water, food, transient traces, splats,
// and ant corpses. The lifecycle step is now only a
// thin pipeline adapter.
(function (global) {
const MEDICINE_LIKE_FOOD_TYPES = Object.freeze([
@ -11,7 +11,7 @@
]);
function decayServingFood(item, dt, worldRef) {
if (typeof isServingFoodType !== "function" || !isServingFoodType(item.type)) return false;
if (!isServingFoodType(item.type)) return false;
const interval = passiveFoodDecayInterval(worldRef);
item.passiveFoodDecayTimer = (item.passiveFoodDecayTimer || 0) + dt;
let changed = false;
@ -26,8 +26,6 @@
changed = true;
item.foodServingsRemaining = Math.max(0, before - lost);
item.amount = item.foodServingsRemaining;
const penalty = global.TarinaiItemRegistry?.food?.hygienePenalty?.(item.type) ?? PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING;
worldRef?.registerFoodSpoilage?.(lost * penalty, item);
worldRef?.markTerrainDirty?.("food-passive-decay");
worldRef?.emit?.("item:decayed", { item, type: item.type, amount: lost, passive: true });
if (item.foodServingsRemaining <= 0.015) item.amount = 0;
@ -38,8 +36,6 @@
item.amount -= lost;
if (lost > 0) {
changed = true;
const penalty = global.TarinaiItemRegistry?.food?.hygienePenalty?.(item.type) ?? 0.002;
worldRef?.registerFoodSpoilage?.(lost * penalty, item);
worldRef?.markTerrainDirty?.("food-passive-decay");
worldRef?.emit?.("item:decayed", { item, type: item.type, amount: lost, passive: true });
}
@ -58,9 +54,5 @@
return { done: false };
}
global.TarinaiItemDecaySystem = Object.freeze({
MEDICINE_LIKE_FOOD_TYPES,
decayServingFood,
update,
});
global.TarinaiItemDecaySystem = Object.freeze({ update });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,17 +1,9 @@
"use strict";
// Layer: entity-runtime/items/growth-system
// Owns passive grass normalization and zunchi lifecycle stage ticking. The
// lifecycle step is kept as a pipeline adapter only.
// Owns zunchi lifecycle stage ticking. Grass growth is handled by
// simulation_item_ant_system.js via sparse random stage advances.
(function (global) {
function updateGrass(item) {
if (!item || item.dead || item.type !== "grass") return false;
// Grass is intentionally not simulated per item. World.update randomly
// advances one grass by one discrete stage every few seconds.
normalizeGrassStage(item);
return true;
}
function updateZunchiLifecycle(item, dt, worldRef) {
if (!item || item.dead || item.type !== "zunchi") return false;
item.lifecycleTimer += dt;
@ -19,20 +11,15 @@
if (item.lifecycleTimer < interval) return false;
const lifecycleDt = Math.min(item.lifecycleTimer, 2.4);
item.lifecycleTimer = 0;
global.TarinaiItemDynamicZunchiSystem?.updateLifecycle(item, lifecycleDt, worldRef);
global.TarinaiItemDynamicZunchiSystem.updateLifecycle(item, lifecycleDt, worldRef);
return true;
}
function update(item, dt, worldRef) {
if (!item || item.dead) return { done: true };
if (item.type === "grass") updateGrass(item);
else if (item.type === "zunchi") updateZunchiLifecycle(item, dt, worldRef);
if (item.type === "zunchi") updateZunchiLifecycle(item, dt, worldRef);
return { done: false };
}
global.TarinaiItemGrowthSystem = Object.freeze({
updateGrass,
updateZunchiLifecycle,
update,
});
global.TarinaiItemGrowthSystem = Object.freeze({ update });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,13 +0,0 @@
"use strict";
// Layer: entity-runtime/items/legacy-lifecycle
// Compatibility shim. The former monolithic fallback was removed so item
// behavior has a single source of truth: TarinaiItemLifecyclePipeline.
(function (global) {
function updateOne(item, dt, worldRef = global.world) {
if (!item || item.dead || !global.TarinaiItemLifecyclePipeline?.updateOne) return false;
return global.TarinaiItemLifecyclePipeline.updateOne(item, dt, worldRef) !== false;
}
global.TarinaiItemLifecycleLegacySystem = Object.freeze({ updateOne });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -11,27 +11,24 @@
"TarinaiItemGrowthLifecycleStep",
]);
let cachedSteps = null;
function concreteSteps() {
return CONCRETE_STEP_NAMES.map(name => global[name]).filter(step => step && typeof step.update === "function");
if (!cachedSteps) cachedSteps = CONCRETE_STEP_NAMES.map(name => global[name]);
return cachedSteps;
}
function canRunConcretePipeline() {
return concreteSteps().length === CONCRETE_STEP_NAMES.length;
}
function updateOne(item, dt, worldRef, options = {}) {
if (!item || item.dead || !canRunConcretePipeline()) return false;
if (!item || item.dead) return false;
const steps = concreteSteps();
if (steps.length !== CONCRETE_STEP_NAMES.length) return false;
const context = options.context || {};
for (const step of concreteSteps()) {
for (const step of steps) {
const result = step.update(item, dt, worldRef, context) || {};
if (result.done) return result.ok !== false;
}
return true;
}
global.TarinaiItemLifecyclePipeline = Object.freeze({
stepNames: CONCRETE_STEP_NAMES,
canRunConcretePipeline,
updateOne,
});
global.TarinaiItemLifecyclePipeline = Object.freeze({ updateOne });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,42 +0,0 @@
"use strict";
// Layer: entity-runtime/items/prototype-boundary
// Keeps Item.prototype methods available while implementation ownership
// lives in lifecycle/dynamic systems.
Object.assign(Item.prototype, {
update(dt, worldRef = world) { return window.TarinaiItemRuntime?.updateOne?.(this, dt, worldRef) ?? window.TarinaiItemLifecyclePipeline?.updateOne?.(this, dt, worldRef) ?? false; },
updateRotator(dt, worldRef) { return window.TarinaiItemDynamicToolSystem?.updateRotator?.(this, dt, worldRef); },
updateDuplicator(dt, worldRef) { return window.TarinaiItemDynamicDuplicatorSystem?.update?.(this, dt, worldRef); },
updateZunchiMotion(dt, worldRef) { return window.TarinaiItemDynamicZunchiSystem?.updateMotion?.(this, dt, worldRef); },
resolveHighSpeedZunchiTarinaiCollision(dt, worldRef, speed = 0) { return window.TarinaiItemDynamicZunchiSystem?.resolveHighSpeedTarinaiCollision?.(this, dt, worldRef, speed); },
updateBall(dt, worldRef) { return window.TarinaiItemDynamicBallSystem?.update?.(this, dt, worldRef); },
resolveBallObstacleCollisions(dt, worldRef) { return window.TarinaiItemDynamicBallSystem?.resolveObstacleCollisions?.(this, dt, worldRef); },
applyBallHayDrag(bed, dt, worldRef) { return window.TarinaiItemDynamicBallSystem?.applyHayDrag?.(this, bed, dt, worldRef); },
resolveBallCircleBounce(obstacle, hitRadius, restitution, worldRef) { return window.TarinaiItemDynamicBallSystem?.resolveCircleBounce?.(this, obstacle, hitRadius, restitution, worldRef); },
resolveBallRectBounce(rect, worldRef, source = null) { return window.TarinaiItemDynamicBallSystem?.resolveRectBounce?.(this, rect, worldRef, source); },
emitBallBounce(worldRef, obstacle) { return window.TarinaiItemDynamicBallSystem?.emitBounce?.(this, worldRef, obstacle); },
updatePushpin(dt, worldRef) { return window.TarinaiItemDynamicPinSystem?.update?.(this, dt, worldRef); },
tryStickPushpin(worldRef) { return window.TarinaiItemDynamicPinSystem?.tryStick?.(this, worldRef); },
attachPushpin(t, worldRef, d = null) { return window.TarinaiItemDynamicPinSystem?.attach?.(this, t, worldRef, d); },
updateLodgedPushpin(dt, worldRef) { return window.TarinaiItemDynamicPinSystem?.updateLodged?.(this, dt, worldRef); },
detachPushpin(worldRef, reason = "released") { return window.TarinaiItemDynamicPinSystem?.detach?.(this, worldRef, reason); },
updateZunchi(dt, worldRef) { return window.TarinaiItemDynamicZunchiSystem?.updateLifecycle?.(this, dt, worldRef); }
});

View file

@ -6,7 +6,7 @@
(function (global) {
function update(item, dt, worldRef) {
if (!item || item.dead) return { done: true };
return global.TarinaiItemDecaySystem?.update(item, dt, worldRef) || { done: false };
return global.TarinaiItemDecaySystem.update(item, dt, worldRef) || { done: false };
}
global.TarinaiItemDecayLifecycleStep = Object.freeze({ update });

View file

@ -6,7 +6,7 @@
(function (global) {
function update(item, dt, worldRef) {
if (!item || item.dead) return { done: true };
global.TarinaiItemDynamicSystem?.update(item, dt, worldRef);
global.TarinaiItemDynamicSystem.update(item, dt, worldRef);
return { done: false };
}

View file

@ -6,7 +6,7 @@
(function (global) {
function update(item, dt, worldRef) {
if (!item || item.dead) return { done: true };
return global.TarinaiItemGrowthSystem?.update(item, dt, worldRef) || { done: false };
return global.TarinaiItemGrowthSystem.update(item, dt, worldRef) || { done: false };
}
global.TarinaiItemGrowthLifecycleStep = Object.freeze({ update });

View file

@ -1,13 +0,0 @@
"use strict";
// Layer: entity-runtime/items/lifecycle-step
// Compatibility shim for old step lists. Do not call a captured monolithic
// update body; delegate to the concrete pipeline only.
(function (global) {
function update(item, dt, worldRef) {
const ok = global.TarinaiItemLifecyclePipeline?.updateOne?.(item, dt, worldRef);
return { done: true, ok: ok !== false };
}
global.TarinaiItemLegacyLifecycleStep = Object.freeze({ update });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -15,13 +15,13 @@ function duplicatorLoadTypeForItem(item) {
if (!item || item.dead || item.type === "duplicator") return "";
const type = String(item.type || "");
if (!type || type === "grass") return "";
if ((typeof isPinType === "function" && isPinType(type)) && item.pinState === "lodged") return "";
if ((isPinType(type)) && item.pinState === "lodged") return "";
// Explicit list first. \u3053\u308c\u3067\u300c\u305a\u3093\u3060\u9905\u300d\u300c\u3078\u3053\u9905\u300d\u300c\u3051\u3093\u304b\u9905\u300d\u304c
// serving-food \u5224\u5b9a\u3084 medicine role \u306e\u63fa\u308c\u306b\u5de6\u53f3\u3055\u308c\u305a\u8907\u88fd\u6a5f\u3078\u5165\u308b\u3002
if (DUPLICATOR_STORABLE_TYPES.has(type)) return type;
if (typeof isServingFoodType === "function" && isServingFoodType(type)) return type;
if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) return type;
const foodDef = global.TarinaiItemRegistry?.food?.get?.(type);
if (isServingFoodType(type)) return type;
if (isParamEffectItemType(type)) return type;
const foodDef = global.TarinaiItemRegistry.food?.get?.(type);
if (foodDef && (Number(foodDef.nutrition || 0) > 0 || foodDef.effectId)) return type;
if (typeof roleMatches === "function" && roleMatches(item, "medicine")) return type;
return "";
@ -29,37 +29,17 @@ function duplicatorLoadTypeForItem(item) {
function itemAngleForRuntime(item) {
return typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0);
}
const DUPLICATOR_LOAD_DWELL_SECONDS = 0.38;
const DUPLICATOR_REPLACE_DWELL_SECONDS = 0.70;
function duplicatorLoadPoint(duplicator) {
const r = duplicator?.r || 34;
return { x: (duplicator?.x || 0) + r * 0.62, y: (duplicator?.y || 0) + r * 0.24 };
}
function duplicatorCandidateReach(item) {
return Math.max(20, Math.min(36, (item?.r || 12) * 0.82 + 13));
}
function duplicatorCanLoadNow(duplicator, worldRef) {
if (!duplicator || duplicator.type !== "duplicator") return false;
if (duplicator.playerHeld || duplicator._heldByPlayer) return false;
const now = Number(worldRef?.time || 0);
if (now < Number(duplicator.duplicatorLoadSuppressedUntil || 0)) return false;
if (Math.hypot(duplicator.vx || 0, duplicator.vy || 0) > 24) return false;
return true;
}
function syncDuplicatorRoles(item) {
if (!item || item.type !== "duplicator") return;
const type = String(item.storedFoodType || "");
const pinLoaded = Boolean(type && typeof isPinType === "function" && isPinType(type));
const pinLoaded = Boolean(type && isPinType(type));
item.roles.food = Boolean(type && !pinLoaded);
item.roles.medicine = Boolean(type && !pinLoaded && (type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))));
item.roles.medicine = Boolean(type && !pinLoaded && (type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (isParamEffectItemType(type))));
item.roles.drink = Boolean(!pinLoaded && (type === "water" || type === "zunda_juice"));
item.roles.danger = pinLoaded;
}
@ -74,15 +54,12 @@ function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, o
if (!loadType) return false;
const changed = duplicator.storedFoodType !== loadType;
if (!changed && duplicator.storedFoodType && !options.force) return true;
const def = typeof itemDefinition === "function" ? itemDefinition(loadType) : null;
const newLabel = def?.label || (typeof toolLabel === "function" ? toolLabel(loadType) : loadType);
const def = itemDefinition(loadType);
const newLabel = def?.label || toolLabel(loadType);
duplicator.storedFoodType = loadType;
duplicator.storedFoodLabel = newLabel;
syncDuplicatorRoles(duplicator);
duplicator.loadedAt = Number(worldRef?.time || 0);
duplicator._duplicatorCandidateKey = "";
duplicator._duplicatorCandidateSince = 0;
duplicator.duplicatorPinFlash = typeof isPinType === "function" && isPinType(loadType) ? 0.28 : (duplicator.duplicatorPinFlash || 0);
duplicator.duplicatorPinFlash = isPinType(loadType) ? 0.28 : (duplicator.duplicatorPinFlash || 0);
const consume = options.consumeItem;
if (consume && typeof consume === "object") {
consume.amount = 0;
@ -92,20 +69,13 @@ function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, o
worldRef?.markSpatialDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
worldRef?.markTerrainDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
if (options.emit !== false) worldRef?.emit?.("duplicator:loaded", { duplicator, foodType: loadType, replaced: changed, direct: Boolean(options.direct) });
if (options.log !== false) worldRef?.log?.(`\u8907\u88fd\u6a5f\u306b${duplicator.storedFoodLabel}\u3092\u30bb\u30c3\u30c8\u3057\u305f\u3002`, typeof isPinType === "function" && isPinType(loadType) ? "observe" : "food");
if (options.log !== false) worldRef?.log?.(`\u8907\u88fd\u6a5f\u306b${duplicator.storedFoodLabel}\u3092\u30bb\u30c3\u30c8\u3057\u305f\u3002`, isPinType(loadType) ? "observe" : "food");
return true;
}
global.TarinaiDuplicatorRuntime = Object.freeze({
loadTypeForItem: duplicatorLoadTypeForItem,
loadPoint: duplicatorLoadPoint,
setStoredType: duplicatorSetStoredType,
syncRoles: syncDuplicatorRoles,
});
function duplicatorApplyStoredPin(duplicator, worldRef) {
const storedType = String(duplicator?.storedFoodType || "");
if (!duplicator || duplicator.type !== "duplicator" || !(typeof isPinType === "function" && isPinType(storedType))) return false;
if (!duplicator || duplicator.type !== "duplicator" || !isPinType(storedType)) return false;
const now = Number(worldRef?.time || 0);
if (now < Number(duplicator.nextDuplicatedPinAt || 0)) return false;
const dish = duplicatorLoadPoint(duplicator);
@ -121,7 +91,6 @@ function duplicatorApplyStoredPin(duplicator, worldRef) {
pin.noStickUntil = 0;
pin.noStickTargetId = "";
pin.noStickTargetUntil = 0;
pin.duplicatedFromId = duplicator.id || "";
const added = worldRef?.addItem?.(pin, "duplicator-pin", { terrain: false }) || null;
if (!added && worldRef?.items) {
pin.world = worldRef;
@ -129,7 +98,7 @@ function duplicatorApplyStoredPin(duplicator, worldRef) {
worldRef.markItemBucketsDirty?.("duplicator-pin");
worldRef.markSpatialDirty?.("duplicator-pin");
}
const ok = pin.attachPushpin?.(t, worldRef, distXY(dish.x, dish.y, t.x, t.y));
const ok = global.TarinaiItemDynamicPinSystem.attach(pin, t, worldRef, distXY(dish.x, dish.y, t.x, t.y));
if (!ok) {
pin.amount = 0;
continue;
@ -146,16 +115,11 @@ function duplicatorApplyStoredPin(duplicator, worldRef) {
}
global.TarinaiItemLifecycleRuntimeSupport = Object.freeze({
itemAngleForRuntime,
duplicatorLoadTypeForItem,
duplicatorLoadPoint,
duplicatorCandidateReach,
duplicatorCanLoadNow,
syncDuplicatorRoles,
duplicatorSetStoredType,
duplicatorApplyStoredPin,
DUPLICATOR_LOAD_DWELL_SECONDS,
DUPLICATOR_REPLACE_DWELL_SECONDS,
});

View file

@ -1,74 +1,9 @@
"use strict";
// Public facade joining the split item registry sources.
// Public facade for registries that are still consumed across modules.
(function (global) {
function itemTypeDefinition(type = "") {
const id = String(type || "");
const tool = typeof global.itemDefinition === "function" ? global.itemDefinition(id) : null;
const foodDefinitions = global.TarinaiFoodDefinitions || {};
const effectDefinitions = global.TarinaiEffectDefinitions || {};
const visualDefinitions = global.TarinaiItemVisualDefinitions || {};
const traits = global.ITEM_TRAITS || global.TarinaiToolRegistry?.traits || {};
const food = foodDefinitions[id] || null;
const effectId = food?.effectId || (effectDefinitions[id] ? id : "");
const effect = effectId ? (effectDefinitions[effectId] || null) : null;
const visual = visualDefinitions[id] || null;
return {
id,
tool,
food,
effect,
effectId,
visual,
label: tool?.label || food?.label || effect?.label || id,
radius: tool?.radius,
amount: tool?.amount,
placeable: Boolean(tool?.placeable),
scalable: Boolean(tool?.scalable),
traits: Object.fromEntries(Object.keys(traits).map(trait => [trait, typeof global.itemHasTrait === "function" ? global.itemHasTrait(id, trait) : false])),
};
}
function itemTypeLabel(type = "") {
return itemTypeDefinition(type).label;
}
function itemTypeDebugSnapshot() {
const toolDefinitions = global.TOOL_DEFINITIONS || global.TarinaiToolRegistry?.definitions || {};
const foodDefinitions = global.TarinaiFoodDefinitions || {};
const effectDefinitions = global.TarinaiEffectDefinitions || {};
const visualDefinitions = global.TarinaiItemVisualDefinitions || {};
const ids = new Set([
...Object.values(toolDefinitions).map(def => def.itemType || def.id),
...Object.keys(foodDefinitions),
...Object.keys(effectDefinitions),
...Object.keys(visualDefinitions),
]);
return [...ids].sort().map(id => itemTypeDefinition(id));
}
global.itemTypeDefinition = itemTypeDefinition;
global.itemTypeLabel = itemTypeLabel;
global.itemTypeDebugSnapshot = itemTypeDebugSnapshot;
global.TarinaiItemRegistry = Object.freeze({
definition: itemTypeDefinition,
label: itemTypeLabel,
debugSnapshot: itemTypeDebugSnapshot,
tool: global.toolDefinition,
item: global.itemDefinition,
food: global.TarinaiFoodRegistry,
effect: global.TarinaiEffectRegistry,
visual: global.itemVisualDefinition,
hasTrait: global.itemHasTrait,
radius: global.itemRadiusFor,
amount: global.itemAmountFor,
categories: global.toolCategories,
physicsToolIds: () => [...(global.PHYSICS_TOOL_IDS || [])],
isFence: global.isFenceItemType,
isMechanical: global.isMechanicalItemType,
isLink: global.isLinkItemType,
scalableToolIds: global.scalableToolIds,
pinBehavior: global.pinBehaviorFor,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -3,7 +3,7 @@
// 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 version = Number(item?._mechanicalShapeVersion || 0) || 0;
const len = Array.isArray(segments) ? segments.length : 0;
let h = len * 2654435761;
if (Array.isArray(segments)) {
@ -52,7 +52,7 @@ function strokeSegments(ctx, item, segments, extra = "") {
}
function mechanicalRenderTier() {
return window.TarinaiPerf?.renderQualityTier?.() || "high";
return window.TarinaiPerf.renderQualityTier();
}
function drawRopeParticlePath(ctx, particles, ox, oy) {
@ -88,7 +88,7 @@ draw(ctx, t, lighting = null) {
const shadow = projectedShadowParams(lightState, Math.max(0.4, this.r / 18));
if (this.type !== "trace" && this.type !== "splat") {
const zunchiId = this.type === "zunchi" ? (this.zunchiVariant || "zunchi") : null;
const zunchiImg = zunchiId ? (typeof getRenderableImage === "function" ? getRenderableImage(zunchiId, "zunchi") : (images.get(zunchiId) || images.get("zunchi"))) : null;
const zunchiImg = zunchiId ? getRenderableImage(zunchiId, "zunchi") : null;
if (zunchiImg) {
const metrics = getImageMetrics(zunchiId || "zunchi") || getImageMetrics("zunchi");
const ratio = metrics?.ratio || 178 / 236;
@ -127,7 +127,7 @@ draw(ctx, t, lighting = null) {
if (isConsumableSpriteType(this.type)) {
drawConsumableFieldSprite(ctx, this.type, this.r, this.seed);
} else if (this.type === "genkotsu") {
const img = typeof getRenderableImage === "function" ? getRenderableImage("genkotsu", "genkotsu") : images.get("genkotsu");
const img = getRenderableImage("genkotsu", "genkotsu");
const impact = clamp(this.impactFlash || 0, 0, 1);
ctx.save();
ctx.shadowColor = "rgba(54,42,31,0.26)";
@ -137,7 +137,7 @@ draw(ctx, t, lighting = null) {
const stretchX = 1 + impact * 0.05;
ctx.scale(stretchX, squashY);
if (img) {
const metrics = typeof getImageMetrics === "function" ? getImageMetrics("genkotsu") : null;
const metrics = getImageMetrics("genkotsu");
const w = this.r * 2.55;
const h = w * (metrics?.ratio || 1.27);
ctx.drawImage(img, -w * 0.5, -h * 0.76, w, h);
@ -165,9 +165,9 @@ draw(ctx, t, lighting = null) {
} else if (isPinType(this.type)) {
const stuck = this.pinState === "lodged";
const isOshibyo = this.type === "oshibyo";
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(this.type) : null;
const behavior = pinBehaviorFor(this.type);
const assetId = behavior ? (stuck ? behavior.lodgedAsset : behavior.looseAsset) : (stuck ? "pushpin_stuck" : "pushpin");
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
const img = getRenderableImage(assetId, assetId);
ctx.save();
if (stuck) ctx.scale((this.pinVisualSide || 1) < 0 ? -1 : 1, 1);
if (!stuck) ctx.rotate((this.spin || 0) + Math.PI * 0.5);
@ -175,7 +175,7 @@ draw(ctx, t, lighting = null) {
ctx.shadowBlur = 3;
ctx.shadowOffsetY = 2;
if (img) {
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
const metrics = getImageMetrics(assetId);
const ratio = metrics?.ratio || (stuck ? 1 : 2.0);
const w = isOshibyo ? (stuck ? this.r * 1.55 : this.r * 0.78) : (stuck ? this.r * 2.15 : this.r * 1.78);
const h = w * ratio;
@ -297,7 +297,7 @@ draw(ctx, t, lighting = null) {
ctx.save();
const flash = clamp(Number(this.sprinklerFlash || 0) || 0, 0, 1);
const spin = Number(this.sprinklerSpin || 0) || 0;
const range = globalThis.TarinaiItemDynamicToolSystem?.sprinklerRange ? globalThis.TarinaiItemDynamicToolSystem.sprinklerRange(this) : Math.max(288, (Number(this.r || 28) || 28) * 13.8);
const range = globalThis.TarinaiItemDynamicToolSystem.sprinklerRange ? globalThis.TarinaiItemDynamicToolSystem.sprinklerRange(this) : Math.max(288, (Number(this.r || 28) || 28) * 13.8);
const metalA = styleNight ? "#6a7782" : "#d6dde3";
const metalB = styleNight ? "#39434d" : "#8a969f";
const darkMetal = styleNight ? "#1e252b" : "#53606a";
@ -570,7 +570,7 @@ draw(ctx, t, lighting = null) {
ctx.textBaseline = "middle";
ctx.fillText(`${count}`, 0, this.r * 1.25);
} else if (this.type === "ant_corpse") {
const img = typeof getRenderableImage === "function" ? getRenderableImage("ant_worker", "ant_worker") : images.get("ant_worker");
const img = getRenderableImage("ant_worker", "ant_worker");
ctx.save();
ctx.rotate((stableUnit(this.id || this.seed, "ant-corpse-rot") - 0.5) * 0.26);
ctx.scale(1, -1);
@ -642,7 +642,7 @@ draw(ctx, t, lighting = null) {
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence") {
const bounce = this.type === "bounce_fence" || this.type === "bounce_fence_v";
const gate = this.type === "gate_fence";
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
const angle = itemAngleFor(this);
const len = Math.max(112, this.r * 3.55);
const thick = Math.max(10, this.r * 0.31);
const open = gate && this.gateOpen;
@ -717,7 +717,7 @@ draw(ctx, t, lighting = null) {
}
ctx.restore();
} else if (this.type === "rotator") {
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
const angle = itemAngleFor(this);
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;
@ -746,7 +746,7 @@ draw(ctx, t, lighting = null) {
ctx.textBaseline = "middle";
ctx.fillText(powered ? (speed >= 0 ? "\u21bb" : "\u21ba") : "\u25cb", 0, 0);
} else if (this.type === "poison_block") {
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
const angle = itemAngleFor(this);
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]]);
@ -766,9 +766,9 @@ draw(ctx, t, lighting = null) {
// v39.15.53: 毒ブロックは物理輪郭だけを安定描画する。
// 中央の塗りつぶし/文字はキャッシュ更新時に明滅して見えるため描画しない。
} else if (this.type === "reciprocator") {
const bodyAngle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
const bodyAngle = itemAngleFor(this);
const pb = window.TarinaiPhysicsBodySystem;
const axisAngle = window.TarinaiMechanicalSystem?.railAxisAngle?.(this) ?? pb?.scalar?.(this, "railAxis", bodyAngle) ?? bodyAngle;
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();

View file

@ -5,8 +5,8 @@
// through the explicit lifecycle pipeline.
(function (global) {
function installPrototypeBoundary() {
const proto = global.Item?.prototype;
if (!proto || proto._tarinaiItemRuntimeInstalled) return false;
const proto = global.Item.prototype;
if (proto._tarinaiItemRuntimeInstalled) return false;
proto.update = function update(dt, worldRef = global.world) {
return global.TarinaiItemRuntime.updateOne(this, dt, worldRef);
};
@ -19,36 +19,19 @@
}
function updateOne(item, dt, worldRef = global.world) {
if (!item || item.dead || !global.TarinaiItemLifecyclePipeline?.updateOne) return false;
if (!item || item.dead) return false;
return global.TarinaiItemLifecyclePipeline.updateOne(item, dt, worldRef) !== false;
}
function updateScheduled(worldRef, dt) {
if (global.TarinaiItemUpdateScheduler?.run) return global.TarinaiItemUpdateScheduler.run(worldRef, dt);
const helpers = global.TarinaiSimulationRuntime?.helpers;
if (!helpers?.forScheduledItems || !helpers?.itemUpdateInterval) return 0;
let ran = 0;
helpers.forScheduledItems(worldRef, (item) => {
const interval = helpers.itemUpdateInterval(item);
if (!Number.isFinite(interval)) return;
if (interval <= 0) {
if (updateOne(item, dt, worldRef)) ran += 1;
return;
}
item._scheduledUpdateDt = Math.min(10, (item._scheduledUpdateDt || 0) + dt);
if (item._scheduledUpdateDt < interval) return;
const scheduledDt = item._scheduledUpdateDt;
item._scheduledUpdateDt = 0;
if (updateOne(item, scheduledDt, worldRef)) ran += 1;
});
return ran;
return global.TarinaiItemUpdateScheduler.run(worldRef, dt);
}
function trackedDynamicItems(worldRef) {
return global.TarinaiItemUpdateScheduler?.trackedDynamicItems?.(worldRef) || worldRef?._itemUpdateRealtime || [];
return global.TarinaiItemUpdateScheduler.trackedDynamicItems(worldRef);
}
const api = Object.freeze({ installPrototypeBoundary, updateOne, updateScheduled, trackedDynamicItems });
const api = Object.freeze({ updateOne, updateScheduled, trackedDynamicItems });
global.TarinaiItemRuntime = api;
installPrototypeBoundary();
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -41,7 +41,7 @@ const TOOL_DEFINITIONS = Object.freeze({
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u30fb\u85ac\u30fb\u753b\u92f2\u30fb\u304a\u3057\u308a\u92f2\u3092\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002\u92f2\u7cfb\u306f\u89e6\u308c\u305f\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3002" },
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "食べ物、薬、鋲をセットすると無限に供給できる。" },
firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
@ -78,9 +78,6 @@ 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", "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 || "")); }
function isLinkItemType(type = "") { return LINK_ITEM_TYPES.has(String(type || "")); }
function isPhysicsToolId(id = "") { return PHYSICS_TOOL_IDS.includes(String(id || "")); }
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
function defaultItemAngle(type = "") {
const t = String(type || "");
@ -97,9 +94,6 @@ function itemAngleFor(itemOrType = "") {
if (itemOrType && typeof itemOrType === "object") return normalizedItemAngle(itemOrType.angle, defaultItemAngle(itemOrType.type));
return defaultItemAngle(String(itemOrType || ""));
}
function rotatableToolIds() {
return Object.values(TOOL_DEFINITIONS).filter(def => isRotatableItemType(def.itemType || def.id)).map(def => def.id);
}
function itemHasTrait(type = "", trait = "") {
return Boolean(ITEM_TRAITS[trait]?.has?.(String(type || "")));
@ -193,32 +187,17 @@ function toolTipsFromDefinitions() {
Object.assign(global, {
TOOL_DEFINITIONS,
ITEM_TRAITS,
FENCE_ITEM_TYPES,
MECHANICAL_ITEM_TYPES,
LINK_ITEM_TYPES,
PHYSICS_TOOL_IDS,
ROTATABLE_ITEM_TYPES,
isFenceItemType,
isMechanicalItemType,
isLinkItemType,
isPhysicsToolId,
isRotatableItemType,
defaultItemAngle,
normalizedItemAngle,
itemAngleFor,
rotatableToolIds,
itemHasTrait,
isPinType,
isSleepFurnitureType,
isLodgedPin,
lodgedPinFor,
lodgedPinBehaviorFor,
hasLodgedPinEffect,
PIN_BEHAVIORS,
pinBehaviorFor,
TOOL_CATEGORIES,
toolCategories,
toolDefinition,
toolLabel,
@ -228,28 +207,10 @@ function toolTipsFromDefinitions() {
itemAmountFor,
TOOL_SIZE_ORDER,
TOOL_SIZE_LABELS,
TOOL_SIZE_SCALES,
normalizedToolSizeFor,
toolSizeScaleForValue,
toolSizeScaleFor,
scalableToolIds,
toolTipsFromDefinitions,
});
global.TarinaiToolRegistry = Object.freeze({
definitions: TOOL_DEFINITIONS,
traits: ITEM_TRAITS,
categories: toolCategories,
definition: toolDefinition,
label: toolLabel,
itemType: toolItemType,
item: itemDefinition,
radius: itemRadiusFor,
amount: itemAmountFor,
hasTrait: itemHasTrait,
physicsToolIds: () => [...PHYSICS_TOOL_IDS],
rotatableToolIds,
scalableToolIds,
pinBehavior: pinBehaviorFor,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -4,8 +4,6 @@
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 === "zunda_juice") item.roles.drink = true;
if (["sweet", "water", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true;
@ -69,7 +67,6 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "ant_nest") {
item.antCount = ANT_NEST_START_COUNT || 6;
item.antSpawnTimer = rand(0.6, 2.2);
item.antSpawnCooldown = rand(0.8, 2.4);
item.antWorkers = typeof buildAntWorkerPool === "function"
? buildAntWorkerPool(item.antCount)
: Array.from({ length: item.antCount }, () => ANT_WORKER_HP || 32);
@ -111,8 +108,8 @@ function initializeItemTypeState(item, type, x, y) {
item.deletable = true;
}
if (typeof isRotatableItemType === "function" && isRotatableItemType(type)) {
item.angle = typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0;
if (isRotatableItemType(type)) {
item.angle = defaultItemAngle(type);
item.amount = Math.max(item.amount || 0, 999);
}
if (type === "gate_fence") {
@ -122,7 +119,6 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "poison_block") {
item.physicsBody = {
schema: bodySchemaVersion,
type: "poison_block",
kind: "passive",
pose: { x, y, angle: Number(item.angle) || 0 },
@ -146,7 +142,6 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "rotator") {
item.physicsBody = {
schema: bodySchemaVersion,
type: "rotator",
kind: "rotational",
pose: { x, y, angle: Number(item.angle) || 0 },
@ -166,7 +161,6 @@ function initializeItemTypeState(item, type, x, y) {
item.r = 48;
item.deletable = true;
item.physicsConstraint = {
schema: constraintSchemaVersion,
type,
kind: type === "rope" ? "flexible-distance" : "rigid-distance",
endpoints: [null, null],
@ -178,7 +172,6 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "reciprocator") {
item.physicsBody = {
schema: bodySchemaVersion,
type: "reciprocator",
kind: "linear",
pose: { x, y, angle: Number(item.angle) || 0 },
@ -193,9 +186,9 @@ function initializeItemTypeState(item, type, x, y) {
item.amount = 999;
}
if (globalThis.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) {
globalThis.TarinaiPhysicsBodySystem.purgeLegacyPhysicsStorage?.(item);
globalThis.TarinaiPhysicsBodySystem.invalidateItem?.(item, "item-initialized");
if (globalThis.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
globalThis.TarinaiPhysicsBodySystem.purgeLegacyPhysicsStorage(item);
globalThis.TarinaiPhysicsBodySystem.invalidateItem(item, "item-initialized");
}
if (isServingFoodType(type)) {

View file

@ -15,7 +15,7 @@
]);
function mobilePerfHint() {
return Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
return Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
}
function itemUpdateInterval(item) {
@ -38,7 +38,7 @@
if (item.type === "trace" || item.type === "splat") return mobile ? 1.8 : 1.2;
if (item.type === "ant_corpse") return 3.5;
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(item.type)) return 5.0;
if (typeof isServingFoodType === "function" && isServingFoodType(item.type)) return 5.0;
if (isServingFoodType(item.type)) return 5.0;
return Infinity;
}
@ -75,18 +75,11 @@
return out;
}
function isRealtime(item) {
return itemUpdateInterval(item) <= 0;
}
global.TarinaiItemUpdatePolicy = Object.freeze({
REALTIME_ITEM_TYPES,
PIN_ITEM_TYPES,
SCHEDULED_ITEM_TYPES,
itemUpdateInterval,
forScheduledItems,
ensureBuckets,
candidateItems,
isRealtime,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,22 +1,19 @@
"use strict";
(function (global) {
const policy = () => global.TarinaiItemUpdatePolicy || {};
const policy = global.TarinaiItemUpdatePolicy;
function itemUpdateInterval(item) {
const interval = policy().itemUpdateInterval?.(item);
const interval = policy.itemUpdateInterval(item);
return Number.isFinite(interval) ? interval : Infinity;
}
function candidateItems(worldRef) {
if (policy().candidateItems) return policy().candidateItems(worldRef);
return (worldRef?.items || []).filter(it => it && !it.dead && it.type !== "grass");
return policy.candidateItems(worldRef);
}
function ensureBuckets(worldRef) {
if (policy().ensureBuckets) return policy().ensureBuckets(worldRef);
if (worldRef?.ensureItemBuckets) worldRef.ensureItemBuckets("item-update-scheduler");
return undefined;
return policy.ensureBuckets(worldRef);
}
function heapPush(heap, entry) {
@ -111,8 +108,7 @@
continue;
}
if (interval <= 0) {
if (global.TarinaiItemRuntime?.updateOne) global.TarinaiItemRuntime.updateOne(item, dt, worldRef);
else item.update(dt, worldRef);
global.TarinaiItemRuntime.updateOne(item, dt, worldRef);
item._schedulerLastAt = now;
ran += 1;
if (Number.isFinite(itemUpdateInterval(item)) && itemUpdateInterval(item) <= 0) nextRealtime.push(item);
@ -143,8 +139,7 @@
continue;
}
const elapsed = Math.min(10, Math.max(0.001, now - (item._schedulerLastAt ?? entry.time - interval)));
if (global.TarinaiItemRuntime?.updateOne) global.TarinaiItemRuntime.updateOne(item, elapsed, worldRef);
else item.update(elapsed, worldRef);
global.TarinaiItemRuntime.updateOne(item, elapsed, worldRef);
item._schedulerLastAt = now;
ran += 1;
dueRuns += 1;
@ -154,10 +149,10 @@
}
function physicsBudgetFor(worldRef) {
const tier = global.TarinaiPerf?.renderQualityTier?.() || "high";
const tier = global.TarinaiPerf.renderQualityTier();
const itemCount = (worldRef?.items || []).length || 0;
const activeHint = worldRef?._itemUpdateRealtime?.length || 0;
const mobile = Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
if (mobile && tier !== "high") return { maxPairs: 112, maxLinks: 72, postMaxPairs: 28, maxSubsteps: 2 };
if (mobile) return { maxPairs: 150, maxLinks: 96, postMaxPairs: 40, maxSubsteps: 2 };
if (tier === "low") return { maxPairs: 140, maxLinks: 90, postMaxPairs: 36, maxSubsteps: 2 };
@ -173,25 +168,23 @@
}
function run(worldRef, dt) {
const end = global.TarinaiPerf?.begin?.("update.items") || null;
const end = global.TarinaiPerf.begin("update.items");
try {
const state = ensure(worldRef);
const realtimeEnd = global.TarinaiPerf?.begin?.("update.items.realtime") || null;
const realtimeEnd = global.TarinaiPerf.begin("update.items.realtime");
let realtimeRan = runRealtime(worldRef, state, dt);
let ran = realtimeRan;
if (realtimeEnd) realtimeEnd();
const dueEnd = global.TarinaiPerf?.begin?.("update.items.scheduled") || null;
const dueEnd = global.TarinaiPerf.begin("update.items.scheduled");
const dueRan = runDue(worldRef, state);
ran += dueRan;
if (dueEnd) dueEnd();
const physicsEnd = global.TarinaiPerf?.begin?.("update.physicsWorld") || null;
const physicsEnd = global.TarinaiPerf.begin("update.physicsWorld");
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);
const physicsWorld = global.TarinaiPhysicsWorldSystem.updateWorld(worldRef, dt, physicsBudget);
const mechanicalWorld = physicsWorld.mechanical || null;
const constraintWorld = physicsWorld.constraintRan || 0;
const postConstraintPairs = physicsWorld.postConstraintPairs || 0;
const poisonContactRemoved = worldRef.resolvePoisonBlockItemContacts?.({ maxPoison: 24, maxTargets: 80 }) || 0;
if (physicsEnd) physicsEnd();
state.signature = schedulerSignature(worldRef);
@ -212,7 +205,7 @@
mechanicalStats: worldRef._mechanicalWorldStats || null,
constraintStats: worldRef._constraintWorldStats || null,
};
return ran + (physicsWorld?.ran || mechanicalWorld?.ran || 0) + constraintWorld + poisonContactRemoved;
return ran + (physicsWorld.ran || mechanicalWorld?.ran || 0) + constraintWorld + poisonContactRemoved;
} finally {
if (end) end();
}
@ -222,5 +215,5 @@
return worldRef?._itemUpdateRealtime || [];
}
global.TarinaiItemUpdateScheduler = Object.freeze({ run, rebuild, ensure, itemUpdateInterval, trackedDynamicItems });
global.TarinaiItemUpdateScheduler = Object.freeze({ run, trackedDynamicItems });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -119,13 +119,5 @@
function itemVisualDefinition(type = "") {
return defs[String(type || "")] || null;
}
function itemVisualPalette(type = "") {
return itemVisualDefinition(type)?.palette || null;
}
global.TarinaiItemVisualDefinitions = defs;
global.itemVisualDefinition = itemVisualDefinition;
global.itemVisualPalette = itemVisualPalette;
global.TarinaiItemVisualRegistry = Object.freeze({ definitions: defs, definition: itemVisualDefinition, palette: itemVisualPalette });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,8 +1,8 @@
// Item construction and shared item helpers. Type-specific initialization is in item_type_initializers.js.
"use strict";
const FOOD_SERVING_TYPES = new Set(window.TarinaiItemRegistry?.food?.servingTypes?.() || ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
const PARAM_EFFECT_ITEM_TYPES = new Set(window.TarinaiItemRegistry?.food?.paramEffectTypes?.() || ["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
const FOOD_SERVING_TYPES = new Set(window.TarinaiItemRegistry.food?.servingTypes?.() || ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
const PARAM_EFFECT_ITEM_TYPES = new Set(window.TarinaiItemRegistry.food?.paramEffectTypes?.() || ["laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice"]);
function isParamEffectItemType(type = "") {
return PARAM_EFFECT_ITEM_TYPES.has(type);
}
@ -15,7 +15,6 @@ function isServingFoodType(type = "") {
return FOOD_SERVING_TYPES.has(type);
}
const PASSIVE_FOOD_DECAY_PER_SECOND = 0.004;
const PASSIVE_FOOD_HYGIENE_PENALTY_PER_SERVING = 0.055;
const GRASS_STAGE_COUNT = 5;
const GRASS_STAGE_AMOUNTS = [24, 48, 72, 96, 120];
@ -96,12 +95,12 @@ function passiveFoodDecayInterval(worldRef = null) {
function passiveFoodDecayRate(item) {
const size = item?.toolSize || "medium";
const sizeMul = size === "small" ? 0.74 : (size === "large" ? 1.32 : 1);
const typeMul = window.TarinaiItemRegistry?.food?.passiveDecayMultiplier?.(item?.type) ?? (item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1));
const typeMul = window.TarinaiItemRegistry.food?.passiveDecayMultiplier?.(item?.type) ?? (item?.type === "sweet" ? 0.82 : (item?.type === "zunda_juice" ? 1.12 : 1));
return PASSIVE_FOOD_DECAY_PER_SECOND * sizeMul * typeMul;
}
function itemVisualFor(type = "") {
return typeof itemVisualDefinition === "function" ? itemVisualDefinition(type) : null;
return itemVisualDefinition(type);
}
function visualPaletteFor(type = "", fallback = {}) {
@ -250,7 +249,7 @@ function drawBulletSprite(ctx, r) {
function isConsumableSpriteType(type = "") {
const key = String(type || "");
return ["food", "sweet", "love_mochi", "fight_mochi", "sleep_drug"].includes(key)
|| (typeof isParamEffectItemType === "function" && isParamEffectItemType(key));
|| isParamEffectItemType(key);
}
function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {}) {
@ -321,7 +320,7 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
}
} else if (key === "sleep_drug") {
drawSleepTabletSprite(ctx, localR * visualFieldScaleFor("sleep_drug"), visualPaletteFor("sleep_drug"));
} else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(key)) {
} else if (isParamEffectItemType(key)) {
const visual = itemVisualFor(key);
const palette = visual?.palette || {};
const renderer = visual?.renderer || "split_pill";
@ -390,14 +389,14 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
function drawStoredConsumableSprite(ctx, type = "", r = 10, seed = 1) {
const key = String(type || "food");
if (typeof isPinType === "function" && isPinType(key)) {
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(key) : null;
if (isPinType(key)) {
const behavior = pinBehaviorFor(key);
const assetId = behavior?.looseAsset || key;
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
const img = getRenderableImage(assetId, assetId);
ctx.save();
ctx.rotate(-0.35);
if (img) {
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
const metrics = getImageMetrics(assetId);
const ratio = metrics?.ratio || 2.0;
const w = key === "oshibyo" ? r * 1.30 : r * 1.70;
const h = w * ratio;
@ -422,7 +421,7 @@ function drawStoredConsumableSprite(ctx, type = "", r = 10, seed = 1) {
}
function previewItemRadiusFor(type = "") {
const def = typeof itemDefinition === "function" ? itemDefinition(type) : null;
const def = itemDefinition(type);
const base = Number(def?.radius || itemRadiusFor?.(type, 16) || 16);
if (type === "genkotsu") return 30;
if (type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") return 24;
@ -523,8 +522,8 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
fuseTimer: 5,
fuseMax: 5,
impactFlash: 0,
foodServingsMax: typeof foodServingsForSize === "function" ? foodServingsForSize("medium") : 5,
foodServingsRemaining: typeof foodServingsForSize === "function" ? foodServingsForSize("medium") : 5,
foodServingsMax: foodServingsForSize("medium"),
foodServingsRemaining: foodServingsForSize("medium"),
toolSize: "medium",
});
if (type === "water") item.amount = 50;
@ -575,7 +574,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
else if (["grass", "water", "zunda_juice"].includes(type)) scale *= 1.85;
else if (type === "duplicator") scale *= 1.55;
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) scale *= 1.35;
else if (isParamEffectItemType(type)) scale *= 1.35;
if (watermark) scale *= 1.42;
ctx.scale(scale, scale);
try {
@ -598,14 +597,10 @@ if (typeof window !== "undefined") {
window.drawStoredConsumableSprite = drawStoredConsumableSprite;
window.drawToolItemPreview = drawToolItemPreview;
window.TarinaiGrass = {
stageCount: GRASS_STAGE_COUNT,
normalize: normalizeGrassStage,
setStage: setGrassStage,
advance: advanceGrassStage,
regress: regressGrassStage,
amountForStage: grassAmountForStage,
growthForStage: grassGrowthForStage,
stageFromGrowth: grassStageFromGrowth,
};
}

View file

@ -9,7 +9,6 @@ function ensureWorldBootstrap() {
if (typeof globalThis !== "undefined") globalThis.world = currentWorld;
return currentWorld;
} catch (error) {
if (typeof window !== "undefined") window.__worldInitError = error?.stack || String(error);
throw error;
}
}
@ -21,19 +20,10 @@ let fpsTimer = 0;
let fpsFrames = 0;
window.__tarinaiFps = 0;
const LOADING_SPRITES = [
"assets/sprites/tarinai_01_smile.webp",
"assets/sprites/tarinai_02_angry.webp",
"assets/sprites/tarinai_03_teary.webp",
"assets/sprites/tarinai_04_jito.webp",
"assets/sprites/tarinai_05_drool.webp",
"assets/sprites/tarinai_06_cry.webp",
"assets/sprites/tarinai_07_sleep.webp",
"assets/sprites/tarinai_08_pokan.webp",
"assets/sprites/tarinai_11_weak.webp",
"assets/sprites/tarinai_21_normal_happy.webp",
"assets/sprites/tarinai_29_hungry_70.webp",
];
const LOADING_SPRITES = SPRITES
.filter(asset => asset && !asset.actionOnly)
.slice(0, 11)
.map(asset => asset.path);
let loadingSpriteTimer = 0;
function shuffledLoadingSprites() {
@ -94,7 +84,7 @@ function loop() {
const rawDt = t - last;
const dt = clamp(rawDt, 0, 0.05);
last = t;
window.TarinaiPerf?.beginFrame?.();
window.TarinaiPerf.beginFrame();
fpsTimer += rawDt;
fpsFrames += 1;
if (fpsTimer >= 0.5) {
@ -102,14 +92,14 @@ function loop() {
fpsTimer = 0;
fpsFrames = 0;
}
const endUpdate = window.TarinaiPerf?.begin?.("update.total") || null;
const endUpdate = window.TarinaiPerf.begin("update.total");
world.update(dt);
if (endUpdate) endUpdate();
const endRender = window.TarinaiPerf?.begin?.("render.total") || null;
const endRender = window.TarinaiPerf.begin("render.total");
render();
if (endRender) endRender();
window.TarinaiPerf?.endFrame?.(rawDt, window.__tarinaiFps || 0);
if (window.TarinaiPerf?.consumeResizeRequest?.() && typeof resizeCanvas === "function") resizeCanvas();
window.TarinaiPerf.endFrame(rawDt, window.__tarinaiFps || 0);
if (window.TarinaiPerf.consumeResizeRequest() && typeof resizeCanvas === "function") resizeCanvas();
statsTimer += dt;
const statsInterval = 2.5;
if (statsTimer > statsInterval) {
@ -121,7 +111,7 @@ function loop() {
initLoadingShowcase();
setLoadingProgress("\u521d\u671f\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u307f\u4e2d", 0.08);
loadImages(null, (done, total) => {
loadImages((done, total) => {
const p = total > 0 ? done / total : 1;
setLoadingProgress(`\u521d\u671f\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u307f\u4e2d ${done}/${total}`, 0.08 + p * 0.46);
}).then(() => {
@ -138,7 +128,7 @@ loadImages(null, (done, total) => {
render();
renderStats();
syncTopButtons();
if (typeof startBackgroundImageLoading === "function") startBackgroundImageLoading();
startBackgroundImageLoading();
setLoadingProgress("\u30b7\u30df\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u59cb\u4e2d", 0.94);
loop();
requestAnimationFrame(hideLoadingScreen);

View file

@ -9,5 +9,3 @@ const distXY = (ax, ay, bx, by) => Math.hypot(ax - bx, ay - by);
const nowSec = () => performance.now() / 1000;
const fmt = (v) => Math.round(v).toString();
window.TarinaiMath = { rand, randi, pick, clamp, lerp, dist, distXY, nowSec, fmt };

View file

@ -9,24 +9,16 @@
function isMechanicalType(itemOrType) { return Boolean(mech()?.isMechanicalType?.(itemOrType)); }
function worldSegments(item) { return mech()?.worldSegments?.(item) || []; }
function obstacleRects(item) { return mech()?.obstacleRects?.(item) || []; }
function placementRects(item) { return mech()?.placementRects?.(item) || obstacleRects(item); }
function poisonHazardRects(item) { return mech()?.poisonHazardRects?.(item) || []; }
function boundsAabb(item) { return mech()?.boundsAabb?.(item) || null; }
function hazardAabb(item) { return mech()?.hazardAabb?.(item) || null; }
function extent(item) { return mech()?.extent?.(item) || 0; }
function reach(item) { return mech()?.reach?.(item) || extent(item); }
function hitTest(item, x, y, worldRef, opts = {}) { return mech()?.hitTest?.(item, x, y, worldRef, opts) || { hit: false, distance: Infinity }; }
global.TarinaiMechanicalShapeSystem = Object.freeze({
isMechanicalType,
worldSegments,
obstacleRects,
placementRects,
poisonHazardRects,
boundsAabb,
hazardAabb,
extent,
reach,
hitTest,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -3,12 +3,86 @@
// Layer: physics/mechanical-body
// Common body, footprint, motion, and contact runtime for \u56de\u8ee2\u4f53, \u6bd2\u30d6\u30ed\u30c3\u30af, and \u5f80\u5fa9\u4f53.
(function (global) {
const FP = () => global.TarinaiCollisionFootprints || {};
function num(v, fallback = 0) {
const n = Number(v);
return Number.isFinite(n) ? n : fallback;
}
function rectCorners(rect, padding = 0) {
const pad = Math.max(0, num(padding));
if (!rect) return [];
if (!rect.oriented) {
return [
{ x: num(rect.left) - pad, y: num(rect.top) - pad },
{ x: num(rect.right) + pad, y: num(rect.top) - pad },
{ x: num(rect.right) + pad, y: num(rect.bottom) + pad },
{ x: num(rect.left) - pad, y: num(rect.bottom) + pad },
];
}
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(num(rect.angle));
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(num(rect.angle));
const hw = num(rect.halfW) + pad;
const hh = num(rect.halfH) + pad;
return [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]].map(([lx, ly]) => ({
x: num(rect.cx) + lx * c - ly * ss,
y: num(rect.cy) + lx * ss + ly * c,
}));
}
function rectAxes(rect) {
if (!rect?.oriented) return [{ x: 1, y: 0 }, { x: 0, y: 1 }];
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(num(rect.angle));
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(num(rect.angle));
return [{ x: c, y: ss }, { x: -ss, y: c }];
}
function projectPoints(points, axis) {
let min = Infinity;
let max = -Infinity;
for (const p of points || []) {
const v = num(p.x) * axis.x + num(p.y) * axis.y;
if (v < min) min = v;
if (v > max) max = v;
}
return { min, max };
}
function aabbOverlap(a, b, padding = 0) {
const pad = Math.max(0, num(padding));
return Boolean(a && b && num(a.left) <= num(b.right) + pad && num(a.right) >= num(b.left) - pad
&& num(a.top) <= num(b.bottom) + pad && num(a.bottom) >= num(b.top) - pad);
}
function rectOverlapInfo(a, b, padding = 0) {
if (!aabbOverlap(a, b, padding)) return null;
const ptsA = rectCorners(a, padding);
const ptsB = rectCorners(b, padding);
if (!ptsA.length || !ptsB.length) return null;
let bestOverlap = Infinity;
let bestAxis = null;
for (const axis of [...rectAxes(a), ...rectAxes(b)]) {
const len = Math.hypot(axis.x, axis.y) || 1;
const n = { x: axis.x / len, y: axis.y / len };
const pa = projectPoints(ptsA, n);
const pb = projectPoints(ptsB, n);
const overlap = Math.min(pa.max, pb.max) - Math.max(pa.min, pb.min);
if (overlap <= 0) return null;
if (overlap < bestOverlap) { bestOverlap = overlap; bestAxis = n; }
}
if (!bestAxis) return null;
const acx = num(a.cx, (num(a.left) + num(a.right)) * 0.5);
const acy = num(a.cy, (num(a.top) + num(a.bottom)) * 0.5);
const bcx = num(b.cx, (num(b.left) + num(b.right)) * 0.5);
const bcy = num(b.cy, (num(b.top) + num(b.bottom)) * 0.5);
if ((bcx - acx) * bestAxis.x + (bcy - acy) * bestAxis.y < 0) bestAxis = { x: -bestAxis.x, y: -bestAxis.y };
return {
overlap: bestOverlap,
nx: bestAxis.x,
ny: bestAxis.y,
x: (Math.max(num(a.left), num(b.left)) + Math.min(num(a.right), num(b.right))) * 0.5,
y: (Math.max(num(a.top), num(b.top)) + Math.min(num(a.bottom), num(b.bottom))) * 0.5,
};
}
function typeOf(itemOrType) { return typeof itemOrType === "string" ? itemOrType : String(itemOrType?.type || ""); }
function isMechanicalType(itemOrType) { const t = typeOf(itemOrType); return t === "rotator" || t === "poison_block" || t === "reciprocator"; }
function motionType(itemOrType) { const t = typeOf(itemOrType); return t === "rotator" ? "rotate" : (t === "reciprocator" ? "reciprocate" : (t === "poison_block" ? "passive" : "none")); }
@ -70,7 +144,7 @@
function shapeVersion(item) {
const shape = bodyShape(item);
return Number(shape?.version ?? item?._mechanicalShapeVersion ?? item?._segmentsVersion ?? 0) || 0;
return Number(shape?.version ?? item?._mechanicalShapeVersion ?? 0) || 0;
}
function invalidateGeometry(item) {
@ -84,7 +158,6 @@
}
item._mechanicalShapeVersion = nextVersion;
item._mechanicalGeomCache = null;
item._mechanicalSupportVersion = nextVersion;
item.world?.markSpatialDirty?.("mechanical-geometry-edited");
item.world?.markItemBucketsDirty?.("mechanical-geometry-edited");
return true;
@ -219,8 +292,6 @@
if (cache) {
cache.localKey = key;
cache.localSegments = out;
cache.displaySegmentCount = normalized.length;
cache.physicsSegmentCount = out.length;
}
return out;
}
@ -298,16 +369,6 @@
return { x: num(rail?.anchorX, ps(item, "railAnchorX", item.x)) + a.x * travel * phase, y: num(rail?.anchorY, ps(item, "railAnchorY", item.y)) + a.y * travel * phase };
}
function phaseFromPosition(item) {
resetAnchor(item);
const a = axis(item);
const travel = halfTravel(item);
const pose = bodyPose(item);
const rail = bodyRail(item);
const dx = num(pose?.x, item.x) - num(rail?.anchorX, ps(item, "railAnchorX", item.x));
const dy = num(pose?.y, item.y) - num(rail?.anchorY, ps(item, "railAnchorY", item.y));
return Math.max(-1, Math.min(1, (dx * a.x + dy * a.y) / Math.max(10, travel)));
}
function signedDrive(item) {
if (!item) return 0;
@ -941,11 +1002,9 @@
function strongestRectContact(rectsA, rectsB, padding = 0) {
let best = null;
const overlapInfo = FP().rectOverlapInfo;
if (typeof overlapInfo !== "function") return null;
for (const ra of rectsA || []) {
for (const rb of rectsB || []) {
const info = overlapInfo(ra, rb, padding);
const info = rectOverlapInfo(ra, rb, padding);
if (!info) continue;
if (!best || info.overlap > best.info.overlap) best = { ra, rb, info };
}
@ -1044,7 +1103,7 @@
const now = worldRef.time || 0;
for (const mechRect of rects) {
for (const fenceRect of fences) {
let info = FP().rectOverlapInfo?.(mechRect, fenceRect, item.type === "rotator" ? 1.35 : 0.5);
let info = rectOverlapInfo(mechRect, fenceRect, item.type === "rotator" ? 1.35 : 0.5);
if (!info) continue;
const separated = applyMechanicalFenceSeparation(item, info, dt, worldRef);
if (separated) {
@ -1697,7 +1756,7 @@
function updateMechanicalWorld(worldRef, dt = 0.016, opts = {}) {
if (!worldRef?.itemsOfType) return { ran: 0, active: 0, solved: 0, fenceSolved: 0, links: 0 };
const profiler = global.TarinaiPerf;
const end = profiler?.begin?.("update.mechanicalWorld") || null;
const end = profiler.begin("update.mechanicalWorld") || null;
try {
const rawDt = Number(dt || 0.016) || 0.016;
const clampedDt = Math.max(0.001, Math.min(0.05, rawDt));
@ -1759,7 +1818,7 @@
}
// Resolve links after all motors/passive bodies have moved, so constraints see
// one coherent world-state rather than each item solving in isolation.
const links = opts.skipLinks === true ? 0 : (global.TarinaiConstraintSystem?.updateWorld?.(worldRef, clampedDt, { maxLinks: opts.maxLinks || 160 }) || 0);
const links = opts.skipLinks === true ? 0 : (global.TarinaiConstraintSystem.updateWorld(worldRef, clampedDt, { maxLinks: opts.maxLinks || 160 }) || 0);
// Build a single physics frame after motion and reuse it for pair and fence passes.
// This is intentionally save-incompatible: transient body records are runtime only.
const frame = buildMechanicalFrame(worldRef, { bodies });
@ -1799,7 +1858,7 @@
if (item.type === "rotator" && !isPowered(item)) {
const a = itemAngle(item);
const pose = bodyPose(item);
const side = Math.sign((num(x) - num(pose?.x, item.x)) * Math.cos(a + Math.PI / 2) + (num(y) - num(pose?.y, item.y)) * Math.sin(a + Math.PI / 2)) || ((typeof stableUnit === "function" ? stableUnit(item.id || item.seed || "rotator", "poke-side") : 0.35) < 0.5 ? -1 : 1);
const side = Math.sign((num(x) - num(pose?.x, item.x)) * Math.cos(a + Math.PI / 2) + (num(y) - num(pose?.y, item.y)) * Math.sin(a + Math.PI / 2)) || (stableUnit(item.id || item.seed || "rotator", "poke-side") < 0.5 ? -1 : 1);
pset(item, "spin", clamp(num(bodyVelocity(item)?.angular, ps(item, "spin", 0)) * 0.82 + side * 0.32, -1.6, 1.6), "pair-spin-response");
commitBody(item, "poke-passive-rotator");
worldRef?.markSpatialDirty?.("poke-passive-rotator");
@ -1808,7 +1867,7 @@
if (item.type === "reciprocator" && !isPowered(item)) {
const a = axis(item);
const pose = bodyPose(item);
const side = Math.sign((num(x) - num(pose?.x, item.x)) * a.x + (num(y) - num(pose?.y, item.y)) * a.y) || ((typeof stableUnit === "function" ? stableUnit(item.id || item.seed || "reciprocator", "poke-side") : 0.65) < 0.5 ? -1 : 1);
const side = Math.sign((num(x) - num(pose?.x, item.x)) * a.x + (num(y) - num(pose?.y, item.y)) * a.y) || (stableUnit(item.id || item.seed || "reciprocator", "poke-side") < 0.5 ? -1 : 1);
pset(item, "slideSpeed", clamp(num(bodyVelocity(item)?.linear, ps(item, "slideSpeed", 0)) * 0.76 + side * 34, -115, 115), "pair-slide-response");
commitBody(item, "poke-passive-reciprocator");
worldRef?.markSpatialDirty?.("poke-passive-reciprocator");
@ -1818,56 +1877,24 @@
}
function hitTest(worldRef, item, x, y, opts = {}) {
return FP().hitTestItem?.(worldRef, item, x, y, opts) || { hit: false, distance: Infinity };
return global.TarinaiCollisionFootprints.hitTestItem(worldRef, item, x, y, opts) || { hit: false, distance: Infinity };
}
global.TarinaiMechanicalSystem = Object.freeze({
isMechanicalType,
motionType,
isPowered,
itemAngle,
sanitizeSegments,
thickness,
extent,
worldSegments,
obstacleRects,
placementRects,
poisonHazardRects,
boundsAabb,
hazardBoundsAabb,
axis,
railAxisAngle,
halfTravel,
resetAnchor,
positionFromPhase,
phaseFromPosition,
signedDrive,
pointVelocity,
applyImpulse,
applyPassiveReciprocatorImpulse,
applyPassiveRotatorImpulse,
applyRailCorrection,
reverseReciprocatorOnMechanicalContact,
resolveReciprocatorPhysicalContacts,
repairPoweredReciprocatorStall,
brakePoweredRotatorOnMechanicalContact,
applySurfaceVelocityToCircle,
strongestRectContact,
resolvePair,
resolveFenceContacts,
resolveInteractions,
resolveMechanicalPairs,
resolveMechanicalPairsBroadphase,
updateWorld: updateMechanicalWorld,
collectMechanicalBodies,
isMechanicallyActive,
estimatedBodyMotion,
shouldCollideMechanical,
hasInteractionCandidates,
updateRotator,
updateReciprocator,
updatePoisonBlock,
resolvePoisonBlockTarinaiContacts,
applyPokeImpulse,
hitTest,
reach,

View file

@ -1,16 +0,0 @@
"use strict";
(function (global) {
const applied = new Set();
function patchPrototype(key, owner, patchFn) {
if (!key || !owner?.prototype || typeof patchFn !== "function") return false;
const fullKey = `${owner.name || "Owner"}:${key}`;
if (applied.has(fullKey) || owner.prototype[`__${key}Patched`]) return false;
patchFn(owner.prototype);
applied.add(fullKey);
owner.prototype[`__${key}Patched`] = true;
return true;
}
function patchWorld(key, patchFn) { return patchPrototype(key, global.World, patchFn); }
global.TarinaiPatcher = { patchPrototype, patchWorld, applied };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -17,7 +17,7 @@
let frameOpen = null;
let frameCount = 0;
let lastQualityCheckAt = 0;
const mobilePerfHint = () => Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
const mobilePerfHint = () => Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
let tier = mobilePerfHint() ? "mid" : "high";
let dprScale = mobilePerfHint() ? 0.82 : 1;
let resizeRequested = false;
@ -154,7 +154,6 @@
}
global.TarinaiPerf = Object.freeze({
enabled: debugEnabled,
begin,
beginFrame,
endFrame,

View file

@ -3,22 +3,11 @@
(function (global) {
function rectLocalPoint(rect, x, y) {
const shared = global.TarinaiCollisionFootprints?.rectLocalPoint;
if (typeof shared === "function") return shared(rect, x, y);
const dx = x - (rect.cx ?? 0);
const dy = y - (rect.cy ?? 0);
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(rect.angle || 0);
return { x: dx * c + dy * ss, y: -dx * ss + dy * c };
return global.TarinaiCollisionFootprints.rectLocalPoint(rect, x, y);
}
function rectWorldVector(rect, x, y) {
const shared = global.TarinaiCollisionFootprints?.rectWorldVector;
if (typeof shared === "function") return shared(rect, x, y);
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(rect.angle || 0);
return { x: x * c - y * ss, y: x * ss + y * c };
return global.TarinaiCollisionFootprints.rectWorldVector(rect, x, y);
}
function circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts = {}) {
@ -90,7 +79,7 @@
}
}
if (rect.mechanical) {
global.TarinaiMechanicalSystem?.applySurfaceVelocityToCircle?.(obj, rect, nx, ny, vx, vy, {
global.TarinaiMechanicalSystem.applySurfaceVelocityToCircle(obj, rect, nx, ny, vx, vy, {
damping: 1,
surfaceScale: rect.rotator ? 0.28 : 0.30,
normalBoost: rect.rotator ? 20 : 14,
@ -269,5 +258,5 @@
return speed;
}
global.TarinaiPhysics = { bounceInsideWorld, bounceCircleOffRect, resolveKinematicBounceFenceCollision, applyKinematicItemMotion };
global.TarinaiPhysics = { bounceCircleOffRect, applyKinematicItemMotion };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -6,29 +6,6 @@
(function (global) {
function num(v, fallback = 0) { const n = Number(v); return Number.isFinite(n) ? n : fallback; }
function clampInsideWorld(worldRef, obj, radius = 0) {
if (!worldRef || !obj) return false;
const pad = Math.max(CONFIG.worldPadding || 30, num(radius, 0));
const ox = num(obj.x), oy = num(obj.y);
obj.x = clamp(ox, pad, worldRef.w - pad);
obj.y = clamp(oy, pad, worldRef.h - pad);
return obj.x !== ox || obj.y !== oy;
}
function projectPair(worldRef, a, b, nx, ny, overlap, weights = {}) {
const amount = Math.max(0, num(overlap));
if (!worldRef || !a || !b || amount <= 0) return false;
const aw = Number.isFinite(Number(weights.a)) ? Number(weights.a) : 0.5;
const bw = Number.isFinite(Number(weights.b)) ? Number(weights.b) : 0.5;
a.x = num(a.x) - num(nx) * amount * aw;
a.y = num(a.y) - num(ny) * amount * aw;
b.x = num(b.x) + num(nx) * amount * bw;
b.y = num(b.y) + num(ny) * amount * bw;
clampInsideWorld(worldRef, a, a.r || a.radius || 0);
clampInsideWorld(worldRef, b, b.r || b.radius || 0);
return true;
}
function markSpatialDirty(worldRef, item, reason = "projection", threshold = 0.35, minInterval = 0.065) {
const world = worldRef || item?.world || null;
if (!world || !item) return false;
@ -50,8 +27,6 @@
}
global.TarinaiPhysicsProjectionSystem = Object.freeze({
clampInsideWorld,
projectPair,
markSpatialDirty,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -60,9 +60,6 @@
return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u));
}
global.TarinaiPhysicsShapeEditorSystem = Object.freeze({
MODE_LABELS,
TEMPLATE_LABELS,
ACTION_LABELS,
decorateToolbar,
templateSegments,
pointSegmentDistance,

View file

@ -4,8 +4,6 @@
// 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"]);
@ -54,13 +52,11 @@
}
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 },
@ -80,7 +76,6 @@
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;
@ -160,28 +155,6 @@
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) {
@ -247,7 +220,7 @@
}
normalizeBody(b, item);
markBodyChanged(item, reason);
if (key === "thickness" || key === "solid") global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
if (key === "thickness" || key === "solid") global.TarinaiMechanicalSystem.invalidateGeometry(item);
return true;
}
@ -260,7 +233,7 @@
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);
global.TarinaiMechanicalSystem.invalidateGeometry(item);
return true;
}
function pose(item) { return ensureBody(item, item?.world || null, { syncFromLegacy: false })?.pose || null; }
@ -327,7 +300,6 @@
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],
@ -340,7 +312,6 @@
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];
@ -414,12 +385,6 @@
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 = [];
@ -434,7 +399,7 @@
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 } };
const frame = { 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;
}
@ -455,15 +420,14 @@
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;
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 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;
const stats = { ran: (mechanicalStats?.ran || 0) + constraintRan, bodies: frame.bodyCount, constraints: frame.constraintCount, mechanical: mechanicalStats, constraintRan, postConstraintPairs, committed, sync: frame.sync || null };
return stats;
} finally { if (end) end(); }
}
@ -531,7 +495,7 @@
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);
item.physicsConstraint = normalizeConstraint({ 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 = []) {
@ -544,7 +508,7 @@
if (!item) return false;
if (isBodyType(item)) {
ensureBody(item, item.world || null, { rebuild: false, syncFromLegacy: false });
global.TarinaiMechanicalSystem?.invalidateGeometry?.(item);
global.TarinaiMechanicalSystem.invalidateGeometry(item);
markBodyChanged(item, reason);
return true;
}
@ -568,22 +532,19 @@
// 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 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,
isPhysicsType,
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,
compactItemExtra, applyCompactExtra, purgeLegacyPhysicsStorage,
invalidateItem,
scalar, setScalar, segments, setSegments, syncPoseFromItem,
endpoint, setEndpoint, linkScalar, setLinkScalar, particles, setParticles,
});
global.TarinaiPhysicsBodySystem = bodyApi;
global.TarinaiPhysicsWorldSystem = Object.freeze({ updateWorld, prepareWorld, commitWorld, collectPhysicsItems, bodyApi });
global.TarinaiPhysicsWorldSystem = Object.freeze({ updateWorld });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -117,14 +117,9 @@
}
global.TarinaiPinAttachmentSystem = Object.freeze({
isPin,
behaviorFor,
isOshibyo,
tarinaiVisualSide,
tarinaiAnchor,
applyTarinaiPose,
ballAnchor,
applyBallPose,
isAttachedToEntity,
drawAttachedForEntity,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -41,28 +41,17 @@
function orientedAabb(rect) {
if (!rect) return null;
if (!rect.oriented) return rect;
const fp = global.TarinaiCollisionFootprints;
const pts = fp?.rectCorners?.(rect, 0) || [];
if (!pts.length) {
const reach = Math.max(num(rect.halfW), num(rect.halfH));
return { left: num(rect.cx) - reach, right: num(rect.cx) + reach, top: num(rect.cy) - reach, bottom: num(rect.cy) + reach };
}
return {
left: Math.min(...pts.map(p => num(p.x))),
right: Math.max(...pts.map(p => num(p.x))),
top: Math.min(...pts.map(p => num(p.y))),
bottom: Math.max(...pts.map(p => num(p.y))),
};
const reach = Math.max(num(rect.halfW), num(rect.halfH));
return { left: num(rect.cx) - reach, right: num(rect.cx) + reach, top: num(rect.cy) - reach, bottom: num(rect.cy) + reach };
}
function rectsFor(worldRef, item) {
return (global.TarinaiPlacementCollisionSystem || global.TarinaiCollisionFootprints)?.placementRectsFor?.(worldRef, item) || worldRef?.solidObstacleRects?.(item) || [];
return global.TarinaiCollisionFootprints.placementRectsFor(worldRef, item);
}
function footprintRadius(worldRef, item, rects = null) {
return (global.TarinaiCollisionFootprints || global.TarinaiPlacementCollisionSystem)?.itemFootprintRadius?.(worldRef, item, rects)
|| Math.max(14, num(item?.r, itemRadius(item?.type, 12)) || itemRadius(item?.type, 12));
return global.TarinaiCollisionFootprints.itemFootprintRadius(worldRef, item, rects);
}
function ensureMechanicalPreview(item, worldRef) {
if (!item || !global.TarinaiMechanicalSystem?.isMechanicalType?.(item.type)) return item;
if (!item || !global.TarinaiMechanicalSystem.isMechanicalType(item.type)) return item;
const pb = global.TarinaiPhysicsBodySystem;
pb?.ensureBody?.(item, worldRef || null, { syncFromLegacy: false });
if (item.type === "rotator") {
@ -94,7 +83,7 @@
if (isRotatable(type)) item.angle = opts.angle ?? (worldRef?.toolAngleFor ? worldRef.toolAngleFor(type) : defaultAngle(type));
if (type === "reciprocator") {
const axis = item.angle ?? defaultAngle(type);
global.TarinaiPhysicsBodySystem?.setScalar?.(item, "railAxis", axis, "preview-axis");
global.TarinaiPhysicsBodySystem.setScalar(item, "railAxis", axis, "preview-axis");
item.angle = defaultAngle(type);
}
ensureMechanicalPreview(item, worldRef);
@ -137,14 +126,6 @@
const rects = rectsFor(worldRef, item);
return { rect: null, rects };
}
function blockedFor(worldRef, item, opts = {}) {
const geometry = opts.geometry || geometryFor(worldRef, item) || {};
const rects = geometry.rects && geometry.rects.length ? geometry.rects : (geometry.rect ? [geometry.rect] : null);
const b = boundsBlocked(worldRef, item, { rects });
if (b) return true;
if (opts.skipWorldBlocked) return false;
return Boolean(worldRef?.placementBlocked?.(item) || (worldRef?.fencePlacementBlocked?.(item) && worldRef?.isFenceType?.(item.type)));
}
function previewForItem(worldRef, item, opts = {}) {
if (!worldRef || !item) return null;
const geometry = geometryFor(worldRef, item) || {};
@ -172,12 +153,12 @@
const r = tmp.r;
let extra = {};
if (type === "sprinkler") {
const range = global.TarinaiItemDynamicToolSystem?.sprinklerRange?.(tmp) || Math.max(288, r * 13.8);
const range = global.TarinaiItemDynamicToolSystem.sprinklerRange(tmp) || Math.max(288, r * 13.8);
extra.influence = { kind: "sprinkler", x, y, radius: range };
extra.boundsRadius = Math.max(14, r * 1.25);
}
if (type === "grass") {
const overlaps = global.TarinaiPhysicsSoftClear?.collectForPlacement?.(worldRef, tmp) || [];
const overlaps = global.TarinaiPhysicsSoftClear.collectForPlacement(worldRef, tmp) || [];
const replacingGrass = overlaps.some(it => it?.type === "grass");
const limitBlocked = !(worldRef.canAddGrass?.(1) ?? true) && !replacingGrass;
const softBlocked = Boolean(worldRef.grassBlockedAt?.(x, y, tmp, { ignoreSoftPlacement: true }) || worldRef.grassOnTarinaiAt?.(x, y));
@ -191,13 +172,8 @@
return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], influence: extra.influence || null, blocked, item: tmp };
}
global.TarinaiPlacementPreviewSystem = Object.freeze({
makePreviewItem,
rectOutside,
circleOutside,
boundsBlocked,
clampPointFor,
geometryFor,
blockedFor,
forItem: previewForItem,
forWorld: previewForWorld,
});

View file

@ -5,16 +5,9 @@
constructor(defs = {}) { this.defs = new Map(Object.entries(defs || {})); }
key(id = "") { return String(id || ""); }
get(id = "") { return this.defs.get(this.key(id)) || null; }
has(id = "") { return this.defs.has(this.key(id)); }
entries() { return [...this.defs.entries()]; }
values() { return [...this.defs.values()]; }
label(id = "", fallback = "") { return this.get(id)?.label || fallback || id || ""; }
color(id = "", fallback = "rgba(255,242,160,0.85)") { return this.get(id)?.color || fallback; }
labels() { return Object.fromEntries(this.entries().map(([id, def]) => [id, def.label])); }
colors() { return Object.fromEntries(this.entries().map(([id, def]) => [id, def.color]).filter(([, color]) => color)); }
debugSnapshot(mapper = null) {
return this.entries().map(([id, def]) => mapper ? mapper(id, def) : ({ id, label: def.label || id }));
}
}
global.TarinaiRegistryBase = TarinaiRegistryBase;
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -7,7 +7,7 @@ const toolCursorIconImages = new Map();
function toolCursorIconDefinition(world) {
const tool = String(world?.tool || "");
if (!tool || tool === "observe" || tool === "undo" || tool === "redo" || tool === "shoot") return null;
const def = typeof toolDefinition === "function" ? toolDefinition(tool) : null;
const def = toolDefinition(tool);
if (!def) return null;
return { tool, def, type: def.itemType || (def.placeable ? tool : "") };
}
@ -236,56 +236,6 @@ function drawToolTargetHoverHighlight(ctx, world) {
ctx.restore();
}
function drawShootHoverHighlight(ctx, world) {
if (!world || world.tool !== "shoot") return;
const aim = world.shootAimPoint?.();
if (!aim?.inside) return;
const target = world.hoverShootTarget;
if (!target || target.dead) return;
ctx.save();
const pulse = 0.52 + Math.sin((world.time || 0) * 14) * 0.12;
ctx.globalAlpha = 0.72 + pulse * 0.12;
ctx.lineWidth = Math.max(1.8, (world.screenSizeToWorld ? world.screenSizeToWorld(2.0) : 2.0));
ctx.strokeStyle = "rgba(255,106,92,0.94)";
ctx.shadowColor = "rgba(255,188,146,0.44)";
ctx.shadowBlur = Math.max(4, (world.screenSizeToWorld ? world.screenSizeToWorld(6) : 6));
const isTarinaiTarget = typeof Tarinai !== "undefined" && target instanceof Tarinai;
const isItemTarget = Boolean(target.type);
if (isTarinaiTarget) {
const r = Math.max(16, target.radius || 22) * 1.12;
ctx.beginPath();
ctx.ellipse(target.x || 0, target.y || 0, r * 0.95, r * 0.76, 0, 0, Math.PI * 2);
ctx.stroke();
} else if (isItemTarget) {
const rects = world.solidObstacleRects?.(target) || [];
if (target.type === "rope" || target.type === "rod") {
const runtime = world.TarinaiLinkRuntime || TARINAI_RENDER_GLOBAL.TarinaiLinkRuntime;
const pb = TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem;
const a = runtime?.endpointWorld?.(pb?.endpoint?.(target, 0), world);
const b = runtime?.endpointWorld?.(pb?.endpoint?.(target, 1), world);
if (a && b) {
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
} else if (rects.length) {
for (const rect of rects.slice(0, 12)) strokeWorldObstacleRect(ctx, rect);
} else {
const r = Math.max(12, target.r || 14) * (target.type === "ball" ? 1.24 : 1.14);
ctx.beginPath();
ctx.ellipse(target.x || 0, target.y || 0, r, r * 0.76, 0, 0, Math.PI * 2);
ctx.stroke();
}
} else {
const r = Math.max(8, target.r || 8) * 1.45;
ctx.beginPath();
ctx.ellipse(target.x || 0, target.y || 0, r, r * 0.86, 0, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
function drawShootCursorFallback(ctx, x, y, size) {
const half = size * 0.5;
const tick = size * 0.16;
@ -324,7 +274,7 @@ function drawShootCursorOverlay(ctx, world) {
const canvasW = world.viewportW || world.w || ctx.canvas?.width || 1;
const canvasH = world.viewportH || world.h || ctx.canvas?.height || 1;
const size = Math.max(90, Math.min(160, Math.round(Math.min(canvasW, canvasH) * 0.205)));
const def = typeof toolDefinition === "function" ? toolDefinition("shoot") : null;
const def = toolDefinition("shoot");
const img = cachedToolCursorIconImage(def?.icon || "");
ctx.save();
ctx.globalAlpha = 0.95;
@ -489,7 +439,7 @@ function drawDeviceInfluenceShape(ctx, influence, blocked = false, alphaScale =
}
function placementPreviewFor(world) {
return TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem?.forWorld?.(world) || null;
return TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem.forWorld(world) || null;
}
@ -561,7 +511,7 @@ function drawOperationToolPreview(ctx, world) {
const type = world.copyBuffer.type || "";
const item = world.copyPreviewItemAt?.(p.x, p.y);
if (item) {
const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem?.forItem?.(world, item) || null;
const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem.forItem(world, item) || null;
const previewRects = placementInfo?.rects || world.solidObstacleRects?.(item) || [];
const boundsBlocked = Boolean(placementInfo?.boundsBlocked);
const blocked = Boolean(placementInfo ? placementInfo.blocked : (world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item)));
@ -727,7 +677,6 @@ function backgroundCacheKey(world, lighting) {
function invalidateRenderCaches(reason = "manual") {
if (backgroundCache) backgroundCache.key = "";
if (terrainCache) terrainCache.key = "";
if (terrainChunkCache) {
terrainChunkCache.key = "";
terrainChunkCache.chunks?.clear?.();
@ -738,13 +687,6 @@ function invalidateRenderCaches(reason = "manual") {
// static visual noise. Rendering them into visible chunks avoids repeating
// thousands of small strokes every frame and avoids redrawing the entire field
// when a small terrain area changes.
const terrainCache = {
canvas: null,
ctx: null,
w: 0,
h: 0,
key: "",
};
const terrainChunkCache = {
size: 256,
@ -837,7 +779,7 @@ function redrawTerrainChunk(chunk, worldRef, lighting) {
}
function drawTerrainLayer(ctx, worldRef, lighting, visibleRect) {
const end = window.TarinaiPerf?.begin?.("render.terrain") || null;
const end = window.TarinaiPerf.begin("render.terrain");
try {
const key = terrainCacheKey(worldRef, lighting);
if (terrainChunkCache.key !== key || worldRef.terrainDirtyGlobal || (worldRef.terrainDirty && !worldRef.terrainDirtyChunks?.size)) {
@ -871,38 +813,6 @@ function drawTerrainLayer(ctx, worldRef, lighting, visibleRect) {
}
}
function ensureTerrainCache(worldRef, lighting) {
// Compatibility fallback for older callers. The main renderer uses chunks.
const w = Math.max(1, Math.floor(worldRef.w || 1));
const h = Math.max(1, Math.floor(worldRef.h || 1));
const key = `${terrainCacheKey(worldRef, lighting)}:fallback:v${worldRef.terrainVersion || 0}`;
if (!terrainCache.canvas) {
terrainCache.canvas = document.createElement("canvas");
terrainCache.ctx = terrainCache.canvas.getContext("2d");
}
if (!worldRef.terrainDirty && terrainCache.w === w && terrainCache.h === h && terrainCache.key === key) return terrainCache.canvas;
terrainCache.w = w;
terrainCache.h = h;
terrainCache.key = key;
terrainCache.canvas.width = w;
terrainCache.canvas.height = h;
const c = terrainCache.ctx;
c.clearRect(0, 0, w, h);
const oldPointer = worldRef.pointer;
try {
if (oldPointer) worldRef.pointer = { ...oldPointer, inside: false };
for (const it of worldRef.items || []) if (isCachedTerrainItem(it)) it.draw(c, worldRef.time || 0, lighting);
worldRef.terrainDirty = false;
return terrainCache.canvas;
} catch (err) {
console.error("terrain cache redraw failed", err);
terrainCache.key = "";
return null;
} finally {
if (oldPointer) worldRef.pointer = oldPointer;
}
}
function visibleWorldRect(worldRef, margin = 160) {
const scale = worldRef.viewScale ? worldRef.viewScale() : 1;
const vw = (worldRef.viewportW || worldRef.w || 1000) / Math.max(0.01, scale);
@ -921,7 +831,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 + (TARINAI_RENDER_GLOBAL.TarinaiPhysicsBodySystem?.scalar?.(entity, "railTravel", 150) || 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;
@ -939,15 +849,6 @@ function isEntityVisibleInRect(entity, rect, extra = 0) {
return x + r >= rect.left && x - r <= rect.right && y + r >= rect.top && y - r <= rect.bottom;
}
function renderStackSignature(worldRef) {
return [
(worldRef.items || []).length,
(worldRef.tarinai || []).length,
(worldRef.ants || []).length,
worldRef.terrainVersion || 0,
].join(":");
}
function syncCarriedPlushieToOwner(it, worldRef) {
if (!it || !worldRef || !it.isStructure || it.type !== "plushie" || !it.carriedById) return false;
const owner = (worldRef.tarinai || []).find(t => t && !t.dead && t.id === it.carriedById);
@ -960,54 +861,9 @@ function syncCarriedPlushieToOwner(it, worldRef) {
return Math.hypot((it.x || 0) - (ox || 0), (it.y || 0) - (oy || 0)) > 0.25;
}
function ensureWorldRenderStack(worldRef) {
if (!worldRef._renderStack) {
worldRef._renderStack = { signature: "", backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] };
}
const stack = worldRef._renderStack;
const signature = renderStackSignature(worldRef);
if (!worldRef.drawListDirty && stack.signature === signature) return stack;
stack.backItems.length = 0;
stack.layered.length = 0;
stack.carriedPlushies.length = 0;
stack.lodgedPins.length = 0;
if (stack.ballLodgedPins) stack.ballLodgedPins.length = 0;
for (const it of worldRef.items || []) {
if (!it || it.dead || isCachedTerrainItem(it)) continue;
if (isPinType(it.type) && it.pinState === "lodged") {
stack.lodgedPins.push(it);
continue;
}
if (isPinType(it.type) && it.pinState === "ball_lodged") {
(stack.ballLodgedPins || (stack.ballLodgedPins = [])).push(it);
continue;
}
if (it.isStructure && it.type === "plushie" && it.carriedById) {
stack.carriedPlushies.push(it);
continue;
}
if (isBehindSpriteLayerItem(it)) stack.backItems.push(it);
else stack.layered.push({ entity: it, y: renderLayerSortY(it), rank: renderLayerKindRank(it) });
}
for (const t of worldRef.tarinai || []) {
if (t && !t.dead) stack.layered.push({ entity: t, y: renderLayerSortY(t), rank: renderLayerKindRank(t) });
}
for (const a of worldRef.ants || []) {
if (a && !a.dead) stack.layered.push({ entity: a, y: renderLayerSortY(a), rank: renderLayerKindRank(a) });
}
stack.backItems.sort((a, b) => renderLayerSortY(a) - renderLayerSortY(b) || (a.x || 0) - (b.x || 0));
stack.layered.sort((a, b) => a.y - b.y || a.rank - b.rank || ((a.entity.x || 0) - (b.entity.x || 0)));
stack.signature = signature;
worldRef.drawListDirty = false;
return stack;
}
function collectVisibleRenderStack(worldRef, visibleRect) {
const end = window.TarinaiPerf?.begin?.("render.stackBuild") || null;
const end = window.TarinaiPerf.begin("render.stackBuild");
try {
if (!worldRef._visibleRenderStack) worldRef._visibleRenderStack = { backItems: [], layered: [], carriedPlushies: [], lodgedPins: [], ballLodgedPins: [] };
const stack = worldRef._visibleRenderStack;
@ -1021,11 +877,11 @@ function collectVisibleRenderStack(worldRef, visibleRect) {
if (it && it.isStructure && it.type === "plushie" && it.carriedById) syncCarriedPlushieToOwner(it, worldRef);
if (!it || it.dead || seen.has(it) || isCachedTerrainItem(it) || !isEntityVisibleInRect(it, visibleRect)) return;
seen.add(it);
if (typeof isPinType === "function" && isPinType(it.type) && it.pinState === "lodged") {
if (isPinType(it.type) && it.pinState === "lodged") {
stack.lodgedPins.push(it);
return;
}
if (typeof isPinType === "function" && isPinType(it.type) && it.pinState === "ball_lodged") {
if (isPinType(it.type) && it.pinState === "ball_lodged") {
(stack.ballLodgedPins || (stack.ballLodgedPins = [])).push(it);
return;
}
@ -1506,7 +1362,7 @@ function weatherLabel(weather) {
}
function renderPerfBegin(label) {
return window.TarinaiPerf?.begin?.(label) || null;
return window.TarinaiPerf.begin(label);
}
function renderPerfEnd(end) {
@ -1578,9 +1434,9 @@ function render() {
// 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);
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) {
@ -1593,7 +1449,7 @@ function render() {
const drawnAttachedPinIds = new Set();
const drawAttachedPinsForEntity = (entity) => {
if (!entity) return;
if (TARINAI_RENDER_GLOBAL.TarinaiPinAttachmentSystem?.drawAttachedForEntity) {
if (TARINAI_RENDER_GLOBAL.TarinaiPinAttachmentSystem.drawAttachedForEntity) {
TARINAI_RENDER_GLOBAL.TarinaiPinAttachmentSystem.drawAttachedForEntity({ renderStack, entity, visibleRect, worldRef: world, time: world.time, drawnIds: drawnAttachedPinIds, isVisible: isEntityVisibleInRect, ctx, lighting });
return;
}
@ -1791,7 +1647,7 @@ function drawSelectedCard(ctx, world, lighting) {
ctx.font = "10px ui-rounded, sans-serif";
const child = t.juvenile && t.parentToFollow?.() ? " / \u5b50" : "";
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35);
ctx.fillText(`${window.TEXT_CATALOG?.stateLabel?.(t.state) || t.state || ""}${child}${zHint}`, x + 10, y + 35);
const genLabel = (t.displayGeneration && t.displayGeneration()) ? ` \u4e16\u4ee3 ${t.generation}` : "";
ctx.fillText(`\u7dcf\u5408\u7684\u6c17\u5206 ${fmt(t.mood)}${genLabel} \u4f53\u529b ${fmt(t.energy)}/${fmt(typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100))}`, x + 10, y + 52);
ctx.fillText(`\u30b9\u30c8\u30ec\u30b9 ${fmt(t.stress)} \u7a7a\u8179 ${fmt(t.hunger)}`, x + 10, y + 68);
@ -1809,4 +1665,4 @@ function roundedRect(ctx, x, y, w, h, r) {
}
window.TarinaiRender = { invalidateRenderCaches, randSeed, roundedBlob, getLightingState, projectedShadowParams, imageVisibleBottomFromTop, drawImageProjectedShadow, drawProjectedShadow, drawRadialGlow, drawItemGlow, drawCreatureGlow, drawDawnRimLight, drawGardenBackdrop, drawGardenBed, drawLightRays, drawAtmosphericLighting, drawRain, fillScreenFallback, drawSelectedCard, render, roundedRect };
window.TarinaiRender = { invalidateRenderCaches };

View file

@ -25,9 +25,5 @@
return result;
}
global.TarinaiRestoreCoordinator = {
restoreSnapshot,
syncRestoreDependents,
renderWorldAfterRestore,
};
global.TarinaiRestoreCoordinator = { restoreSnapshot };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -12,6 +12,10 @@
BINARY_SCHEMA_VERSION,
FIELD_IDS,
GROUND_IDS,
MOOD_IDS,
LEGACY_MOOD_IDS,
canonicalMoodId,
enumIndex,
ITEM_TYPE_IDS,
STRUCTURE_TYPE_IDS,
SERVING_FOOD_TYPE_IDS,
@ -249,15 +253,7 @@
return bytes;
}
function writeFixed(writer, arr = [], count = 0, signed = true) {
for (let i = 0; i < count; i++) signed ? writer.s(arr[i] || 0) : writer.u(arr[i] || 0);
}
function readFixed(reader, count = 0, signed = true) {
const out = [];
for (let i = 0; i < count; i++) out.push(signed ? reader.s() : reader.u());
return out;
}
function writeFixedPacked(writer, arr = [], count = 0, bits = 8, step = 1) {
function writeFixedPacked(writer, arr = [], count = 0, bits = 8, step = 1) {
writeQuantizedBitFields(writer, arr, count, bits, step);
}
function readFixedPacked(reader, count = 0, bits = 8, step = 1) {
@ -298,57 +294,8 @@
return out;
}
function generatedNameCode(value = "") {
const name = String(value || "");
if (!name.startsWith("\u306f")) return -1;
const stop = name.endsWith("\u3063");
const core = name.slice(1, stop ? -1 : undefined);
if (core.length > 6) return -1;
let bits = 0;
for (let i = 0; i < core.length; i++) {
const ch = core.charAt(i);
if (ch !== "\u3046" && ch !== "\u3045") return -1;
if (ch === "\u3045") bits |= (1 << i);
}
return core.length | (stop ? 8 : 0) | (bits << 4);
}
function nameFromGeneratedCode(code = 0) {
const middleLen = code & 7;
const stop = !!(code & 8);
const bits = code >> 4;
let name = "\u306f";
for (let i = 0; i < middleLen; i++) name += (bits & (1 << i)) ? "\u3045" : "\u3046";
if (stop) name += "\u3063";
return name;
}
function writeName(writer, value = "") {
const code = generatedNameCode(value);
if (code >= 0) {
writer.u(code + 1);
return;
}
writer.u(0);
writer.str(value || "");
}
function readName(reader) {
const code = reader.u();
if (code > 0) return nameFromGeneratedCode(code - 1);
return reader.str();
}
function valuesDiffer(a, b) { return (Number(a) || 0) !== (Number(b) || 0); }
function writeFixedDiff(writer, base = [], current = [], count = 0, signed = false) {
let mask = 0;
for (let i = 0; i < count; i++) if (valuesDiffer(current?.[i], base?.[i])) mask |= (1 << i);
writer.u(mask);
for (let i = 0; i < count; i++) if (mask & (1 << i)) signed ? writer.s(current[i] || 0) : writer.u(current[i] || 0);
}
function readFixedDiff(reader, base = [], count = 0, signed = false) {
const out = Array.from({ length: count }, (_, i) => base?.[i] || 0);
const mask = reader.u();
for (let i = 0; i < count; i++) if (mask & (1 << i)) out[i] = signed ? reader.s() : reader.u();
return out;
}
function writeTimers(writer, timers = []) {
function valuesDiffer(a, b) { return (Number(a) || 0) !== (Number(b) || 0); }
function writeTimers(writer, timers = []) {
const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100];
let mask = 0;
for (let i = 0; i < defaults.length; i++) if (valuesDiffer(timers?.[i], defaults[i])) mask |= (1 << i);
@ -407,7 +354,7 @@
function seedPersonalityArray(seed = "", flags = 0) {
// \u305a\u3093\u3061\u3069\u308c\u3044\u306f\u8a95\u751f\u6642\u6027\u683c\u3082\u5168\u8ef8 -1 \u306a\u306e\u3067\u3001\u5dee\u5206\u57fa\u6e96\u30820\u56fa\u5b9a\u306b\u3059\u308b\u3002
if ((Number(flags) || 0) & (1 << 5)) return [0, 0, 0, 0];
return global.TarinaiSeedFactory?.personalityArray?.(seed) || [100, 100, 100, 100];
return global.TarinaiSeedFactory.personalityArray(seed) || [100, 100, 100, 100];
}
function writeTarinai(writer, row = [], coordState = null) {
@ -618,10 +565,14 @@
function decodeBinarySnapshot(bytes) {
const reader = new BinaryReader(bytes);
const schema = reader.b();
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
if (schema !== BINARY_SCHEMA_VERSION && schema !== 15) throw new Error("unsupported binary save schema");
const snapshotVersion = reader.u();
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
if (snapshotVersion !== Snapshot.version && snapshotVersion !== 11) throw new Error("unsupported binary save version");
const w = readWorld(reader);
if (schema === 15) {
const legacyMood = LEGACY_MOOD_IDS?.[Number(w[2]) | 0] || "relaxed";
w[2] = enumIndex(MOOD_IDS, canonicalMoodId?.(legacyMood) || "relaxed");
}
const tCount = reader.u();
const t = [];
const tarCoordState = { x: 0, y: 0 };

View file

@ -4,13 +4,13 @@
// Owns stable save identifiers shared by snapshot_system.js and save_codec.js.
// Current-format save identifiers only.
(function (global) {
const SNAPSHOT_VERSION = 11;
const BINARY_SCHEMA_VERSION = 15;
const SNAPSHOT_VERSION = 12;
const BINARY_SCHEMA_VERSION = 16;
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
const WEATHER_IDS = Object.freeze(["sunny", "cloudy", "light_rain"]);
const MOOD_IDS = Object.freeze(["relaxed", "calm", "prickly", "stinky", "weaklings", "zunda_party", "ball_party", "sun_party", "ant_overrun"]);
const MOOD_IDS = Object.freeze(["relaxed", "devastation", "crisis", "confusion", "fearful", "disease_spread", "weakened", "breeding", "happy", "overcrowded", "isolated"]);
const STATE_IDS = Object.freeze(["idle", "sleep", "panic", "sunbath", "fight", "seek_food", "seek_bed", "wander"]);
const POWER_MODE_IDS = Object.freeze(["", "protein", "niteropu"]);
const SIZE_MODE_IDS = Object.freeze(["", "giant_drug", "dwarf_drug"]);
@ -31,9 +31,6 @@
const STRUCTURE_TYPE_IDS = typeIds(["grass_bed", "plushie"]);
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", "poison_block", "reciprocator"]);
const LINK_ITEM_TYPE_IDS = typeIds(["rope", "rod"]);
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"]);
@ -42,17 +39,30 @@
return i >= 0 ? i : fallback;
}
const LEGACY_MOOD_IDS = Object.freeze(["relaxed", "calm", "prickly", "stinky", "weaklings", "zunda_party", "ball_party", "sun_party", "ant_overrun"]);
const LEGACY_MOOD_ALIASES = Object.freeze({
calm: "relaxed",
prickly: "confusion",
stinky: "disease_spread",
weaklings: "weakened",
zunda_party: "happy",
ball_party: "happy",
sun_party: "happy",
ant_overrun: "fearful",
});
function canonicalMoodId(value = "relaxed") {
const id = String(value || "relaxed");
return LEGACY_MOOD_ALIASES[id] || id;
}
function enumValue(list, index, fallback = "") {
const i = Number(index) | 0;
return list[i] ?? fallback ?? list[0] ?? "";
}
function itemTypeId(type, fallback = -1) {
return enumIndex(ITEM_TYPE_IDS, type, fallback);
}
function itemTypeValue(index, fallback = "") {
return enumValue(ITEM_TYPE_IDS, index, fallback);
function moodValue(index, fallback = "relaxed") {
const i = Number(index) | 0;
return canonicalMoodId(MOOD_IDS[i] ?? LEGACY_MOOD_IDS[i] ?? fallback);
}
global.TarinaiSaveSchema = Object.freeze({
@ -62,6 +72,10 @@
GROUND_IDS,
WEATHER_IDS,
MOOD_IDS,
LEGACY_MOOD_IDS,
LEGACY_MOOD_ALIASES,
canonicalMoodId,
moodValue,
STATE_IDS,
POWER_MODE_IDS,
SIZE_MODE_IDS,
@ -72,14 +86,9 @@
STRUCTURE_TYPE_IDS,
SERVING_FOOD_TYPE_IDS,
PIN_TYPE_IDS,
FENCE_ITEM_TYPE_IDS,
MECHANICAL_ITEM_TYPE_IDS,
LINK_ITEM_TYPE_IDS,
JSON_EXTRA_ITEM_TYPE_IDS,
ROTATABLE_ITEM_TYPE_IDS,
enumIndex,
enumValue,
itemTypeId,
itemTypeValue,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -31,5 +31,5 @@
global.localStorage?.removeItem(slotKey(slot));
global.localStorage?.removeItem(metaKey(slot));
}
global.TarinaiSaveStorage = { STORAGE_PREFIX, SLOT_COUNT, writeSlot, readSlot, requireSlot, deleteSlot };
global.TarinaiSaveStorage = { SLOT_COUNT, writeSlot, readSlot, requireSlot, deleteSlot };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -191,16 +191,5 @@
});
}
global.TarinaiSaveSystem = {
createSnapshot,
restoreSnapshot,
encodeSnapshot,
decodeSnapshot,
saveSlot,
loadSlot,
deleteSlot,
readSlot,
bindSaveSystem,
openDialog,
};
global.TarinaiSaveSystem = { bindSaveSystem };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -64,33 +64,6 @@ function stableUnit(seed, salt = "") {
return ((h >>> 0) % 100000) / 100000;
}
function stableKeyPart(value) {
const shared = globalThis.TarinaiDeterministicHelpers?.keyPart;
if (typeof shared === "function") return shared(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);
}
if (typeof window !== "undefined") {
window.TarinaiDeterminism = {
stableKeyPart,
deterministicFrame: window.deterministicFrame,
deterministicUnit: window.deterministicUnit,
deterministicRange: window.deterministicRange,
deterministicSigned: window.deterministicSigned,
deterministicChance: window.deterministicChance,
deterministicAngle: window.deterministicAngle,
};
}
const GENETIC_KEYS = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul"];
const GENETIC_LIMITS = Object.freeze({
@ -164,14 +137,6 @@ function tarinaiMaxEnergy(tarinai = null) {
return Math.round(clamp(100 * tarinaiBodySizeRatio(tarinai), 28, 240));
}
function clampTarinaiEnergy(tarinai = null) {
if (!tarinai) return 100;
const max = tarinaiMaxEnergy(tarinai);
tarinai.maxEnergy = max;
tarinai.energy = clamp(Number(tarinai.energy) || 0, 0, max);
return max;
}
function tarinaiEnergyRatio(tarinai = null) {
const max = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai?.maxEnergy) || 100));
return clamp((Number(tarinai?.energy) || 0) / max, 0, 1);
@ -185,8 +150,6 @@ function tarinaiMetabolicHungerScale(tarinai = null) {
return clamp(attack * speed * size, 0.18, 8.0);
}
const PERSONALITIES = Object.freeze({});
const PERSONALITY_IDS = [];
const PERSONALITY_KEYS = ["aggression", "openness", "sociability", "neuroticism"];
const PERSONALITY_DAILY_LIMIT = { perAxis: 0.08, total: 0.20 };
const FRIEND_AFFINITY_THRESHOLD = 14;
@ -235,14 +198,6 @@ function randomBirthPersonality(seed = "") {
return out;
}
function personalityFromParents(a, b) {
const pa = ensurePersonality(a).currentPersonality;
const pb = ensurePersonality(b).currentPersonality;
const out = {};
for (const key of PERSONALITY_KEYS) out[key] = clampPersonalityValue(((pa[key] || 0) + (pb[key] || 0)) / 2 + rand(-0.2, 0.2));
return out;
}
function getTraitStrength(value) {
const v = clampPersonalityValue(value);
if (v >= 0.75) return { direction: 1, strength: "strong" };
@ -696,21 +651,6 @@ class SpatialGrid {
this.dynamicHazardCells.clear();
}
clearStaticItems(opts = {}) {
this.staticItemCells.clear();
this.staticObstacleCells.clear();
this.staticFoodCells.clear();
this.staticHazardCells.clear();
if (opts.rebuildCombined) this.rebuildCombinedItemCells();
}
clearDynamicItems(opts = {}) {
this.dynamicItemCells.clear();
this.dynamicObstacleCells.clear();
this.dynamicFoodCells.clear();
this.dynamicHazardCells.clear();
if (opts.rebuildCombined) this.rebuildCombinedItemCells();
}
clearTarinai() {
this.tarinaiCells.clear();
@ -771,7 +711,7 @@ class SpatialGrid {
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;
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) {
@ -811,7 +751,7 @@ class SpatialGrid {
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 (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;
@ -823,11 +763,11 @@ class SpatialGrid {
const obstacleMap = dynamic ? this.dynamicObstacleCells : this.staticObstacleCells;
const foodMap = dynamic ? this.dynamicFoodCells : this.staticFoodCells;
const hazardMap = dynamic ? this.dynamicHazardCells : this.staticHazardCells;
const hasObstacle = typeof itemHasTrait === "function" ? itemHasTrait(type, "obstacle") : (type === "stone" || type === "ball" || type === "nest_box" || type === "bed" || type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v");
const hasFood = typeof itemHasTrait === "function" ? itemHasTrait(type, "food_interest") : (type === "sweet" || type === "love_mochi" || type === "fight_mochi" || type === "grass" || type === "water" || type === "ant_corpse" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)));
const hasHazard = typeof itemHasTrait === "function" && itemHasTrait(type, "hazard");
const hasObstacle = itemHasTrait(type, "obstacle");
const hasFood = itemHasTrait(type, "food_interest");
const hasHazard = itemHasTrait(type, "hazard");
if (type === "poison_block") {
const body = (typeof window !== "undefined" ? window : globalThis).TarinaiPhysicsBodySystem?.ensureBody?.(it, null, { syncFromLegacy: false });
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");

View file

@ -5,19 +5,9 @@
(function (global) {
function update(worldRef, dt) {
const order = global.TarinaiSystemOrder;
if (order?.update) return order.update(worldRef, dt);
const phases = global.TarinaiWorldUpdatePhases;
if (!phases?.update) throw new Error("TarinaiWorldUpdatePhases is not available");
return phases.update(worldRef, dt);
if (!order?.update) throw new Error("TarinaiSystemOrder is not available");
return order.update(worldRef, dt);
}
function phases() {
return global.TarinaiWorldUpdatePhases?.phases || null;
}
function systemOrder() {
return global.TarinaiSystemOrder?.names || null;
}
global.TarinaiSimulation = Object.freeze({ update, phases, systemOrder });
global.TarinaiSimulation = Object.freeze({ update });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -15,7 +15,7 @@
function spawnAmbientGrass(worldRef, dt) {
if ((worldRef.groundGrassMultiplier?.() ?? 1) <= 0) return;
const groundGrowthMul = Math.max(0, Number(global.TarinaiGround?.grassGrowthMultiplier?.(worldRef.groundType || "soil") ?? (worldRef.groundGrassMultiplier?.() ?? 1)) || 0);
const groundGrowthMul = Math.max(0, Number(global.TarinaiGround.grassGrowthMultiplier(worldRef.groundType || "soil") ?? (worldRef.groundGrassMultiplier?.() ?? 1)) || 0);
if (groundGrowthMul <= 0) return;
if ((worldRef.canAddGrass?.(1) ?? true) && Math.random() < dt * 0.070 * groundGrowthMul) {
const spot = worldRef.findGrassPlantingSpot(rand(60, worldRef.w - 60), rand(60, worldRef.h - 60), {
@ -26,7 +26,7 @@
});
if (spot) {
const sprout = new Item("grass", spot.x, spot.y);
global.TarinaiGrass?.setStage?.(sprout, 0, { force: true });
global.TarinaiGrass.setStage(sprout, 0, { force: true });
if (worldRef.addItem) worldRef.addItem(sprout, "grass-spawn");
else worldRef.items.push(sprout);
}
@ -37,7 +37,7 @@
// Rain visuals are diagonal line streaks in render.js. This branch creates
// horizontal oval water items spawned by rain; it intentionally does not
// share the rain overlay renderer.
if (global.TarinaiWeatherSystem?.shouldDropRainWater?.(worldRef, dt)) {
if (global.TarinaiWeatherSystem.shouldDropRainWater(worldRef, dt)) {
const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef);
worldRef.addItem?.(drop, "rain-water") || worldRef.items.push(drop);
worldRef.emit?.("rain:itemDropped", { item: drop, type: "water" });

View file

@ -4,8 +4,8 @@
// Owns Tarinai update cadence, Tarinai movement invalidation, and creature
// collision/interactions scheduled after individual updates.
(function (global) {
const helpers = () => global.TarinaiSimulationRuntime?.helpers || {};
const updatePolicy = () => global.TarinaiCreatureUpdatePolicy || {};
const helpers = global.TarinaiSimulationRuntime.helpers;
const updatePolicy = global.TarinaiCreatureUpdatePolicy;
function inc(map, key, n = 1) {
const k = String(key || "none");
@ -13,9 +13,9 @@
}
function updateCreatures(worldRef, dt) {
const h = helpers();
const end = global.TarinaiPerf?.begin?.("update.tarinai.individual") || null;
const visibleRect = h.updateVisibleWorldRect?.(worldRef, 160) || { left: -Infinity, top: -Infinity, right: Infinity, bottom: Infinity };
const h = helpers;
const end = global.TarinaiPerf.begin("update.tarinai.individual") || null;
const visibleRect = h.updateVisibleWorldRect(worldRef, 160);
const now = worldRef.time || 0;
const stats = {
total: 0,
@ -46,9 +46,9 @@
for (const t of worldRef.tarinai) {
if (!t || t.dead) continue;
stats.total += 1;
const behaviorId = updatePolicy().behaviorId?.(t) || t.state || "none";
const behaviorId = updatePolicy.behaviorId(t);
inc(stats.states, behaviorId);
const cadence = updatePolicy().updateCadence?.(worldRef, t, visibleRect) || { interval: 0, lane: "legacy", smoothMotion: false, visible: true, urgent: false };
const cadence = updatePolicy.updateCadence(worldRef, t, visibleRect);
const interval = Number.isFinite(cadence.interval) ? cadence.interval : Infinity;
inc(stats.lanes, cadence.lane || "unknown");
if (worldRef.selected === t) stats.selected += 1;
@ -66,25 +66,23 @@
t._updateAccum = 0;
stats.full += 1;
stats.realtime += 1;
if (global.TarinaiCreatureRuntime?.updateOne) global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
else t.update(runDt);
global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
continue;
}
t._updateAccum = Math.min(2.4, (t._updateAccum || 0) + dt);
if (!Number.isFinite(t._nextLowFreqUpdateAt)) {
const jitter = typeof stableUnit === "function" ? stableUnit(t.id || t.familyKey || Math.random(), "creature-cadence") : Math.random();
const jitter = stableUnit(t.id || t.familyKey || Math.random(), "creature-cadence");
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();
const jitter = stableUnit(t.id || t.familyKey || Math.random(), `creature-cadence:${Math.floor(now * 2)}`);
t._updateAccum = 0;
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) {
global.TarinaiCreatureRuntime.updateOne(t, runDt, { context });
} else if (cadence.smoothMotion) {
if (global.TarinaiCreatureRuntime.updateMotionOnly(t, dt, { context })) {
stats.smooth += 1;
inc(stats.smoothStates, behaviorId);
@ -108,15 +106,15 @@
} else {
worldRef.workStats && (worldRef.workStats.bedConflictSkips = (worldRef.workStats.bedConflictSkips || 0) + 1);
}
const endSpatial = global.TarinaiPerf?.begin?.("update.tarinai.spatial") || null;
const tarinaiMoved = h.markSpatialDirtyIfMoved?.(worldRef, worldRef.tarinai, "tarinai-moved", 0.25) || false;
const endSpatial = global.TarinaiPerf.begin("update.tarinai.spatial") || null;
const tarinaiMoved = h.markSpatialDirtyIfMoved(worldRef, worldRef.tarinai, "tarinai-moved", 0.25);
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;
const endBallInteractions = global.TarinaiPerf.begin("update.ballInteractions") || null;
worldRef.limitBallChasers();
worldRef.resolveBallInteractions(dt);
if (endBallInteractions) endBallInteractions();
@ -128,7 +126,7 @@
if (hasFastTarinai || now >= (worldRef.nextTarinaiCollisionCheckAt || 0)) {
worldRef.nextTarinaiCollisionCheckAt = now + collisionInterval;
worldRef.workStats && (worldRef.workStats.collisionRuns = (worldRef.workStats.collisionRuns || 0) + 1);
const endCollision = global.TarinaiPerf?.begin?.("update.tarinai.collisions") || null;
const endCollision = global.TarinaiPerf.begin("update.tarinai.collisions") || null;
try { worldRef.resolveTarinaiHighSpeedCollisions(dt); }
finally { if (endCollision) endCollision(); }
} else {
@ -137,11 +135,6 @@
}
const phases = Object.freeze({ updateCreatures });
const helpersApi = Object.freeze({
updateInterval(worldRef, tarinai, visibleRect) {
return updatePolicy().updateInterval?.(worldRef, tarinai, visibleRect) ?? Infinity;
},
});
global.TarinaiCreatureSimulationSystem = Object.freeze({ phases, helpers: helpersApi });
global.TarinaiCreatureSimulationSystem = Object.freeze({ phases });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -6,24 +6,16 @@
(function (global) {
function normalizeDelta(worldRef, dt) {
if (!worldRef || worldRef.paused) return null;
let frameDt = dt * worldRef.speed;
frameDt = Math.min(frameDt, 0.12 * worldRef.speed);
return frameDt;
return dt * worldRef.speed;
}
function updateClock(worldRef, dt) {
const prevTime = worldRef.time;
worldRef.time += dt;
const previousDay = worldRef.day || 1;
worldRef.time += dt;
worldRef.day = Math.floor(worldRef.time / CONFIG.dayLength) + 1;
const prevHour = worldRef.hourOfDay ? worldRef.hourOfDay(prevTime) : 0;
const nowHour = worldRef.hourOfDay ? worldRef.hourOfDay(worldRef.time) : 0;
const crossedColonyEvalHour = (worldRef.day !== previousDay) ? (nowHour >= 6) : (prevHour < 6 && nowHour >= 6);
const pendingColonyMoodEvaluation = crossedColonyEvalHour && worldRef.lastColonyMoodDay !== worldRef.day;
if (worldRef.day !== previousDay) {
for (const t of worldRef.tarinai || []) if (t) t.personalityDaily = { day: worldRef.day, total: 0, byKey: {}, byCause: {} };
}
return { prevTime, previousDay, prevHour, nowHour, crossedColonyEvalHour, pendingColonyMoodEvaluation };
}
function updateFrameSignals(worldRef, dt) {
@ -44,7 +36,6 @@
function updateEnvironment(worldRef, dt) {
worldRef.updateWeather(dt);
worldRef.foodSpoilagePenalty = Math.max(0, (worldRef.foodSpoilagePenalty || 0) - dt * 0.010);
worldRef.updateTerrainDirtyState?.(dt);
worldRef.evaluateColonyMood?.();
}

View file

@ -4,12 +4,12 @@
// Owns scheduled item updates, random grass growth, ball/ant spatial refresh,
// and item/ant spatial post-processing.
(function (global) {
const helpers = () => global.TarinaiSimulationRuntime?.helpers || {};
const helpers = global.TarinaiSimulationRuntime.helpers;
function updateRandomGrassGrowth(worldRef, dt) {
if (!worldRef || !global.TarinaiGrass) return false;
if ((worldRef.groundGrassMultiplier?.() ?? 1) <= 0) return false;
const groundGrowthMul = Math.max(0, Number(global.TarinaiGround?.grassGrowthMultiplier?.(worldRef.groundType || "soil") ?? (worldRef.groundGrassMultiplier?.() ?? 1)) || 0);
const groundGrowthMul = Math.max(0, Number(global.TarinaiGround.grassGrowthMultiplier(worldRef.groundType || "soil") ?? (worldRef.groundGrassMultiplier?.() ?? 1)) || 0);
if (groundGrowthMul <= 0) return false;
worldRef.grassGrowthTimer = (worldRef.grassGrowthTimer || 0) + dt;
const field = worldRef.fieldType || "garden";
@ -40,7 +40,7 @@
}
function updateItemsAndAnts(worldRef, dt) {
const h = helpers();
const h = helpers;
const itemCountBefore = worldRef.items.length;
const prevMode = worldRef.deferSpatialRebuildMode || "";
worldRef.deferSpatialRebuildDepth = (worldRef.deferSpatialRebuildDepth || 0) + 1;
@ -48,16 +48,16 @@
let itemsMoved = false;
let antsMoved = false;
try {
h.updateItemsScheduled?.(worldRef, dt);
h.updateItemsScheduled(worldRef, dt);
updateRandomGrassGrowth(worldRef, dt);
itemsMoved = h.markScheduledItemsMoved?.(worldRef, "items-moved", 0.25) || false;
itemsMoved = h.markScheduledItemsMoved(worldRef, "items-moved", 0.25);
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;
antsMoved = h.markSpatialDirtyIfMoved(worldRef, worldRef.ants || [], "ants-moved", 0.25);
} finally {
worldRef.deferSpatialRebuildDepth = Math.max(0, (worldRef.deferSpatialRebuildDepth || 1) - 1);
worldRef.deferSpatialRebuildMode = prevMode;
@ -74,7 +74,6 @@
}
const phases = Object.freeze({ updateItemsAndAnts });
const helpersApi = Object.freeze({ updateRandomGrassGrowth });
global.TarinaiItemAntSimulationSystem = Object.freeze({ phases, helpers: helpersApi });
global.TarinaiItemAntSimulationSystem = Object.freeze({ phases });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -3,47 +3,19 @@
// Layer: simulation/helpers
// Shared, UI-free helpers used by the concrete simulation systems.
(function (global) {
const policy = () => global.TarinaiItemUpdatePolicy || {};
const policy = global.TarinaiItemUpdatePolicy;
function forScheduledItems(worldRef, visitor) {
if (policy().forScheduledItems) return policy().forScheduledItems(worldRef, visitor);
if (!worldRef || typeof visitor !== "function") return 0;
let count = 0;
for (const it of worldRef.items || []) {
if (!it || it.dead || it.type === "grass") continue;
visitor(it);
count += 1;
}
return count;
return policy.forScheduledItems(worldRef, visitor);
}
function itemUpdateInterval(item) {
const interval = policy().itemUpdateInterval?.(item);
const interval = policy.itemUpdateInterval(item);
return Number.isFinite(interval) ? interval : Infinity;
}
function updateItemsScheduled(worldRef, dt) {
if (global.TarinaiItemRuntime?.updateScheduled) return global.TarinaiItemRuntime.updateScheduled(worldRef, dt);
if (global.TarinaiItemUpdateScheduler?.run) return global.TarinaiItemUpdateScheduler.run(worldRef, dt);
let ran = 0;
forScheduledItems(worldRef, (it) => {
const interval = itemUpdateInterval(it);
if (!Number.isFinite(interval)) return;
if (interval <= 0) {
if (global.TarinaiItemRuntime?.updateOne) global.TarinaiItemRuntime.updateOne(it, dt, worldRef);
else it.update?.(dt, worldRef);
ran += 1;
return;
}
it._scheduledUpdateDt = Math.min(10, (it._scheduledUpdateDt || 0) + dt);
if (it._scheduledUpdateDt < interval) return;
const scheduledDt = it._scheduledUpdateDt;
it._scheduledUpdateDt = 0;
if (global.TarinaiItemRuntime?.updateOne) global.TarinaiItemRuntime.updateOne(it, scheduledDt, worldRef);
else it.update?.(scheduledDt, worldRef);
ran += 1;
});
return ran;
return global.TarinaiItemRuntime.updateScheduled(worldRef, dt);
}
function markSpatialDirtyIfMoved(worldRef, list, reason = "moved", threshold = 0.35) {
@ -77,7 +49,7 @@
if (!worldRef) return false;
const thresholdSq = threshold * threshold;
let moved = false;
const tracked = global.TarinaiItemRuntime?.trackedDynamicItems?.(worldRef) || global.TarinaiItemUpdateScheduler?.trackedDynamicItems?.(worldRef);
const tracked = global.TarinaiItemRuntime.trackedDynamicItems(worldRef);
const source = tracked && tracked.length ? tracked : null;
const visit = (entity) => {
const x = Number.isFinite(entity.x) ? entity.x : 0;
@ -120,8 +92,6 @@
}
const helpers = Object.freeze({
forScheduledItems,
itemUpdateInterval,
updateItemsScheduled,
markSpatialDirtyIfMoved,
markScheduledItemsMoved,
@ -130,11 +100,6 @@
});
global.TarinaiSimulationRuntime = Object.freeze({
helpers,
constants: Object.freeze({
get REALTIME_ITEM_TYPES() { return policy().REALTIME_ITEM_TYPES || new Set(); },
get PIN_ITEM_TYPES() { return policy().PIN_ITEM_TYPES || new Set(); },
get SCHEDULED_ITEM_TYPES() { return policy().SCHEDULED_ITEM_TYPES || Object.freeze([]); },
}),
helpers
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -27,24 +27,6 @@
function finalizeDiagnostics(worldRef) { return phaseFrom("TarinaiEnvironmentSimulationSystem", "finalizeDiagnostics")(worldRef); }
// finalizeDiagnostics publishes worldRef.lastRuntimeDiagnostics.
function update(worldRef, dt) {
const frameDt = normalizeDelta(worldRef, dt);
if (frameDt === null) return;
updateClock(worldRef, frameDt);
updateFrameSignals(worldRef, frameDt);
updateEnvironment(worldRef, frameDt);
prepareSpatialFrame(worldRef);
const mobile = updateItemsAndAnts(worldRef, frameDt);
updateEffects(worldRef, frameDt);
updateCreatures(worldRef, frameDt);
runMaintenance(worldRef, frameDt, mobile.itemCountBefore);
emitPhaseChange(worldRef);
spawnAmbientTarinai(worldRef, frameDt);
spawnAmbientGrass(worldRef, frameDt);
spawnRainWater(worldRef, frameDt);
finalizeDiagnostics(worldRef);
}
const phases = Object.freeze({
normalizeDelta,
updateClock,
@ -62,15 +44,5 @@
finalizeDiagnostics,
});
const helpers = Object.freeze({
...(global.TarinaiSimulationRuntime?.helpers || {}),
...(global.TarinaiItemAntSimulationSystem?.helpers || {}),
...(global.TarinaiCreatureSimulationSystem?.helpers || {}),
});
global.TarinaiSimulationSystems = Object.freeze({
update,
phases,
helpers,
});
global.TarinaiSimulationSystems = Object.freeze({ phases });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -11,6 +11,8 @@
GROUND_IDS,
WEATHER_IDS,
MOOD_IDS,
canonicalMoodId,
moodValue,
STATE_IDS,
POWER_MODE_IDS,
SIZE_MODE_IDS,
@ -21,19 +23,6 @@
enumIndex,
enumValue,
} = SaveSchema;
function spriteIds() {
const list = typeof SPRITES !== "undefined" ? SPRITES : (global.SPRITES || []);
return (list || []).map(s => s?.id).filter(Boolean);
}
function spriteIndex(id = "smile") {
const ids = spriteIds();
const i = ids.indexOf(String(id || ""));
return i >= 0 ? i : Math.max(0, ids.indexOf("smile"));
}
function spriteValue(index = 0) {
const ids = spriteIds();
return ids[Number(index) | 0] || "smile";
}
function q(value, scale = 1, fallback = 0) {
const n = Number(value);
return Number.isFinite(n) ? Math.round(n * scale) : fallback;
@ -78,52 +67,6 @@
}
return out;
}
function copyOwnData(obj, skip = DEFAULT_SKIP) {
const out = {};
if (!obj) return out;
for (const key of Object.keys(obj)) {
if (skip?.has?.(key)) continue;
const cloned = clonePlain(obj[key]);
if (cloned !== undefined) out[key] = cloned;
}
return out;
}
function applyOwnData(target, data, skip = DEFAULT_SKIP) {
if (!target || !data) return target;
for (const [key, value] of Object.entries(data)) {
if (skip?.has?.(key) || key.startsWith("__")) continue;
const c = clonePlain(value);
if (c !== undefined) target[key] = c;
}
return target;
}
function refFor(worldRef, value) {
if (!value || typeof value !== "object") return null;
if ((worldRef?.tarinai || []).includes(value)) return { kind: "tarinai", id: value.id || "", familyKey: value.familyKey || "" };
if ((worldRef?.items || []).includes(value)) return { kind: "item", id: value.id || "", type: value.type || "" };
if (Number.isFinite(value.x) && Number.isFinite(value.y)) return { kind: "point", x: value.x, y: value.y, dead: Boolean(value.dead) };
return null;
}
function resolveRef(worldRef, ref) {
if (!ref || typeof ref !== "object") return null;
if (ref.kind === "tarinai") return (worldRef.tarinai || []).find(t => t && !t.dead && ((ref.id && t.id === ref.id) || (ref.familyKey && t.familyKey === ref.familyKey))) || null;
if (ref.kind === "item") return (worldRef.items || []).find(it => it && !it.dead && it.id === ref.id) || null;
if (ref.kind === "point") return { x: Number(ref.x) || 0, y: Number(ref.y) || 0, dead: Boolean(ref.dead) };
return null;
}
function worldData(worldRef) {
return {
fieldType: worldRef?.fieldType || "garden",
groundType: worldRef?.groundType || "soil",
time: Number(worldRef?.time) || 0,
day: Number(worldRef?.day) || 1,
weather: worldRef?.weather || "sunny",
deadCount: Number(worldRef?.deadCount) || 0,
lastBirthAt: Number(worldRef?.lastBirthAt) || -999,
maxGeneration: Number(worldRef?.maxGeneration) || 1,
colonyMood: worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed",
};
}
function needsToArray(needs = {}) {
return NEED_KEYS.map(k => q(needs[k], 1));
}
@ -185,13 +128,13 @@
return [
enumIndex(FIELD_IDS, worldRef?.fieldType || "garden"),
enumIndex(GROUND_IDS, worldRef?.groundType || "soil"),
enumIndex(MOOD_IDS, worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed"),
enumIndex(MOOD_IDS, canonicalMoodId?.(worldRef?.colonyMood?.id || worldRef?.colonyMood || "relaxed") || "relaxed"),
worldTick10,
enumIndex(WEATHER_IDS, worldRef?.weather || "sunny"),
q(worldRef?.lastBirthAt, 10, -9990),
q(worldRef?.maxGeneration, 1, 1),
q(worldRef?.deadCount, 1, 0),
worldRef?.worldSeed || global.TarinaiSeedFactory?.ensureWorldSeed?.(worldRef) || "",
worldRef?.worldSeed || global.TarinaiSeedFactory.ensureWorldSeed(worldRef) || "",
q(worldRef?.birthSerial, 1, 0),
];
}
@ -201,7 +144,7 @@
const totalTime = u(arr[3], 10, 0);
worldRef.fieldType = enumValue(FIELD_IDS, arr[0], "garden");
worldRef.weather = enumValue(WEATHER_IDS, arr[4], "sunny");
worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory?.createWorldSeed?.() || `w${Date.now().toString(36)}`;
worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory.createWorldSeed() || `w${Date.now().toString(36)}`;
worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0));
worldRef.day = Math.max(1, Math.floor(totalTime / dayLength) + 1);
worldRef.time = totalTime - (worldRef.day - 1) * dayLength;
@ -211,7 +154,7 @@
worldRef.liveIdSerial = 1;
worldRef.lastBirthAt = u(arr[5], 10, -999);
worldRef.maxGeneration = Math.max(1, u(arr[6], 1, 1));
const moodId = enumValue(MOOD_IDS, arr[2], "relaxed");
const moodId = moodValue?.(arr[2], "relaxed") || enumValue(MOOD_IDS, arr[2], "relaxed");
worldRef.colonyMood = worldRef.colonyMoodDefinition?.(moodId) || { id: moodId, label: moodId, effects: { personality: {} } };
worldRef.setGroundType?.(enumValue(GROUND_IDS, arr[1], "soil"), { silent: true, force: true });
}
@ -276,11 +219,11 @@
const needs = arrayToNeeds(row[9]);
const age = u(row[4], 10);
const birthSeed = String(row[1] || `s${index}`);
const profile = global.TarinaiSeedFactory?.birthProfile?.(birthSeed) || {};
const profile = global.TarinaiSeedFactory.birthProfile(birthSeed) || {};
const opts = {
familyKey: birthSeed,
birthSeed,
birthSerial: global.TarinaiSeedFactory?.serialFromBirthSeed?.(birthSeed) || 0,
birthSerial: global.TarinaiSeedFactory.serialFromBirthSeed(birthSeed) || 0,
id: `st${index}`,
liveToken: 1,
currentPersonality: arrayToPersonality(row[10]),
@ -331,11 +274,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 (global.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) {
const packed = global.TarinaiPhysicsBodySystem.compactItemExtra?.(item, tarinaiIndex, itemIndex);
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 (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)];
@ -376,17 +319,17 @@
}
if (type === "grass") {
item.growth = u(extra[0], 100, item.growth || 0); item.health = u(extra[1], 100, item.health || 1); item.fertilityBoost = u(extra[2], 100, 0); item.manualGrass = !!extra[3]; item.placedByPlayer = !!extra[3];
global.TarinaiGrass?.normalize?.(item);
global.TarinaiGrass.normalize(item);
} else if (type === "zunchi") {
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 (global.TarinaiPhysicsBodySystem?.applyCompactExtra?.(item, extra, tarinaiList, itemList)) {
} else if (global.TarinaiPhysicsBodySystem.applyCompactExtra(item, extra, tarinaiList, itemList)) {
return;
} else if (global.TarinaiPhysicsBodySystem?.isPhysicsType?.(type)) {
} 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");
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);
@ -451,7 +394,6 @@
worldRef.selected = null;
worldRef.events = global.TarinaiEvents || null;
worldRef.liveTarinai = new Map();
worldRef.liveIdFree = [];
applyCompactWorld(worldRef, snapshot.w || []);
const tarRows = Array.isArray(snapshot.t) ? snapshot.t : [];
@ -546,12 +488,6 @@
global.TarinaiSnapshot = {
version: SNAPSHOT_VERSION,
clonePlain,
copyOwnData,
applyOwnData,
refFor,
resolveRef,
worldData,
createSnapshot,
restoreSnapshot,
};

View file

@ -7,7 +7,6 @@
categoryGain: { voice: 3.2, notify: 18.0, ops: 16.0 },
idGain: {
sfx_notify: 2.60,
sfx_log_push: 2.60,
sfx_ui_click: 2.35,
sfx_ui_fold: 2.20,
sfx_place: 2.10,
@ -35,7 +34,6 @@
sfx_zunchi: 0.14,
sfx_ant_die: 0.14,
sfx_notify: 0.10,
sfx_log_push: 0.10,
voice_sunbath: 0.95,
sfx_shoot: 0.045,
sfx_bolt_action: 0.10,
@ -71,9 +69,9 @@
sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice",
sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice",
sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice",
sfx_notify: "notify", sfx_phase: "notify", sfx_log_push: "notify",
sfx_notify: "notify", sfx_phase: "notify",
sfx_ui_click: "ops", sfx_ui_fold: "ops", sfx_place: "ops", sfx_delete: "ops", sfx_poke: "ops",
sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_water_clean: "ops", sfx_drink: "ops",
sfx_grab: "ops", sfx_drop: "ops", sfx_water_hose: "ops", sfx_drink: "ops",
sfx_ball_hit: "ops", sfx_stone_impact: "ops", sfx_firecracker_fuse: "ops", sfx_firecracker_explode: "ops",
sfx_shoot: "ops", sfx_bolt_action: "ops",
sfx_genkotsu_fall: "ops", sfx_genkotsu_impact: "ops", sfx_ground_rumble: "ops", sfx_grass: "ops",

View file

@ -119,7 +119,6 @@
if (nestMatch) {
t.insideNestBoxId = null;
t.nestBoxSleepTimer = 0;
}
if (sleepTargetMatch && t.sleepSession) {
t.sleepSession.targetId = null;
@ -172,15 +171,9 @@
}
const api = Object.freeze({
markWorldAfterStructureChange,
markLooseItemGone,
releaseStructureUsers,
deleteStructure,
consumeGrassBedOnWake,
deleteItem,
destroyItem: deleteItem,
triggerOwnedStructureDestroyedPanic,
});
global.TarinaiStructureLifecycle = api;
global.TarinaiItemDestruction = api;
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -98,7 +98,7 @@
this.dead = true;
this.amount = 0;
global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, breaker);
global.TarinaiStructureLifecycle?.triggerOwnedStructureDestroyedPanic?.(worldRef, this, breaker, "structure-damaged-destroyed");
global.TarinaiStructureLifecycle.triggerOwnedStructureDestroyedPanic(worldRef, this, breaker, "structure-damaged-destroyed");
return true;
}
@ -145,10 +145,10 @@
} else if (this.type === "plushie") {
const spriteId = "plushie_bear";
this.plushieSpriteId = spriteId;
const img = typeof getRenderableImage === "function" ? getRenderableImage(spriteId, spriteId) : (global.images?.get?.(spriteId) || null);
const img = getRenderableImage(spriteId, spriteId);
ctx.rotate(Math.sin(time * 2 + this.seed) * 0.05);
if (img) {
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(spriteId) || getImageMetrics("smile") : null;
const metrics = getImageMetrics(spriteId) || getImageMetrics("smile");
const w = this.r * 3.35;
const h = w * (metrics?.ratio || 1.05);
ctx.drawImage(img, -w * 0.50, -h * 0.62, w, h);
@ -220,7 +220,6 @@
onUse(user, structure, worldRef, options = {}) {
if (user?.id && !structure.ownerId) {
structure.ownerId = user.id;
structure.ownerName = user.name || "";
structure.attachment = Math.max(structure.attachment || 0, 24);
structure.unownedDecayTimer = 0;
structure.dead = false;
@ -244,7 +243,6 @@
structure.unownedDecayTimer = 0;
structure.singleUsePending = true;
structure.singleUseConsumedById = user?.id || "";
structure.singleUseConsumedAt = worldRef?.time || 0;
structure.singleUseWakeReason = "sleep";
structure.amount = Math.max(0.001, structure.hp || structure.amount || 1);
worldRef?.markItemBucketsDirty?.("grass-bed-sleep-reserved");

View file

@ -5,7 +5,7 @@
// with the phase facade kept as a stable named entry point.
(function (global) {
function phase(name) {
return global.TarinaiSimulationSystems?.phases?.[name] || global.TarinaiWorldUpdatePhases?.phases?.[name] || null;
return global.TarinaiSimulationSystems.phases?.[name] || null;
}
const SYSTEM_ORDER = Object.freeze([
@ -26,13 +26,13 @@
function update(worldRef, dt) {
const normalize = phase("normalizeDelta");
if (!normalize) return global.TarinaiWorldUpdatePhases?.update?.(worldRef, dt);
if (!normalize) throw new Error("Tarinai simulation normalize phase is not available");
const frameDt = normalize(worldRef, dt);
if (frameDt === null) return;
const ctx = { dt: frameDt, mobile: null };
const profiler = global.TarinaiPerf;
for (const system of SYSTEM_ORDER) {
const end = profiler?.begin?.(`update.phase.${system.id}`) || null;
const end = profiler.begin(`update.phase.${system.id}`) || null;
try {
system.run(worldRef, ctx);
} finally {
@ -41,9 +41,5 @@
}
}
global.TarinaiSystemOrder = Object.freeze({
SYSTEM_ORDER,
names: Object.freeze(SYSTEM_ORDER.map(system => system.id)),
update,
});
global.TarinaiSystemOrder = Object.freeze({ update });
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -16,18 +16,17 @@ function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
}
class Tarinai { constructor(world, opts = {}) {
if (world && !opts.birthSeed && !opts.familyKey && !opts.id && globalThis.TarinaiSeedFactory?.prepareAddOptions) {
if (world && !opts.birthSeed && !opts.familyKey && !opts.id && globalThis.TarinaiSeedFactory.prepareAddOptions) {
opts = globalThis.TarinaiSeedFactory.prepareAddOptions(world, opts) || opts;
}
this.world = world;
this.birthSeed = opts.birthSeed || opts.familyKey || opts.id || (globalThis.TarinaiSeedFactory?.childSeed?.(world?.worldSeed || "", Number(world?.birthSerial || 0) + 1, []) || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`));
this.birthSerial = Number.isFinite(opts.birthSerial) ? Number(opts.birthSerial) : (globalThis.TarinaiSeedFactory?.serialFromBirthSeed?.(this.birthSeed) || 0);
const birthProfile = globalThis.TarinaiSeedFactory?.birthProfile?.(this.birthSeed, opts) || {};
this.birthSeed = opts.birthSeed || opts.familyKey || opts.id || (globalThis.TarinaiSeedFactory.childSeed(world?.worldSeed || "", Number(world?.birthSerial || 0) + 1, []) || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`));
this.birthSerial = Number.isFinite(opts.birthSerial) ? Number(opts.birthSerial) : (globalThis.TarinaiSeedFactory.serialFromBirthSeed(this.birthSeed) || 0);
const birthProfile = globalThis.TarinaiSeedFactory.birthProfile(this.birthSeed, opts) || {};
this.familyKey = opts.familyKey || this.birthSeed;
this.id = opts.id || "";
this.liveToken = opts.liveToken || 0;
this.name = opts.name || birthProfile.name || makeName(world);
this.personality = "";
this.type = opts.type || birthProfile.type || baseSpriteId();
const typeMeta = SPRITES.find(s => s.id === this.type);
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = birthProfile.type || baseSpriteId();
@ -73,8 +72,6 @@ class Tarinai { constructor(world, opts = {}) {
this.needRaw = opts.needRaw && typeof opts.needRaw === "object" ? { ...defaultNeeds, ...opts.needRaw } : { ...this.needs };
this.needRawBase = opts.needRawBase && typeof opts.needRawBase === "object" ? { ...defaultNeeds, ...opts.needRawBase } : { ...this.needRaw };
this.needSatisfaction = opts.needSatisfaction && typeof opts.needSatisfaction === "object" ? { ...defaultNeeds, ...opts.needSatisfaction } : { ...defaultNeeds };
this.lastSatisfiedAt = opts.lastSatisfiedAt && typeof opts.lastSatisfiedAt === "object" ? { ...opts.lastSatisfiedAt } : {};
this.lastSatisfactionSource = opts.lastSatisfactionSource || "";
this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needRaw || this.needs) : 0;
this.previousNeeds = opts.previousNeeds && typeof opts.previousNeeds === "object" ? { ...defaultNeeds, ...opts.previousNeeds } : { ...this.needs };
this.need = "";
@ -92,13 +89,11 @@ class Tarinai { constructor(world, opts = {}) {
this.stressBarTimer = opts.stressBarTimer ?? 0;
this.affection = opts.affection ?? rand(0, 30);
this.dead = !!opts.dead;
this.lifeStatus = opts.lifeStatus || (this.dead ? "dead" : "alive");
this.deathReason = opts.deathReason || "";
this.lastDamageCause = opts.lastDamageCause || "";
this.lastDamageAmount = opts.lastDamageAmount ?? 0;
this.lastDamageAt = opts.lastDamageAt ?? -999;
this.lastMajorDamageCause = opts.lastMajorDamageCause || "";
this.lastMajorDamageAmount = opts.lastMajorDamageAmount ?? 0;
this.lastMajorDamageAt = opts.lastMajorDamageAt ?? -999;
this.thought = "";
this.target = null;
@ -116,16 +111,12 @@ class Tarinai { constructor(world, opts = {}) {
this.lastLog = 0;
this.sleeping = false;
this.awakeLockTimer = opts.awakeLockTimer ?? 0;
this.wasSleepingLastTick = false;
this.blink = rand(0, 10);
this.lastSocial = 0;
this.goodMode = opts.goodMode ?? birthProfile.goodMode ?? stableChoice(this.birthSeed || this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";
this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1);
this.visualFacing = opts.visualFacing ?? this.facing;
this.facingLockUntil = opts.facingLockUntil ?? 0;
this.eatTimer = opts.eatTimer ?? 0;
this.eatCooldown = opts.eatCooldown ?? 0;
this.lastAteAt = Number.isFinite(opts.lastAteAt) ? opts.lastAteAt : (this.world?.time || 0);
this.mealCooldownUntil = Number.isFinite(opts.mealCooldownUntil) ? opts.mealCooldownUntil : 0;
this.foodReactTimer = opts.foodReactTimer ?? 0;
this.surpriseTimer = opts.surpriseTimer ?? 0;
@ -169,7 +160,6 @@ class Tarinai { constructor(world, opts = {}) {
this.favorite = Boolean(opts.favorite);
this.insideNestBoxId = opts.insideNestBoxId || null;
this.nestFade = opts.nestFade ?? (this.insideNestBoxId ? 1 : 0);
this.nestBoxEnteredAt = opts.nestBoxEnteredAt ?? (this.insideNestBoxId ? (world?.time || 0) : -Infinity);
this.nestBoxStayUntil = opts.nestBoxStayUntil ?? (this.insideNestBoxId ? ((world?.time || 0) + 10) : -Infinity);
this.focusPulseTimer = opts.focusPulseTimer ?? 0;
if (this.isZunchiSlave) {
@ -195,7 +185,6 @@ class Tarinai { constructor(world, opts = {}) {
: 0;
this.poopCount = opts.poopCount ?? 0;
this.oshiriByoZunchiStock = opts.oshiriByoZunchiStock ?? 0;
this.stretchTimer = opts.stretchTimer ?? 0;
this.lastMealColor = opts.lastMealColor || "#f7cf67";
this.grassEatTimer = opts.grassEatTimer ?? 0;
this.hurtTimer = opts.hurtTimer ?? 0;
@ -210,12 +199,9 @@ class Tarinai { constructor(world, opts = {}) {
this.blastSpinTimer = opts.blastSpinTimer ?? 0;
this.blastSpinMax = opts.blastSpinMax ?? 0;
this.postBirthPeaceTimer = opts.postBirthPeaceTimer ?? 0;
this.sleepStart = 0.60 + stableUnit(this.familyKey, "sleep-start") * 0.22;
this.sleepEnd = 0.15 + stableUnit(this.familyKey, "sleep-end") * 0.14;
this.sleepFacing = stableUnit(this.familyKey, "sleep-facing") < 0.5 ? -1 : 1;
this.nextBubbleAt = 0;
this.nextSleepBubbleAt = 0;
this.nextFearBubbleAt = 0;
this.nextCursorHeartAt = opts.nextCursorHeartAt ?? 0;
this.cursorPetting = opts.cursorPetting ?? 0;
this.aiTimer = opts.aiTimer ?? stableUnit(this.familyKey, "ai-start") * 0.22;

View file

@ -1,21 +0,0 @@
"use strict";
// Boundary for ordinary need/action planning. The current implementation still
// delegates to the extracted planner body, and callers no longer depend on the
// Tarinai prototype owning that body.
(function (global) {
function updateOne(tarinai, dt = 0) {
if (!tarinai || tarinai.dead) return false;
return Boolean(global.TarinaiNeedPlannerCoreSystem?.updateOne?.(tarinai, dt));
}
function updateWithThis(dt = 0) {
return updateOne(this, dt);
}
global.TarinaiActionPlannerSystem = Object.freeze({
updateOne,
updateWithThis,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -116,25 +116,12 @@
return specs;
}
function registerTarinaiActionSpec(definition = {}) {
const spec = normalizeTarinaiActionSpec(definition);
registry.set(spec.id, spec);
return spec;
}
function getTarinaiActionSpec(id = "") {
return registry.get(String(id || "")) || null;
}
function listTarinaiActionSpecs() {
return Array.from(registry.values());
}
function selectTarinaiActionTarget(action, tarinai, world, ctx = {}) {
const spec = resolveActionSpec(action);
if (!spec) return null;
return hasOwnFn(spec, "selectTarget") ? spec.selectTarget(tarinai, world, ctx) : (ctx.target ?? tarinai?.target ?? null);
}
function canStartTarinaiAction(action, tarinai, world, ctx = {}) {
const spec = resolveActionSpec(action);
@ -175,19 +162,13 @@
}
Object.assign(global, {
normalizeTarinaiActionSpec,
createTarinaiActionSpec: normalizeTarinaiActionSpec,
registerTarinaiActionSpec,
registerTarinaiActionSpecs,
getTarinaiActionSpec,
listTarinaiActionSpecs,
selectTarinaiActionTarget,
canStartTarinaiAction,
startTarinaiAction,
tickTarinaiAction,
finishTarinaiAction,
failTarinaiAction,
textTarinaiAction,
TarinaiActionSpecRegistry: registry,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -26,8 +26,7 @@
const reason = String(opts.reason ?? this.thought ?? "");
if (nextState === "idle") {
if (typeof clearBehavior === "function") clearBehavior(this, reason || "\u5f85\u3063\u3066\u3044\u308b");
else if (typeof clearTarinaiBehavior === "function") clearTarinaiBehavior(this, { clearTarget: !!opts.clearTarget });
clearBehavior(this, reason || "\u5f85\u3063\u3066\u3044\u308b");
return true;
}
@ -52,8 +51,7 @@
const actionId = String(opts.actionId || opts.behaviorId || stateActionIds[nextState] || nextState);
const target = opts.clearTarget ? null : ("target" in opts ? opts.target : this.target);
const phase = opts.phase || (nextState === "sleep" || nextState === "eat" || nextState === "fight" || nextState === "intimidate" || nextState === "birth_ritual" || nextState === "sunbath" ? "acting" : "approaching");
if (typeof setBehaviorText === "function") {
setBehaviorText(this, {
setBehaviorText(this, {
need: opts.need || null,
subNeed: opts.subNeed || "",
actionId,
@ -67,19 +65,6 @@
sourceReasonText: opts.sourceReasonText || opts.forcedReasonText || "",
causeText: opts.causeText || "",
});
} else if (typeof patchTarinaiBehavior === "function") {
patchTarinaiBehavior(this, {
actionId,
phase,
target,
source: opts.source || "state",
reason,
label: opts.actionLabel || reason || nextState,
text: reason || opts.actionLabel || nextState,
need: opts.need || "fulfill",
subNeed: opts.subNeed || "",
});
}
return true;
},
@ -90,17 +75,17 @@
reason: text,
sleeping: false,
});
if (typeof setBehaviorText === "function") setBehaviorText(this, { need: "food", actionId: "eat_food", actionLabel: "\u98df\u3079\u3066\u3044\u308b", reasonText: text, target: item || null, phase: "perform", source: "behavior" });
setBehaviorText(this, { need: "food", actionId: "eat_food", actionLabel: "\u98df\u3079\u3066\u3044\u308b", reasonText: text, target: item || null, phase: "perform", source: "behavior" });
return ok;
},
currentLodgedPin() {
return typeof lodgedPinFor === "function" ? lodgedPinFor(this, this.world) : null;
return lodgedPinFor(this, this.world);
},
currentLodgedPinBehavior() {
const pin = this.currentLodgedPin ? this.currentLodgedPin() : null;
return typeof pinBehaviorFor === "function" ? pinBehaviorFor(pin?.type) : null;
return pinBehaviorFor(pin?.type);
},
hasLodgedPinEffect(effectKey = "") {
@ -133,7 +118,7 @@
target.singleUseWakeReason = "sleep";
}
const text = reason || (target?.type === "nest_box" ? "\u5de3\u7bb1\u306e\u4e2d\u3067\u7720\u3063\u3066\u3044\u308b" : "\u7720\u3063\u3066\u3044\u308b");
if (typeof setBehaviorText === "function") setBehaviorText(this, { need: "sleep", actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: "\u7720\u3063\u3066\u3044\u308b", reasonText: text, target, phase: "perform", source: "behavior" });
setBehaviorText(this, { need: "sleep", actionId: target ? "sleep_in_bed" : "sleep_anywhere", actionLabel: "\u7720\u3063\u3066\u3044\u308b", reasonText: text, target, phase: "perform", source: "behavior" });
return this.setActionState("sleep", { target, reason: text, sleeping: true });
},
@ -165,7 +150,7 @@
return false;
}
this.pendingGrassBedWakeId = "";
return window.TarinaiStructureLifecycle?.consumeGrassBedOnWake?.(worldRef, bed, this, reason) || false;
return window.TarinaiStructureLifecycle.consumeGrassBedOnWake(worldRef, bed, this, reason) || false;
},
seekTarget(state, target = null, reason = "") {
@ -173,13 +158,11 @@
},
forceBehavior(actionId = "", opts = {}) {
if (typeof queueForcedTarinaiBehavior !== "function") return false;
return queueForcedTarinaiBehavior(this, actionId, opts);
},
currentBehaviorText() {
if (typeof behaviorText === "function") return behaviorText(this);
return String((typeof getTarinaiBehaviorText === "function" ? getTarinaiBehaviorText(this) : "") || this.thought || "").trim();
return behaviorText(this);
},
enterPanic(opts = {}) {
@ -210,10 +193,8 @@
wake: opts.wake !== false,
sleeping: false,
});
if (typeof setBehaviorText === "function") {
const causeText = this.lastPanicDetail || (String(panicReason).includes("\u3001") ? String(panicReason).split("\u3001")[0].replace(/\u3066$/, "\u305f") : "\u6016\u3044");
setBehaviorText(this, { need: "safety", actionId: "panic_escape", actionLabel: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", reasonText: panicReason, causeText, target, phase: "perform", source: "behavior" });
}
const causeText = this.lastPanicDetail || (String(panicReason).includes("\u3001") ? String(panicReason).split("\u3001")[0].replace(/\u3066$/, "\u305f") : "\u6016\u3044");
setBehaviorText(this, { need: "safety", actionId: "panic_escape", actionLabel: "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", reasonText: panicReason, causeText, target, phase: "perform", source: "behavior" });
this.fearTimer = Math.max(this.fearTimer || 0, 2.4);
}
if (Number.isFinite(opts.surpriseTimer)) this.surpriseTimer = Math.max(this.surpriseTimer || 0, opts.surpriseTimer);

View file

@ -229,8 +229,6 @@
Object.assign(global, {
normalizeTarinaiBehaviorState: normalizeBehaviorState,
normalizeTarinaiBehaviorValue: normalizeBehaviorValue,
getTarinaiBehavior,
currentTarinaiBehavior: getTarinaiBehavior,
getTarinaiBehaviorId,
getTarinaiBehaviorNeed,
@ -245,6 +243,5 @@
clearTarinaiForcedBehavior: function clearTarinaiForcedBehavior(tarinai) { return setTarinaiForcedBehavior(tarinai, null); },
isTarinaiBehaviorForced: function isTarinaiBehaviorForced(tarinai) { return isBehaviorForcedValue(ensureBehavior(tarinai)); },
isTarinaiBehaviorValueForced: isBehaviorForcedValue,
tarinaiBehaviorForcedSource: forcedBehaviorSource,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -1,7 +1,7 @@
"use strict";
function fulfillPrimaryReason(tarinai, action = null) {
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior;
const behavior = currentTarinaiBehavior(tarinai);
const actionId = action?.id || behavior?.actionId || "";
if (actionId === "build_grass_bed") return "\u5bdd\u5e8a\u304c\u307b\u3057\u3044";
if (actionId === "build_plushie") return "\u306c\u3044\u3050\u308b\u307f\u304c\u307b\u3057\u3044";
@ -34,7 +34,7 @@ function needPhraseForReason(need, tarinai = null, action = null) {
}
function actionTextForReason(action = null, tarinai = null) {
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior;
const behavior = currentTarinaiBehavior(tarinai);
const id = action?.id || behavior?.actionId || "";
const target = tarinai?.target || currentBehaviorTarget?.(tarinai) || null;
const name = target?.name || "\u76f8\u624b";
@ -75,7 +75,7 @@ function formatCauseActionText(cause = "", actionText = "\u884c\u52d5\u3057\u306
function causePhraseForReason(chosenNeed, action, tarinai = null, options = {}) {
if (options?.causeText) return String(options.causeText || "").replace(/[\u3002\uff01\uff1f]+$/, "");
const b = (typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior) || {};
const b = (currentTarinaiBehavior(tarinai)) || {};
if (b.causeText) {
const cause = String(b.causeText).replace(/[\u3002\uff01\uff1f]+$/, "");
const forced = (typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(b) : Boolean(b.forcedId || b.forcedRequest)) || (typeof isTarinaiBehaviorForced === "function" ? isTarinaiBehaviorForced(tarinai) : false);
@ -139,7 +139,7 @@ function causeForBehaviorText(tarinai, behavior = {}, action = null, options = {
}
function composeTarinaiBehaviorText(tarinai, behavior = null, options = {}) {
const b = behavior || (typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior) || null;
const b = behavior || (currentTarinaiBehavior(tarinai)) || null;
if (!b) return "";
const stored = String(b.text || b.reason || "").trim();
if (stored && options.regenerate !== true) return stored;
@ -148,7 +148,7 @@ function composeTarinaiBehaviorText(tarinai, behavior = null, options = {}) {
const tiedNeeds = Array.isArray(options.tiedNeeds) && options.tiedNeeds.length ? [...options.tiedNeeds] : needsForBehaviorText(tarinai, b, need);
const causeText = causeForBehaviorText(tarinai, { ...b, need }, action, options);
const generated = action ? buildReasonText(need, tiedNeeds, action, tarinai, tarinai?.world || null, { ...options, causeText }) : "";
const actionLabel = typeof textTarinaiAction === "function" ? textTarinaiAction(action || b.actionId, tarinai, tarinai?.world || null, { ...options, behavior: b, need, tiedNeeds, causeText, phase: options.phase || b.phase || "active" }) : "";
const actionLabel = textTarinaiAction(action || b.actionId, tarinai, tarinai?.world || null, { ...options, behavior: b, need, tiedNeeds, causeText, phase: options.phase || b.phase || "active" });
const rawFallback = String(actionLabel || b.label || b.reason || b.text || "").trim();
const catalog = globalThis.TEXT_CATALOG || (typeof window !== "undefined" ? window.TEXT_CATALOG : null);
const fallback = catalog?.cleanBehaviorText
@ -158,22 +158,21 @@ function composeTarinaiBehaviorText(tarinai, behavior = null, options = {}) {
}
function patchBehaviorTextFromComposer(tarinai, behavior = null, options = {}) {
const b = behavior || (typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior) || null;
const b = behavior || (currentTarinaiBehavior(tarinai)) || null;
if (!b) return "";
const prevReason = String(b.reason || "");
const text = composeTarinaiBehaviorText(tarinai, b, { ...options, regenerate: true });
if (!text) return "";
const tiedNeeds = Array.isArray(options.tiedNeeds) && options.tiedNeeds.length ? [...options.tiedNeeds] : needsForBehaviorText(tarinai, b, b.need || options.need || "fulfill");
if (text !== b.reason || text !== b.text || JSON.stringify(tiedNeeds) !== JSON.stringify(b.tiedNeeds || [])) {
if (typeof patchTarinaiBehavior === "function") patchTarinaiBehavior(tarinai, { reason: text, text: text, tiedNeeds });
else { b.reason = text; b.text = text; b.tiedNeeds = tiedNeeds; }
patchTarinaiBehavior(tarinai, { reason: text, text: text, tiedNeeds });
}
if (text && (!tarinai.thought || tarinai.thought === prevReason || tarinai.thought === b.label)) tarinai.thought = text;
return text;
}
function refreshBehaviorReasonText(tarinai, action = null) {
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(tarinai) : tarinai?.behavior;
const behavior = currentTarinaiBehavior(tarinai);
if (!behavior?.need) return "";
return patchBehaviorTextFromComposer(tarinai, behavior, { action });
}

View file

@ -32,7 +32,7 @@ function stopFailedBuildPlan(t, world, type = "", reason = "") {
}
t.behaviorSwitchCooldownUntil = Math.max(t.behaviorSwitchCooldownUntil || 0, (world?.time || 0) + 1.2);
const text = reason || "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044";
if (typeof clearBehavior === "function") clearBehavior(t, text);
clearBehavior(t, text);
t.goIdle?.(text);
return false;
}
@ -54,7 +54,6 @@ function forceBuiltStructureVisualRefresh(world, structure, reason = "structure-
world.ensureSpatial?.(reason);
if (structure) {
structure.world = world;
structure._builtVisibleAt = Number(world.time || 0) || 0;
}
if (typeof globalThis.render === "function") {
globalThis.render();
@ -146,7 +145,7 @@ function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) {
if (d > maxDist) continue;
// Prefer nearby grass, but lightly spread simultaneous builders across
// separate tufts so a crowd does not all reserve one valid material.
const jitter = typeof stableUnit === "function" ? stableUnit(tarinai.id || tarinai.familyKey || "", `build-material:${item.id || ""}`) : Math.random();
const jitter = stableUnit(tarinai.id || tarinai.familyKey || "", `build-material:${item.id || ""}`);
const score = d + Math.max(0, reserved) * 42 + jitter * 12;
if (score < bestScore) { best = item; bestScore = score; }
}

View file

@ -73,10 +73,10 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
return true;
}
if ((foodType === "sweet" || foodType === "food" || (isDuplicatorFoodSource(target) && foodType === "grass") || (typeof isServingFoodType === "function" && isServingFoodType(foodType)) || (typeof isParamEffectItemType === "function" && isParamEffectItemType(foodType))) && t.foodItemHasServingLeft?.(target)) {
if ((foodType === "sweet" || foodType === "food" || (isDuplicatorFoodSource(target) && foodType === "grass") || (isServingFoodType(foodType)) || (isParamEffectItemType(foodType))) && t.foodItemHasServingLeft?.(target)) {
const eating = t.consumeFoodServing?.(target);
if (!(eating > 0)) return false;
const isParam = typeof isParamEffectItemType === "function" && isParamEffectItemType(foodType);
const isParam = isParamEffectItemType(foodType);
const isSpecial = foodType === "love_mochi" || foodType === "fight_mochi" || foodType === "sleep_drug" || isParam;
const color = ({ sweet: "#78b957", love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff", protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b", mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f" })[foodType] || "#f1dfb7";
finishBehaviorConsumption(t, target, foodType, eating, effectiveDt, `${labelFor(foodType)}\u3092\u98df\u3079\u3066\u3044\u308b`, color);
@ -100,7 +100,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
}
if (target.type === "grass" && (target.amount || 0) > 0) {
const bite = window.TarinaiGrass?.regress?.(target, 1) ?? 0;
const bite = window.TarinaiGrass.regress(target, 1) ?? 0;
if (!(bite > 0)) return false;
target.eatenAmount = (target.eatenAmount || 0) + bite;
world.markTerrainDirty?.("grass-eaten-stage");

View file

@ -73,14 +73,8 @@ if (d > contactReach) return false;
return true;
}
function updateOne(tarinai, ctx, dt = 0) {
if (!tarinai || tarinai.dead || !ctx) return false;
for (const it of ctx.foodCandidates || []) updateCandidate(tarinai, ctx, it, dt);
return true;
}
global.TarinaiContactItemSystem = Object.freeze({
updateCandidate,
updateOne,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -34,7 +34,6 @@
}
global.TarinaiCursorCareSystem = Object.freeze({
updateBody,
updateOne,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -259,7 +259,6 @@
this._nestBoxSeekStartedAt = NaN;
this._nestBoxSeekTargetId = "";
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(box) : { x: box.x, y: box.y };
this.nestBoxEnteredAt = now;
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, now + 14);
this.nestBoxSlotIndex = Math.min(occupants.length, Math.max(0, capacity - 1));
this._nestBoxEntryStartX = Number.isFinite(this.x) ? this.x : entry.x;
@ -394,7 +393,6 @@
this.sleeping = false;
this.sleepSession = null;
if (this.state === "sleep" || this.state === "seek_bed") this.goIdle?.("巣箱から出た");
this.nestBoxEnteredAt = -Infinity;
this.nestBoxStayUntil = -Infinity;
this.nestBoxSlotIndex = -1;
this._nestBoxEntrySmoothUntil = -Infinity;
@ -451,7 +449,7 @@
this._nestBoxSeekStartedAt = this.world?.time || 0;
this._nestBoxSeekTargetId = targetBox.id;
}
if (!this.insideNestBoxId && global.TarinaiNestSleepSystem?.sleepPlaceAvailableFor && !global.TarinaiNestSleepSystem.sleepPlaceAvailableFor(this.world, this, targetBox)) {
if (!this.insideNestBoxId && global.TarinaiNestSleepSystem.sleepPlaceAvailableFor && !global.TarinaiNestSleepSystem.sleepPlaceAvailableFor(this.world, this, targetBox)) {
return this.abortNestBoxEntry?.(targetBox, "巣箱がいっぱいで入れない") || null;
}

Some files were not shown because too many files have changed in this diff Show more