From 4d3fc2cd47ddef68d2319fa8928d21568451f432 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Wed, 1 Jul 2026 14:41:05 +0900 Subject: [PATCH] hm --- AI_MAP.md | 2 +- BUILD_NOTE.txt | 13 +- FILES.json | 6 +- README.md | 2 +- app_manifest.json | 14 +- css/base.css | 50 +- css/components.css | 4 +- css/layout.css | 30 +- css/mobile.css | 424 +++-------------- css/panel.css | 112 +---- index.html | 338 +++++++------- js/ants.js | 37 +- js/burn_motion_util.js | 20 + js/collision_footprint_system.js | 139 +----- js/constraint_system.js | 79 ++-- js/family_graph.js | 80 +++- js/fire_runtime_system.js | 469 +++++++++++++++++++ js/geometry_helpers.js | 201 ++++++++ js/ground_types.js | 4 +- js/health.js | 1 - js/item_dynamic_ball_system.js | 102 +--- js/item_dynamic_tool_system.js | 616 ++----------------------- js/item_dynamic_zunchi_system.js | 30 +- js/item_lifecycle_pipeline.js | 19 +- js/item_render_helpers.js | 87 ++++ js/item_render_runtime.js | 150 +----- js/item_tool_definitions.js | 47 +- js/item_tool_metadata.js | 56 +++ js/item_type_initializers.js | 13 +- js/item_update_policy.js | 8 +- js/item_update_scheduler.js | 14 +- js/main.js | 2 +- js/mechanical_system.js | 138 ++---- js/perf_profiler.js | 2 +- js/physics_helpers.js | 60 ++- js/physics_shape_editor_system.js | 249 +++++++++- js/pin_attachment_system.js | 14 + js/render.js | 55 ++- js/robot_cleaner_system.js | 344 ++++++++++++++ js/save_codec.js | 5 +- js/sim_core.js | 19 +- js/simulation_maintenance_system.js | 8 + js/simulation_runtime_helpers.js | 66 +-- js/snapshot_system.js | 3 +- js/structure_lifecycle.js | 4 - js/structures.js | 3 - js/tarinai_action_state.js | 1 - js/tarinai_behavior_state.js | 1 + js/tarinai_behavior_text.js | 5 +- js/tarinai_building_behavior.js | 4 +- js/tarinai_contact_item_system.js | 11 +- js/tarinai_disease_nest.js | 3 - js/tarinai_food_interaction_system.js | 6 +- js/tarinai_food_prototype_mixin.js | 55 +++ js/tarinai_forced_behavior.js | 4 +- js/tarinai_item_effects.js | 1 - js/tarinai_item_interaction_context.js | 14 + js/tarinai_item_targeting.js | 29 +- js/tarinai_needs_items.js | 52 +-- js/tarinai_nest_sleep_system.js | 38 +- js/tarinai_render.js | 17 + js/tarinai_social_move_life.js | 18 +- js/tarinai_update_pipeline.js | 18 +- js/tarinai_update_policy.js | 4 +- js/tarinai_update_step_movement.js | 16 +- js/ui_family_async.js | 17 +- js/ui_family_data.js | 21 +- js/ui_family_paths.js | 64 ++- js/ui_input_shared.js | 7 - js/ui_layout_dialogs.js | 5 +- js/ui_tools.js | 13 +- js/update_step_pipeline_runner.js | 29 ++ js/version.js | 4 +- js/world.js | 56 +-- js/world_combat_effects.js | 2 - js/world_environment.js | 514 +-------------------- js/world_family_social.js | 382 +-------------- js/world_grass_placement_system.js | 65 +++ js/world_pathfinding_system.js | 184 ++++++++ js/world_placement_log.js | 494 +++++--------------- js/world_reset_presets.js | 375 +++++++++++++++ js/world_temperature_system.js | 238 ++++++++++ scripts/regression_check.py | 9 +- service-worker.js | 4 +- 84 files changed, 3434 insertions(+), 3485 deletions(-) create mode 100644 js/burn_motion_util.js create mode 100644 js/fire_runtime_system.js create mode 100644 js/geometry_helpers.js create mode 100644 js/item_render_helpers.js create mode 100644 js/item_tool_metadata.js create mode 100644 js/robot_cleaner_system.js create mode 100644 js/tarinai_food_prototype_mixin.js create mode 100644 js/update_step_pipeline_runner.js create mode 100644 js/world_grass_placement_system.js create mode 100644 js/world_pathfinding_system.js create mode 100644 js/world_reset_presets.js create mode 100644 js/world_temperature_system.js diff --git a/AI_MAP.md b/AI_MAP.md index a51e7cc..36b8cf4 100644 --- a/AI_MAP.md +++ b/AI_MAP.md @@ -6,7 +6,7 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th - Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order. - Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader. -- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.92`. +- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.101`. - Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render. - Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`. - Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents. diff --git a/BUILD_NOTE.txt b/BUILD_NOTE.txt index 9122311..72ad9b0 100644 --- a/BUILD_NOTE.txt +++ b/BUILD_NOTE.txt @@ -1,7 +1,6 @@ -tarinai_king_fix_v9 -- Added fire item: spreads to tarinai, ants, and grass; burning causes continuous damage and panic movement. -- Added flame_firecracker: scatters fire when the fuse ends without blast damage/explosion. -- Burning tarinai behavior text and fire-sighting panic text are forced through observation data. -- Fire can be extinguished by water droplets or random extinguish checks; rain and <=10C increase extinguish chance. -- Fixed duplicator stored-water preview color by rendering droplet sprites instead of fallback orange food shape. -- Version bumped to 39.15.98. +tarinai_king_fix_v10 +- Fire tool button hidden; flames spawn only from flame firecracker. +- Burning tarinai and ants draw lightweight flame-shaped overlays. +- Flame firecracker is redder and shows a countdown timer; normal firecracker is darker. +- Standalone flame and fire particles use a simpler flame silhouette. +- Version: 39.15.101 diff --git a/FILES.json b/FILES.json index 8e8fab2..fd9ff73 100644 --- a/FILES.json +++ b/FILES.json @@ -1,6 +1,6 @@ { "purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.", - "version": "39.15.92", + "version": "39.15.101", "entrypoints": { "app": "index.html", "runtime": "js/main.js", @@ -71,7 +71,9 @@ "js/main.js", "js/math.js", "js/perf_profiler.js", - "js/debug_tools.js" + "js/debug_tools.js", + "js/update_step_pipeline_runner.js", + "js/burn_motion_util.js" ], "meaning": "Bootstrap, shared constants/helpers, registries, config, profiling/debug." }, diff --git a/README.md b/README.md index a473d5e..5376277 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small; ## Runtime Shape -- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.92`. +- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.101`. - Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots. - Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases. - UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`. diff --git a/app_manifest.json b/app_manifest.json index 131aa90..120434a 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.15.98", + "version": "39.15.101", "css": [ "css/base.css", "css/layout.css", @@ -14,6 +14,7 @@ "js/disease_registry.js", "js/sound_pack.js", "js/deterministic_helpers.js", + "js/item_tool_metadata.js", "js/item_tool_definitions.js", "js/item_visual_definitions.js", "js/item_food_definitions.js", @@ -23,6 +24,7 @@ "js/ground_types.js", "js/input_mode_manager.js", "js/math.js", + "js/geometry_helpers.js", "js/collision_footprint_system.js", "js/physics_helpers.js", "js/placement_preview_system.js", @@ -44,6 +46,8 @@ "js/item_type_initializers.js", "js/item_lifecycle_support.js", "js/item_update_policy.js", + "js/fire_runtime_system.js", + "js/robot_cleaner_system.js", "js/item_dynamic_tool_system.js", "js/item_dynamic_ball_system.js", "js/item_dynamic_duplicator_system.js", @@ -56,10 +60,13 @@ "js/item_lifecycle_step_decay.js", "js/item_lifecycle_step_dynamic.js", "js/item_lifecycle_step_growth.js", + "js/update_step_pipeline_runner.js", "js/item_lifecycle_pipeline.js", "js/item_runtime.js", "js/structure_lifecycle.js", + "js/item_render_helpers.js", "js/item_render_runtime.js", + "js/burn_motion_util.js", "js/ants.js", "js/health.js", "js/tarinai.js", @@ -79,6 +86,7 @@ "js/tarinai_building_behavior.js", "js/tarinai_action_definitions.js", "js/tarinai_needs_items.js", + "js/tarinai_food_prototype_mixin.js", "js/tarinai_need_planner_system.js", "js/tarinai_item_interaction_context.js", "js/tarinai_food_interaction_system.js", @@ -99,12 +107,16 @@ "js/world.js", "js/world_view.js", "js/family_graph.js", + "js/world_reset_presets.js", "js/world_family_social.js", "js/impact_core_system.js", "js/impact_response_system.js", "js/collision_response_system.js", "js/world_combat_effects.js", "js/world_environment.js", + "js/world_temperature_system.js", + "js/world_pathfinding_system.js", + "js/world_grass_placement_system.js", "js/world_spatial_budget.js", "js/world_ants_system.js", "js/weather_system.js", diff --git a/css/base.css b/css/base.css index 176ffbc..58f4e9a 100644 --- a/css/base.css +++ b/css/base.css @@ -36,23 +36,6 @@ button { color: inherit; } -.loading-screen { - position: fixed; - inset: 0; - z-index: 9999; - display: grid; - place-items: center; - padding: 24px; - background: - radial-gradient(circle at 50% 30%, rgba(164, 225, 104, 0.22), transparent 32%), - linear-gradient(180deg, rgba(239, 248, 255, 0.98), rgba(247, 245, 236, 0.98)); - transition: opacity 180ms ease, visibility 180ms ease; -} -.loading-screen.hidden { - opacity: 0; - visibility: hidden; - pointer-events: none; -} .loading-panel { width: min(420px, 92vw); padding: 24px 22px; @@ -62,18 +45,6 @@ button { box-shadow: 0 18px 48px rgba(64, 48, 32, 0.16); text-align: center; } -.loading-title { - font-weight: 700; - font-size: 18px; - letter-spacing: 0.04em; - margin-bottom: 8px; -} -.loading-text { - color: var(--muted); - font-size: 13px; - line-height: 1.55; - min-height: 1.55em; -} #app { width: min(1480px, 100vw); @@ -120,7 +91,6 @@ h1 { white-space: nowrap; } - .top-actions { display: flex; gap: 6px; @@ -260,7 +230,6 @@ h1 { min-width: 132px; } - .ecology-dialog-panel { width: min(760px, 100%); max-height: calc(100vh - 36px); @@ -453,16 +422,15 @@ h1 { .tool[data-tool="oshibyo"] { --tool-icon: url("../assets/ui/tool_oshibyo.webp"); } /* Generated parameter item tool icons. */ -.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp?v=39.15.92"); } -.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp?v=39.15.92"); } -.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp?v=39.15.92"); } -.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp?v=39.15.92"); } -.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp?v=39.15.92"); } -.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp?v=39.15.92"); } -.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp?v=39.15.92"); } -.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp?v=39.15.92"); } -.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp?v=39.15.92"); } - +.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp"); } +.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp"); } +.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp"); } +.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp"); } +.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp"); } +.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp"); } +.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp"); } +.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp"); } +.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp"); } .signboard-editor { position: fixed; diff --git a/css/components.css b/css/components.css index df2d727..9d4c636 100644 --- a/css/components.css +++ b/css/components.css @@ -762,12 +762,10 @@ background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(231, 243, 255, 0.98)); border-bottom-color: rgba(82,128,190,0.30); } -.tool-category[data-tool-category="operate"] .tool-category-body { - background: rgba(238, 247, 255, 0.98); -} .tool-category[data-tool-category="operate"] .tool-category-body { grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 5px; + background: rgba(238, 247, 255, 0.98); } .tool-category[data-tool-category="operate"] .tool { grid-template-columns: 1fr; diff --git a/css/layout.css b/css/layout.css index ed5e7b7..07707eb 100644 --- a/css/layout.css +++ b/css/layout.css @@ -2,26 +2,15 @@ .tool[data-tool="sleep_drug"] { --tool-icon: url("../assets/ui/tool_sleep_drug.webp"); } .tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.webp"); } .tool[data-tool="ant_nest"] { --tool-icon: url("../assets/ui/tool_ant_nest.webp"); } -.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.webp"); } +.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.webp"); --tool-icon-zoom: 1.36; } .tool[data-tool="delete"] { --tool-icon: url("../assets/ui/tool_delete.webp"); } .tool[data-tool="area_delete"] { --tool-icon: url("../assets/ui/tool_delete.webp"); } .tool[data-tool="new"] { --tool-icon: url("../assets/ui/tool_new.webp"); } .tool[data-tool="shoot"] { --tool-icon: url("../assets/ui/tool_shoot.png"); --tool-icon-zoom: 1.48; } -.tool[data-tool="poke"] { --tool-emoji: "\1F448"; } -.tool[data-tool="pinch"] { --tool-emoji: "\1F90F"; } -.tool[data-tool="water_hose"] { --tool-emoji: "\1F6BF"; } -.tool[data-tool="clear_tool"] { --tool-emoji: "\2715"; } -.tool[data-tool="copy"] { --tool-emoji: "\29c9 "; } -.tool[data-tool="area_delete"] { --tool-emoji: "\d7 "; } -.tool[data-tool="rope"] { --tool-emoji: "\2570 "; } -.tool[data-tool="rod"] { --tool-emoji: "\2501 "; } -.tool[data-tool="reciprocator"] { --tool-emoji: "\21c4 "; } -.tool[data-tool="undo"] { --tool-emoji: "\21b6 "; } -.tool[data-tool="redo"] { --tool-emoji: "\21b7 "; } .tool.tool-emoji-icon { --tool-icon: none; } .tool.tool-emoji-icon::before { - content: var(--tool-emoji); + content: var(--tool-emoji, ""); background: none; display: grid; place-items: center; @@ -55,7 +44,6 @@ transform: translate(-50%, -50%) scale(var(--tool-icon-zoom, 1)) !important; } -.tool[data-tool="ball"] { --tool-icon-zoom: 1.36; } .tool[data-tool="observe"], .tool[data-tool="new"] { --tool-icon-zoom: 1.20; } .tool[data-tool="giant_drug"] { --tool-icon-zoom: 1.58; } @@ -78,16 +66,16 @@ .tool-body { margin-top: 10px; } .tool-palette { display: flex; flex-direction: column; gap: 8px; } .tool-category { - border: 1px solid rgba(84,68,48,0.10); + border: 1px solid var(--tool-cat-line, rgba(84,68,48,0.10)); border-radius: 14px; - background: rgba(255,255,255,0.34); + background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,255,255,0.36)), rgba(255,255,255,0.28)); overflow: hidden; } .tool-category-toggle { width: 100%; border: 0; - border-bottom: 1px solid rgba(84,68,48,0.08); - background: rgba(255,252,244,0.66); + border-bottom: 1px solid var(--tool-cat-line, rgba(84,68,48,0.08)); + background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,252,244,0.66)), var(--tool-cat-b, rgba(255,252,244,0.54))); padding: 7px 10px; text-align: left; cursor: pointer; @@ -114,8 +102,8 @@ .selected-category-toggle { width: 100%; border: 0; - border-bottom: 1px solid rgba(84,68,48,0.08); - background: rgba(255,252,244,0.66); + border-bottom: 1px solid var(--tool-cat-line, rgba(84,68,48,0.08)); + background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,252,244,0.66)), var(--tool-cat-b, rgba(255,252,244,0.54))); padding: 8px 10px; text-align: left; cursor: pointer; @@ -340,8 +328,6 @@ meter { .tool-category[data-tool-category="habitat"] { --tool-cat-a: rgba(255,245,218,0.84); --tool-cat-b: rgba(250,229,186,0.76); --tool-cat-line: rgba(174,125,54,0.28); } .tool-category[data-tool-category="physics"] { --tool-cat-a: rgba(226,247,246,0.84); --tool-cat-b: rgba(199,232,231,0.76); --tool-cat-line: rgba(70,145,148,0.28); } .tool-category[data-tool-category="hazard"] { --tool-cat-a: rgba(255,230,224,0.84); --tool-cat-b: rgba(250,207,197,0.78); --tool-cat-line: rgba(189,88,66,0.30); } -.tool-category { border-color: var(--tool-cat-line, rgba(84,68,48,0.10)); background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,255,255,0.36)), rgba(255,255,255,0.28)); } -.tool-category-toggle { background: linear-gradient(180deg, var(--tool-cat-a, rgba(255,252,244,0.66)), var(--tool-cat-b, rgba(255,252,244,0.54))); border-bottom-color: var(--tool-cat-line, rgba(84,68,48,0.08)); } @media (min-width: 921px) { .mobile-mode-controls { display: none !important; } } diff --git a/css/mobile.css b/css/mobile.css index 63a3603..9fa63f2 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -1,4 +1,4 @@ -/* v15.12.1: top-right sound settings panel */ +/* Sound menu */ .sound-menu { position: relative; display: inline-block; } .sound-menu-btn.active { background: linear-gradient(180deg, #f3ffe9, #e7f8db); border-color: rgba(103,178,44,.46); } .sound-panel { @@ -22,19 +22,8 @@ .sound-panel input:disabled + span { opacity: .55; } .sound-master { font-weight: 700; } .sound-panel-divider { height: 1px; background: rgba(84,68,48,.12); margin: 2px 0; } -@media (max-width: 980px) { .sound-panel { right: auto; left: 0; } } - -/* Collapsed card sizing is shared by the base panel rules; no event-only shrink override. */ - -/* v15.12.4: event log scroll chains to the right menu at top/bottom */ -.log { - overscroll-behavior-y: auto !important; -} - -/* v15.12.4: sound settings live only in the top-right sound menu */ - -/* v15.17.0: mobile layout isolation and canvas touch gestures */ +/* Shared layout and gesture guards */ #app { overflow-x: clip; } .layout { position: relative; z-index: 2; } .panel { position: relative; z-index: 3; } @@ -55,81 +44,21 @@ max-width: 100%; box-sizing: border-box; } -@media (max-width: 980px) { - .layout { - position: relative; - z-index: 4; - } - .panel { - position: relative; - z-index: 5; - isolation: isolate; - } - .lineage-main-card { - z-index: 1; - width: 100%; - max-width: 100%; - margin-top: 14px; - clear: both; - } - .lineage-main-content { - height: auto; - min-height: 340px; - max-height: 72vh; - overflow: auto; - overscroll-behavior: contain; - -webkit-overflow-scrolling: touch; - touch-action: pan-x pan-y pinch-zoom; - } - .lineage-family-tree { - overflow: auto; - overscroll-behavior: contain; - -webkit-overflow-scrolling: touch; - touch-action: pan-x pan-y pinch-zoom; - } - .lineage-family-tree .lineage-tree-panel { - max-width: none; - } +.log { + overscroll-behavior-y: auto !important; } - -/* Mobile input mode */ -.mobile-mode-controls { +body.touch-mobile-ui .tool-tip-popover { + max-width: min(520px, calc(100vw - 24px)); +} +body.touch-mobile-ui .tool-category:not([data-tool-category="operate"]).collapsed .tool-category-body { display: none; - position: absolute; - left: 10px; - bottom: 10px; - z-index: 24; - gap: 5px; - padding: 6px; - border-radius: 14px; - background: rgba(255, 251, 245, 0.90); - box-shadow: 0 8px 22px rgba(80,60,35,.18); - border: 1px solid rgba(120, 95, 60, .16); - backdrop-filter: blur(8px); } -.mobile-mode-controls button { - border: 0; - border-radius: 10px; - padding: 7px 8px; - font-size: 12px; - font-weight: 800; - color: #65533f; - background: rgba(255,255,255,.78); - box-shadow: inset 0 -1px 0 rgba(0,0,0,.05); -} -.mobile-mode-controls button.active { - color: #3d321f; - background: linear-gradient(180deg, #ffe9a8, #ffd376); - box-shadow: 0 3px 10px rgba(150, 105, 30, .20); -} -body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { display: flex; } -body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; } +/* Mobile input controls */ +.mobile-mode-controls, .mobile-rotate-controls { display: none; position: absolute; - right: 10px; - bottom: 58px; z-index: 24; gap: 6px; padding: 6px; @@ -139,6 +68,17 @@ body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; } border: 1px solid rgba(120, 95, 60, .16); backdrop-filter: blur(8px); } +.mobile-mode-controls { + left: 10px; + bottom: 10px; + gap: 5px; + background: rgba(255, 251, 245, 0.90); +} +.mobile-rotate-controls { + right: 10px; + bottom: 58px; +} +.mobile-mode-controls button, .mobile-rotate-controls button { border: 0; border-radius: 10px; @@ -149,83 +89,22 @@ body:not(.touch-mobile-ui) .mobile-mode-controls { display: none !important; } background: rgba(255,255,255,.82); box-shadow: inset 0 -1px 0 rgba(0,0,0,.05); } -.mobile-rotate-controls button:active { transform: translateY(1px); } -body.touch-mobile-ui .mobile-rotate-controls:not([hidden]) { display: flex; } -body:not(.touch-mobile-ui) .mobile-rotate-controls { display: none !important; } -@media (max-width: 980px) { - body.touch-mobile-ui .mobile-rotate-controls:not([hidden]) { - position: fixed; - left: 50%; - right: auto; - bottom: calc(env(safe-area-inset-bottom, 0px) + 56px); - transform: translateX(-50%); - z-index: 12002; - max-width: calc(100vw - 22px); - } +.mobile-mode-controls button { padding: 7px 8px; background: rgba(255,255,255,.78); } +.mobile-mode-controls button.active { + color: #3d321f; + background: linear-gradient(180deg, #ffe9a8, #ffd376); + box-shadow: 0 3px 10px rgba(150, 105, 30, .20); } - +.mobile-rotate-controls button:active { transform: translateY(1px); } +body.touch-mobile-ui .mobile-mode-controls:not([hidden]), +body.touch-mobile-ui .mobile-rotate-controls:not([hidden]) { display: flex; } +body:not(.touch-mobile-ui) .mobile-mode-controls, +body:not(.touch-mobile-ui) .mobile-rotate-controls { display: none !important; } body.touch-mobile-ui.mobile-mode-camera #gameCanvas { cursor: grab; } body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; } -/* Keep panel themes on narrow layouts. */ -.card.tool-card { --ui-theme-rgb: 0, 114, 178; background: linear-gradient(180deg, rgba(242, 248, 252, 0.94), rgba(232, 243, 250, 0.91)); border-color: rgba(0, 114, 178, 0.24); } -.card.selected-card { --ui-theme-rgb: 204, 121, 167; background: linear-gradient(180deg, rgba(253, 245, 250, 0.94), rgba(249, 235, 244, 0.91)); border-color: rgba(204, 121, 167, 0.24); } -.card.colony-card { --ui-theme-rgb: 0, 158, 115; background: linear-gradient(180deg, rgba(240, 250, 247, 0.94), rgba(229, 245, 239, 0.91)); border-color: rgba(0, 158, 115, 0.24); } -.card.log-card { --ui-theme-rgb: 230, 159, 0; background: linear-gradient(180deg, rgba(255, 249, 236, 0.94), rgba(253, 242, 215, 0.91)); border-color: rgba(230, 159, 0, 0.24); } - - -/* v39.15.35: mobile controls and scroll affordances */ -@media (max-width: 980px) { - body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { - display: flex !important; - } - .panel-quick-tabs { - grid-template-columns: repeat(5, minmax(0, 1fr)); - position: sticky; - top: 0; - } - .panel-quick-tabs button { - padding-left: 3px; - padding-right: 3px; - font-size: 10.5px; - } -} -body.touch-mobile-ui .tool-tip-popover { - max-width: min(520px, calc(100vw - 24px)); -} - -/* v39.15.36: mobile navigation redesign without family input mode */ -@media (max-width: 980px) { - body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { - position: fixed; - left: 50%; - right: auto; - bottom: calc(env(safe-area-inset-bottom, 0px) + 10px); - transform: translateX(-50%); - z-index: 12001; - max-width: calc(100vw - 22px); - } - .panel-quick-tabs { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - .lineage-main-card { - margin: 10px 0 0; - contain: content; - } - .lineage-main-card .lineage-head { - gap: 8px; - } - .lineage-main-card .lineage-actions { - flex-wrap: wrap; - } - body.touch-mobile-ui .tool-category:not([data-tool-category="operate"]).collapsed .tool-category-body { - display: none; - } - body.touch-mobile-ui .log { - max-height: 240px; - } -} +/* Back to canvas affordance */ .back-to-game-btn { position: fixed; right: calc(env(safe-area-inset-right, 0px) + 12px); @@ -242,180 +121,7 @@ body.touch-mobile-ui .tool-tip-popover { backdrop-filter: blur(8px); } .back-to-game-btn[hidden] { display: none !important; } -@media (min-width: 981px) { - .back-to-game-btn { display: none !important; } -} -/* v39.15.38: keep desktop layout close to v39.15.36; only hide the PC quick-scroll buttons that duplicate the canvas/tool card. */ -@media (min-width: 981px) { - .panel-quick-tabs { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .panel-quick-tabs [data-panel-tab="game"] { - display: none !important; - } -} - -/* v39.15.38: mobile navigation/footer room and family-graph reachability. */ -@media (max-width: 980px) { - html, - body { - max-width: 100%; - overflow-x: hidden; - } - #app { - width: 100%; - max-width: 100%; - box-sizing: border-box; - padding-left: max(10px, env(safe-area-inset-left, 0px)); - padding-right: max(10px, env(safe-area-inset-right, 0px)); - padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 92px); - overflow-x: clip; - } - .topbar, - .top-left, - .brand, - .layout, - .stage-wrap, - .panel, - .card, - .lineage-main-card { - min-width: 0; - max-width: 100%; - box-sizing: border-box; - } - .top-actions { - width: 100%; - max-width: 100%; - display: flex; - flex-wrap: nowrap; - justify-content: flex-start; - gap: 5px; - overflow-x: auto; - overflow-y: hidden; - padding: 1px 0 5px; - -webkit-overflow-scrolling: touch; - scrollbar-width: thin; - } - .top-actions > .btn, - .top-actions > .sound-menu, - .top-actions .sound-menu-btn { - flex: 0 0 auto; - white-space: nowrap; - } - .top-actions .btn { - padding: 7px 9px; - font-size: 11px; - } - .room-link { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - } - body.touch-mobile-ui .mobile-mode-controls:not([hidden]) { - position: absolute; - left: 50%; - right: auto; - bottom: 10px; - transform: translateX(-50%); - z-index: 24; - max-width: calc(100% - 18px); - } - body.touch-mobile-ui .panel-quick-tabs { - position: fixed; - left: max(8px, env(safe-area-inset-left, 0px)); - right: max(8px, env(safe-area-inset-right, 0px)); - bottom: calc(env(safe-area-inset-bottom, 0px) + 8px); - top: auto; - z-index: 12002; - width: auto; - max-width: none; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 4px; - margin: 0; - padding: 6px; - border: 1px solid rgba(102, 169, 215, 0.34); - border-radius: 16px; - background: rgba(244, 250, 247, 0.95); - box-shadow: 0 12px 28px rgba(42, 54, 64, 0.18); - } - body.touch-mobile-ui .panel-quick-tabs button { - min-height: 34px; - padding: 5px 2px; - font-size: 10px; - } - body.touch-mobile-ui .selected-card, - body.touch-mobile-ui .tool-card, - body.touch-mobile-ui .colony-card, - body.touch-mobile-ui .log-card, - body.touch-mobile-ui .lineage-main-card { - scroll-margin-top: 10px; - scroll-margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 88px); - } - body.touch-mobile-ui .lineage-main-card { - margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 78px); - } - .back-to-game-btn { - bottom: calc(env(safe-area-inset-bottom, 0px) + 62px); - right: max(10px, env(safe-area-inset-right, 0px)); - } -} - - -/* Desktop rollback guard. Keep the normal PC layout; only remove duplicated quick-scroll buttons. */ -@media (min-width: 981px) { - .topbar { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: nowrap; - gap: 12px; - } - .top-left { - flex: 1 1 auto; - min-width: 0; - max-width: 100%; - } - .brand, - .brand h1, - h1 { - flex: 0 0 auto; - min-width: max-content; - max-width: none !important; - overflow: visible !important; - text-overflow: clip !important; - } - .top-actions { - flex: 0 0 auto; - margin-left: auto; - justify-content: flex-end; - align-items: center; - flex-wrap: nowrap; - gap: 6px; - white-space: nowrap; - } - .top-actions .btn, - .top-actions .sound-menu-btn, - .room-link { - min-height: 38px; - padding: 8px 10px; - font-size: 12px; - line-height: 1.12; - } - .room-link { - max-width: 180px; - overflow: hidden; - text-overflow: ellipsis; - } - .panel-quick-tabs { - grid-template-columns: repeat(5, minmax(0, 1fr)); - } - .panel-quick-tabs [data-panel-tab="game"] { - display: none !important; - } -} - -/* PC / smartphone UI isolation. All override UI placement is scoped by width. */ @media (min-width: 981px) { html, body { @@ -427,18 +133,8 @@ body.touch-mobile-ui .tool-tip-popover { padding: 18px; overflow: visible; } - .topbar { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: nowrap; - gap: 12px; - } - .top-left { - flex: 1 1 auto; - min-width: 0; - max-width: 100%; - } + .topbar { gap: 12px; } + .top-left { max-width: 100%; } .brand, .brand h1, h1 { @@ -448,15 +144,7 @@ body.touch-mobile-ui .tool-tip-popover { overflow: visible !important; text-overflow: clip !important; } - .top-actions { - flex: 0 0 auto; - margin-left: auto; - justify-content: flex-end; - align-items: center; - flex-wrap: nowrap; - gap: 6px; - white-space: nowrap; - } + .top-actions { margin-left: auto; } .top-actions .btn, .top-actions .sound-menu-btn, .room-link { @@ -488,18 +176,12 @@ body.touch-mobile-ui .tool-tip-popover { top: 0; grid-template-columns: repeat(5, minmax(0, 1fr)); } - .panel-quick-tabs [data-panel-tab="game"] { - display: none !important; - } + .panel-quick-tabs [data-panel-tab="game"] { display: none !important; } .lineage-main-card { display: block; visibility: visible; - position: relative; - z-index: 1; width: auto; margin: 16px 0 0; - overflow: hidden; - contain: layout paint; } .lineage-main-content { height: min(78vh, 980px); @@ -508,6 +190,7 @@ body.touch-mobile-ui .tool-tip-popover { overflow: auto; overscroll-behavior: auto; } + .back-to-game-btn { display: none !important; } } @media (max-width: 980px) { @@ -540,16 +223,11 @@ body.touch-mobile-ui .tool-tip-popover { box-sizing: border-box; } .topbar { - display: flex; flex-direction: column; align-items: stretch; gap: 8px; } - .top-left { - display: flex; - align-items: center; - gap: 8px; - } + .top-left { gap: 8px; } .brand h1, h1 { max-width: 100%; @@ -559,8 +237,6 @@ body.touch-mobile-ui .tool-tip-popover { font-size: clamp(20px, 7vw, 30px); } .top-actions { - display: flex; - flex-wrap: nowrap; justify-content: flex-start; gap: 5px; width: 100%; @@ -581,13 +257,12 @@ body.touch-mobile-ui .tool-tip-popover { padding: 7px 9px; font-size: 11px; } + .sound-panel { right: auto; left: 0; } .layout { display: flex; flex-direction: column; align-items: stretch; gap: 10px; - position: relative; - z-index: 2; overflow: visible; } .stage-wrap { @@ -642,6 +317,15 @@ body.touch-mobile-ui .tool-tip-popover { z-index: 24; max-width: calc(100% - 18px); } + body.touch-mobile-ui .mobile-rotate-controls:not([hidden]) { + position: fixed; + left: 50%; + right: auto; + bottom: calc(env(safe-area-inset-bottom, 0px) + 56px); + transform: translateX(-50%); + z-index: 12002; + max-width: calc(100vw - 22px); + } body.touch-mobile-ui.mobile-mode-auto #gameCanvas { touch-action: pan-y pinch-zoom !important; } @@ -653,13 +337,10 @@ body.touch-mobile-ui .tool-tip-popover { max-height: none !important; height: auto !important; overflow: visible !important; - overscroll-behavior-y: auto !important; } .lineage-main-card { display: block !important; visibility: visible !important; - position: relative; - z-index: 1; width: 100%; margin: 10px 0 calc(env(safe-area-inset-bottom, 0px) + 132px); overflow: visible !important; @@ -678,7 +359,6 @@ body.touch-mobile-ui .tool-tip-popover { touch-action: pan-y pinch-zoom; } .lineage-family-tree { - max-width: 100%; overflow-x: auto !important; overflow-y: visible !important; overscroll-behavior-x: contain; @@ -686,9 +366,7 @@ body.touch-mobile-ui .tool-tip-popover { -webkit-overflow-scrolling: touch; touch-action: pan-x pan-y pinch-zoom; } - .lineage-family-tree .lineage-tree-panel { - max-width: none; - } + .lineage-family-tree .lineage-tree-panel { max-width: none; } .selected-card, .tool-card, .colony-card, @@ -698,4 +376,8 @@ body.touch-mobile-ui .tool-tip-popover { scroll-margin-top: 0 !important; scroll-margin-bottom: calc(env(safe-area-inset-bottom, 0px) + 104px); } + .back-to-game-btn { + bottom: calc(env(safe-area-inset-bottom, 0px) + 62px); + right: max(10px, env(safe-area-inset-right, 0px)); + } } diff --git a/css/panel.css b/css/panel.css index e14e3fd..c9818a8 100644 --- a/css/panel.css +++ b/css/panel.css @@ -99,6 +99,7 @@ body::before { box-shadow: 0 3px 8px rgba(64,48,32,0.045); overflow: hidden; isolation: isolate; + overflow: hidden !important; } .family-node.child { border-color: rgba(103, 178, 44, 0.38); @@ -182,40 +183,6 @@ body::before { text-overflow: ellipsis; white-space: nowrap; } -.family-personality-tags { - display: grid !important; - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-auto-rows: minmax(13px, auto); - align-items: stretch; - gap: 2px 4px; - margin-top: 2px; - max-width: 84px; - width: 84px; - max-height: 34px; - overflow: hidden !important; - text-overflow: clip !important; - white-space: normal !important; -} -.family-personality-tags em { - display: flex; - align-items: center; - justify-content: center; - min-width: 0; - max-width: 100%; - padding: 1px 3px; - border-radius: 999px; - background: rgba(103,178,44,0.11); - border: 1px solid rgba(103,178,44,0.20); - color: rgba(64,72,48,0.86); - font-size: 8.2px; - font-style: normal; - line-height: 1.18; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - - .tool[data-tip] { position: relative; } @@ -353,23 +320,6 @@ body::before { border-radius: 9px; background: rgba(255,255,255,0.70); } -.lineage-family-tree .tree-node .family-copy { - max-width: calc(100% - 46px); - width: auto; -} -.lineage-family-tree .tree-node .family-copy strong, -.lineage-family-tree .tree-node .family-copy > span:not(.family-genetic-stats):not(.family-fight-stat):not(.family-personality-tags) { - max-width: 100%; -} -.lineage-family-tree .tree-node .family-personality-tags { - max-width: 84px; - width: 84px; -} -.lineage-family-tree .tree-node .family-personality-tags em { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} @media (max-width: 720px) { .lineage-main-content { height: 70vh; @@ -421,21 +371,22 @@ body::before { } -/* v9: keep 2x2 personality tags inside fixed family-tree nodes. */ -.lineage-family-tree .tree-node .family-personality-tags, +/* Family tree personality tags: consolidated final cascade. */ .family-personality-tags { box-sizing: border-box !important; display: grid !important; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; grid-auto-rows: 13px !important; gap: 2px 3px !important; - width: 86px !important; - max-width: 86px !important; + width: 100% !important; + max-width: 100% !important; height: 28px !important; max-height: 28px !important; + margin-left: 0 !important; + margin-right: 0 !important; + justify-self: stretch !important; overflow: hidden !important; } -.lineage-family-tree .tree-node .family-personality-tags em, .family-personality-tags em { box-sizing: border-box !important; display: block !important; @@ -449,54 +400,23 @@ body::before { white-space: nowrap !important; line-height: 11px !important; font-size: 7.2px !important; + font-style: normal; letter-spacing: -0.08em !important; text-align: center !important; -} - -/* v39.15.85: push family-tree personality tags to the node's right edge. */ -.lineage-family-tree .tree-node .family-personality-tags, -.family-personality-tags { - margin-left: auto !important; - margin-right: 0 !important; - justify-self: end !important; + border-radius: 999px; + background: rgba(103,178,44,0.11); + border: 1px solid rgba(103,178,44,0.20); + color: rgba(64,72,48,0.86); } .lineage-family-tree .tree-node .family-copy { width: 100% !important; max-width: 100% !important; -} -.lineage-family-tree .tree-node .family-copy strong, -.lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) { - max-width: calc(100% - 46px) !important; -} -.lineage-family-tree .tree-node .family-copy > .family-personality-tags { - max-width: 86px !important; - width: 86px !important; -} - -/* v39.15.87: let family personality tags use the full node width. */ -.lineage-family-tree .tree-node .family-copy > .family-personality-tags, -.lineage-family-tree .tree-node .family-personality-tags, -.family-personality-tags { - width: 100% !important; - max-width: 100% !important; - margin-left: 0 !important; - margin-right: 0 !important; - justify-self: stretch !important; - grid-template-columns: repeat(2, minmax(0, 1fr)) !important; -} -.lineage-family-tree .tree-node .family-copy strong, -.lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) { - max-width: 100% !important; -} - - -/* v39.15.92: keep personality tags inside the family-tree node bottom edge. */ -.lineage-family-tree .tree-node { - overflow: hidden !important; -} -.lineage-family-tree .tree-node .family-copy { position: static !important; } +.lineage-family-tree .tree-node .family-copy strong, +.lineage-family-tree .tree-node .family-copy > span:not(.family-personality-tags) { + max-width: 100% !important; +} .lineage-family-tree .tree-node .family-copy > .family-personality-tags { position: absolute !important; left: 7px !important; diff --git a/index.html b/index.html index 3665853..f829e79 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,11 @@ - - - - - + + + + +
@@ -159,20 +159,20 @@

リセット設定

サイズとプリセットを組み合わせて観察環境を生成します。

サイズ
-
+
プリセット
- - - - - - - + + + + + + +
@@ -217,14 +217,14 @@ `); global.TarinaiPhysicsShapeEditorSystem.decorateToolbar(dialog, "rotator"); return dialog; } @@ -300,15 +282,26 @@ const apply = dialog.querySelector("#rotatorApply"); const cancel = dialog.querySelector("#rotatorCancel"); const closeBtn = dialog.querySelector("#rotatorEditorClose"); - const undo = dialog.querySelector("#rotatorUndo"); - const clear = dialog.querySelector("#rotatorClear"); if (!canvas || !ctx) return false; - let mode = "line"; - let segments = cloneSegmentsForRotator(item); - let draft = null; - let drawing = false; - const cx = canvas.width / 2; - const cy = canvas.height / 2; + const editor = global.TarinaiPhysicsShapeEditorSystem; + const controller = editor.createSegmentEditorController({ + dialog, + canvas, + ctx, + prefix: "rotator", + segments: cloneSegmentsForRotator(item), + fallback: [-78, 0, 78, 0], + lineWidth: () => Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)), + eraseRadius: (lw) => Math.max(24, lw * 1.35), + strokeStyle: () => isPoisonBlock ? "rgba(92,172,76,0.92)" : "rgba(127,92,190,0.92)", + drawCenter: () => { + ctx.fillStyle = isPoisonBlock ? "#eefee7" : "#f5edff"; + ctx.strokeStyle = isPoisonBlock ? "#3c8f32" : "#6d4ca0"; + ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + }, + }); + if (!controller) return false; const deg = Math.round((Number(phyScalar(item, "motorSpeed", 0) || 0) || 0) * 180 / Math.PI); const initialThickness = Math.max(4, Math.min(34, Number(phyScalar(item, "thickness", isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12))); if (titleEl) titleEl.textContent = isPoisonBlock ? "\u6bd2\u30d6\u30ed\u30c3\u30af" : "\u56de\u8ee2\u4f53"; @@ -323,162 +316,17 @@ if (thick) thick.value = String(initialThickness); if (thickNum) thickNum.value = String(initialThickness); if (poweredInput) poweredInput.checked = phyScalar(item, "motorOn", true) !== false; - const toLocal = (e) => { - const rect = canvas.getBoundingClientRect(); - const sx = canvas.width / Math.max(1, rect.width); - const sy = canvas.height / Math.max(1, rect.height); - return { x: (e.clientX - rect.left) * sx - cx, y: (e.clientY - rect.top) * sy - cy }; - }; - const cleanSegments = () => { - const out = []; - for (const seg of segments) { - const x1 = Math.max(-420, Math.min(420, Number(seg[0]) || 0)); - const y1 = Math.max(-420, Math.min(420, Number(seg[1]) || 0)); - const x2 = Math.max(-420, Math.min(420, Number(seg[2]) || 0)); - const y2 = Math.max(-420, Math.min(420, Number(seg[3]) || 0)); - if (Math.hypot(x2 - x1, y2 - y1) >= 4) out.push([x1, y1, x2, y2]); - if (out.length >= 96) break; - } - if (!out.length) out.push([-78, 0, 78, 0]); - segments = out; - }; - const draw = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.fillStyle = "#f7f3ea"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.translate(cx, cy); - ctx.strokeStyle = "rgba(100,80,60,0.16)"; - ctx.lineWidth = 1; - for (let x = -cx; x <= cx; x += 40) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); } - for (let y = -cy; y <= cy; y += 40) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); } - ctx.strokeStyle = "rgba(70,60,50,0.38)"; - ctx.lineWidth = 1.5; - ctx.beginPath(); ctx.moveTo(-cx, 0); ctx.lineTo(cx, 0); ctx.moveTo(0, -cy); ctx.lineTo(0, cy); ctx.stroke(); - const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.strokeStyle = isPoisonBlock ? "rgba(92,172,76,0.92)" : "rgba(127,92,190,0.92)"; - ctx.lineWidth = lw; - ctx.beginPath(); - for (const seg of segments) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); } - ctx.stroke(); - ctx.strokeStyle = "rgba(255,255,255,0.58)"; - ctx.lineWidth = Math.max(1.3, lw * 0.20); - ctx.beginPath(); - for (const seg of segments) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); } - ctx.stroke(); - if (draft) { - ctx.setLineDash([7, 5]); - ctx.strokeStyle = "rgba(60,130,210,0.85)"; - ctx.lineWidth = Math.max(2, lw * 0.45); - ctx.beginPath(); ctx.moveTo(draft[0], draft[1]); ctx.lineTo(draft[2], draft[3]); ctx.stroke(); - ctx.setLineDash([]); - } - ctx.fillStyle = isPoisonBlock ? "#eefee7" : "#f5edff"; - ctx.strokeStyle = isPoisonBlock ? "#3c8f32" : "#6d4ca0"; - ctx.lineWidth = 2; - ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); - ctx.restore(); - }; - const setMode = (next) => { - mode = next === "free" || next === "erase" ? next : "line"; - for (const btn of dialog.querySelectorAll("[data-rotator-mode]")) btn.classList.toggle("active", btn.dataset.rotatorMode === mode); - canvas.style.cursor = mode === "erase" ? "cell" : "crosshair"; - }; - const pointSegmentDistance = (p, seg) => global.TarinaiPhysicsShapeEditorSystem.pointSegmentDistance(p, seg) ?? (() => { - if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity; - const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0; - const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0; - const dx = x2 - x1, dy = y2 - y1; - const lenSq = dx * dx + dy * dy; - if (lenSq <= 0.0001) return Math.hypot(p.x - x1, p.y - y1); - const u = Math.max(0, Math.min(1, ((p.x - x1) * dx + (p.y - y1) * dy) / lenSq)); - return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u)); - })(); - const eraseAt = (p) => { - const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)); - const radius = Math.max(24, lw * 1.35); - const before = segments.length; - segments = segments.filter(seg => pointSegmentDistance(p, seg) > radius); - if (segments.length !== before) { draft = null; draw(); return true; } - draw(); - ctx.save(); - ctx.translate(cx, cy); - ctx.globalAlpha = 0.42; - ctx.strokeStyle = "rgba(210,70,60,0.86)"; - ctx.lineWidth = 1.6; - ctx.setLineDash([4, 4]); - ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.stroke(); - ctx.restore(); - return false; - }; - const setTemplate = (name) => { - const shared = global.TarinaiPhysicsShapeEditorSystem.templateSegments(name); - if (shared) segments = shared.map(seg => seg.slice(0, 4)); - cleanSegments(); draw(); - }; - const syncPair = (a, b) => { if (!a || !b) return; a.oninput = () => { b.value = a.value; draw(); }; b.oninput = () => { a.value = b.value; draw(); }; }; - syncPair(speed, speedNum); - syncPair(thick, thickNum); - dialog.querySelectorAll("[data-rotator-mode]").forEach(btn => { btn.onclick = () => setMode(btn.dataset.rotatorMode); }); - dialog.querySelectorAll("[data-rotator-template]").forEach(btn => { btn.onclick = () => setTemplate(btn.dataset.rotatorTemplate); }); - canvas.onpointerdown = (e) => { - canvas.setPointerCapture?.(e.pointerId); - const p = toLocal(e); - drawing = true; - if (mode === "erase") { - draft = null; - eraseAt(p); - return; - } - draft = [p.x, p.y, p.x, p.y]; - if (mode === "free") segments.push([p.x, p.y, p.x, p.y]); - draw(); - }; - canvas.onpointermove = (e) => { - if (!drawing) return; - const p = toLocal(e); - if (mode === "erase") { eraseAt(p); return; } - if (!draft) return; - if (mode === "free") { - const last = segments[segments.length - 1]; - if (!last) return; - const lx = last[2], ly = last[3]; - if (Math.hypot(p.x - lx, p.y - ly) >= 5) { - last[2] = p.x; last[3] = p.y; - segments.push([p.x, p.y, p.x, p.y]); - } - } else { - draft[2] = p.x; draft[3] = p.y; - } - draw(); - }; - canvas.onpointerup = (e) => { - if (!drawing) return; - drawing = false; - const p = toLocal(e); - if (mode === "erase") { draft = null; draw(); return; } - if (!draft) return; - if (mode === "line") { - const seg = [draft[0], draft[1], p.x, p.y]; - if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= 4) segments.push(seg); - } - draft = null; - cleanSegments(); - draw(); - }; - if (undo) undo.onclick = () => { segments.pop(); cleanSegments(); draw(); }; - if (clear) clear.onclick = () => { segments = []; draft = null; draw(); }; + editor.syncInputPair(speed, speedNum, controller.draw); + editor.syncInputPair(thick, thickNum, controller.draw); const close = () => { dialog.classList.add("hidden"); - canvas.style.cursor = ""; - canvas.onpointerdown = canvas.onpointermove = canvas.onpointerup = null; + controller.close(); }; if (apply) apply.onclick = () => { global.TarinaiHistory.capture(worldRef, isPoisonBlock ? "poison-block-edit" : "rotator-edit"); - cleanSegments(); - physicsApi()?.setSegments?.(item, segments.map(seg => seg.slice(0, 4)), isPoisonBlock ? "poison-shape-edit" : "rotator-shape-edit"); + controller.cleanSegments(); + const editedSegments = controller.getSegments(); + physicsApi()?.setSegments?.(item, editedSegments, isPoisonBlock ? "poison-shape-edit" : "rotator-shape-edit"); phySet(item, "thickness", Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || (isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12))), "editor-thickness"); phySet(item, "solid", poisonCollisionInput ? Boolean(poisonCollisionInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid"); if (isPoisonBlock) { @@ -491,7 +339,6 @@ if (!global.TarinaiPhysicsBodySystem.invalidateItem(item, isPoisonBlock ? "poison-block-edited" : "rotator-edited")) global.TarinaiMechanicalSystem.invalidateGeometry(item); const extent = worldRef?.rotatorExtent?.(item); if (Number.isFinite(extent)) item.r = Math.max(32, Math.min(460, extent)); - item.rotatorEditorOpenAt = worldRef?.time || 0; global.TarinaiLinkRuntime.remapLinksForEditedMechanicalItem(worldRef, item, isPoisonBlock ? "poison-block-link-remap" : "rotator-link-remap"); worldRef?.markSpatialDirty?.(isPoisonBlock ? "poison-block-edited" : "rotator-edited"); worldRef.drawListDirty = true; @@ -502,20 +349,14 @@ if (cancel) cancel.onclick = close; if (closeBtn) closeBtn.onclick = close; dialog.onclick = (e) => { if (e.target === dialog) close(); }; - setMode(mode); - draw(); + controller.draw(); dialog.classList.remove("hidden"); return true; } function ensureReciprocatorEditor() { - let dialog = document.getElementById("reciprocatorEditor"); - if (dialog) return dialog; - dialog = document.createElement("div"); - dialog.id = "reciprocatorEditor"; - dialog.className = "rotator-editor hidden"; - dialog.innerHTML = ` + const dialog = ensureEditorDialog("reciprocatorEditor", "rotator-editor hidden", ` `; - document.body.appendChild(dialog); +
`); global.TarinaiPhysicsShapeEditorSystem.decorateToolbar(dialog, "reciprocator"); return dialog; } @@ -570,15 +410,36 @@ const apply = dialog.querySelector("#reciprocatorApply"); const cancel = dialog.querySelector("#reciprocatorCancel"); const closeBtn = dialog.querySelector("#reciprocatorEditorClose"); - const undo = dialog.querySelector("#reciprocatorUndo"); - const clear = dialog.querySelector("#reciprocatorClear"); if (!canvas || !ctx) return false; - let mode = "line"; - let segments = cloneSegmentsForRotator(item); - let draft = null; - let drawing = false; - const cx = canvas.width / 2; - const cy = canvas.height / 2; + const editor = global.TarinaiPhysicsShapeEditorSystem; + const controller = editor.createSegmentEditorController({ + dialog, + canvas, + ctx, + prefix: "reciprocator", + segments: cloneSegmentsForRotator(item), + fallback: [-78, 0, 78, 0], + lineWidth: () => Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)), + eraseRadius: (lw) => Math.max(28, lw * 1.55), + strokeStyle: "rgba(80,150,205,0.92)", + drawBeforeSegments: () => { + const travelNow = Math.max(24, Math.min(520, Number(travelNum?.value || travel?.value || 150) || 150)); + ctx.save(); + ctx.strokeStyle = "rgba(70,120,170,0.42)"; + ctx.lineWidth = 2; + ctx.setLineDash([7, 6]); + ctx.beginPath(); ctx.moveTo(-travelNow * 0.5, 22); ctx.lineTo(travelNow * 0.5, 22); ctx.stroke(); + ctx.setLineDash([]); + ctx.restore(); + }, + drawCenter: () => { + ctx.fillStyle = "#eaf6ff"; + ctx.strokeStyle = "#4d8cba"; + ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + }, + }); + if (!controller) return false; const axisRad = window.TarinaiMechanicalSystem.railAxisAngle(item) ?? phyScalar(item, "railAxis", (itemAngleFor(item))); const deg = Math.round((axisRad * 180 / Math.PI) % 360 + 360) % 360; const speedVal = Math.round(Number(phyScalar(item, "railMotorSpeed", 92)) || 0); @@ -594,167 +455,18 @@ if (thickNum) thickNum.value = String(initialThickness); if (powered) powered.checked = phyScalar(item, "railOn", true) !== false; if (solidInput) solidInput.checked = phyScalar(item, "solid", true) !== false; - const toLocal = (e) => { - const rect = canvas.getBoundingClientRect(); - const sx = canvas.width / Math.max(1, rect.width); - const sy = canvas.height / Math.max(1, rect.height); - return { x: (e.clientX - rect.left) * sx - cx, y: (e.clientY - rect.top) * sy - cy }; - }; - const cleanSegments = () => { - const out = []; - for (const seg of segments) { - const x1 = Math.max(-420, Math.min(420, Number(seg[0]) || 0)); - const y1 = Math.max(-420, Math.min(420, Number(seg[1]) || 0)); - const x2 = Math.max(-420, Math.min(420, Number(seg[2]) || 0)); - const y2 = Math.max(-420, Math.min(420, Number(seg[3]) || 0)); - if (Math.hypot(x2 - x1, y2 - y1) >= 4) out.push([x1, y1, x2, y2]); - if (out.length >= 96) break; - } - if (!out.length) out.push([-78, 0, 78, 0]); - segments = out; - }; - const draw = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); - ctx.fillStyle = "#f7f3ea"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.translate(cx, cy); - ctx.strokeStyle = "rgba(100,80,60,0.16)"; - ctx.lineWidth = 1; - for (let x = -cx; x <= cx; x += 40) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); } - for (let y = -cy; y <= cy; y += 40) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); } - const travelNow = Math.max(24, Math.min(520, Number(travelNum?.value || travel?.value || 150) || 150)); - ctx.save(); - ctx.strokeStyle = "rgba(70,120,170,0.42)"; - ctx.lineWidth = 2; - ctx.setLineDash([7, 6]); - ctx.beginPath(); ctx.moveTo(-travelNow * 0.5, 22); ctx.lineTo(travelNow * 0.5, 22); ctx.stroke(); - ctx.setLineDash([]); - ctx.restore(); - ctx.strokeStyle = "rgba(70,60,50,0.38)"; - ctx.lineWidth = 1.5; - ctx.beginPath(); ctx.moveTo(-cx, 0); ctx.lineTo(cx, 0); ctx.moveTo(0, -cy); ctx.lineTo(0, cy); ctx.stroke(); - const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)); - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.strokeStyle = "rgba(80,150,205,0.92)"; - ctx.lineWidth = lw; - ctx.beginPath(); - for (const seg of segments) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); } - ctx.stroke(); - ctx.strokeStyle = "rgba(255,255,255,0.58)"; - ctx.lineWidth = Math.max(1.3, lw * 0.20); - ctx.beginPath(); - for (const seg of segments) { ctx.moveTo(seg[0], seg[1]); ctx.lineTo(seg[2], seg[3]); } - ctx.stroke(); - if (draft) { - ctx.setLineDash([7, 5]); - ctx.strokeStyle = "rgba(60,130,210,0.85)"; - ctx.lineWidth = Math.max(2, lw * 0.45); - ctx.beginPath(); ctx.moveTo(draft[0], draft[1]); ctx.lineTo(draft[2], draft[3]); ctx.stroke(); - ctx.setLineDash([]); - } - ctx.fillStyle = "#eaf6ff"; - ctx.strokeStyle = "#4d8cba"; - ctx.lineWidth = 2; - ctx.beginPath(); ctx.arc(0, 0, 8, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); - ctx.restore(); - }; - const setMode = (next) => { - mode = next === "free" || next === "erase" ? next : "line"; - for (const btn of dialog.querySelectorAll("[data-reciprocator-mode]")) btn.classList.toggle("active", btn.dataset.reciprocatorMode === mode); - canvas.style.cursor = mode === "erase" ? "cell" : "crosshair"; - }; - const pointSegmentDistance = (p, seg) => global.TarinaiPhysicsShapeEditorSystem.pointSegmentDistance(p, seg) ?? (() => { - if (!p || !Array.isArray(seg) || seg.length < 4) return Infinity; - const x1 = Number(seg[0]) || 0, y1 = Number(seg[1]) || 0; - const x2 = Number(seg[2]) || 0, y2 = Number(seg[3]) || 0; - const dx = x2 - x1, dy = y2 - y1; - const lenSq = dx * dx + dy * dy; - if (lenSq <= 0.0001) return Math.hypot(p.x - x1, p.y - y1); - const u = Math.max(0, Math.min(1, ((p.x - x1) * dx + (p.y - y1) * dy) / lenSq)); - return Math.hypot(p.x - (x1 + dx * u), p.y - (y1 + dy * u)); - })(); - const eraseAt = (p) => { - const lw = Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)); - const radius = Math.max(28, lw * 1.55); - const before = segments.length; - segments = segments.filter(seg => pointSegmentDistance(p, seg) > radius); - if (segments.length !== before) { draft = null; draw(); return true; } - draw(); - ctx.save(); - ctx.translate(cx, cy); - ctx.globalAlpha = 0.42; - ctx.strokeStyle = "rgba(210,70,60,0.86)"; - ctx.lineWidth = 1.6; - ctx.setLineDash([4, 4]); - ctx.beginPath(); ctx.arc(p.x, p.y, radius, 0, Math.PI * 2); ctx.stroke(); - ctx.restore(); - return false; - }; - const setTemplate = (name) => { - const shared = global.TarinaiPhysicsShapeEditorSystem.templateSegments(name); - if (shared) segments = shared.map(seg => seg.slice(0, 4)); - cleanSegments(); draw(); - }; - const syncPair = (a, b, redraw = false) => { if (!a || !b) return; a.oninput = () => { b.value = a.value; if (redraw) draw(); }; b.oninput = () => { a.value = b.value; if (redraw) draw(); }; }; - syncPair(angle, angleNum, false); - syncPair(speed, speedNum, false); - syncPair(travel, travelNum, true); - syncPair(thick, thickNum, true); - dialog.querySelectorAll("[data-reciprocator-mode]").forEach(btn => { btn.onclick = () => setMode(btn.dataset.reciprocatorMode); }); - dialog.querySelectorAll("[data-reciprocator-template]").forEach(btn => { btn.onclick = () => setTemplate(btn.dataset.reciprocatorTemplate); }); - canvas.onpointerdown = (e) => { - canvas.setPointerCapture?.(e.pointerId); - const p = toLocal(e); - drawing = true; - if (mode === "erase") { draft = null; eraseAt(p); return; } - draft = [p.x, p.y, p.x, p.y]; - if (mode === "free") segments.push([p.x, p.y, p.x, p.y]); - draw(); - }; - canvas.onpointermove = (e) => { - if (!drawing) return; - const p = toLocal(e); - if (mode === "erase") { eraseAt(p); return; } - if (!draft) return; - if (mode === "free") { - const last = segments[segments.length - 1]; - if (!last) return; - const lx = last[2], ly = last[3]; - if (Math.hypot(p.x - lx, p.y - ly) >= 5) { - last[2] = p.x; last[3] = p.y; - segments.push([p.x, p.y, p.x, p.y]); - } - } else { - draft[2] = p.x; draft[3] = p.y; - } - draw(); - }; - canvas.onpointerup = (e) => { - if (!drawing) return; - drawing = false; - const p = toLocal(e); - if (mode === "erase") { draft = null; draw(); return; } - if (!draft) return; - if (mode === "line") { - const seg = [draft[0], draft[1], p.x, p.y]; - if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= 4) segments.push(seg); - } - draft = null; - cleanSegments(); - draw(); - }; - if (undo) undo.onclick = () => { segments.pop(); cleanSegments(); draw(); }; - if (clear) clear.onclick = () => { segments = []; draft = null; draw(); }; + editor.syncInputPair(angle, angleNum); + editor.syncInputPair(speed, speedNum); + editor.syncInputPair(travel, travelNum, controller.draw); + editor.syncInputPair(thick, thickNum, controller.draw); const close = () => { dialog.classList.add("hidden"); - canvas.style.cursor = ""; - canvas.onpointerdown = canvas.onpointermove = canvas.onpointerup = null; + controller.close(); }; if (apply) apply.onclick = () => { global.TarinaiHistory.capture(worldRef, "reciprocator-edit"); - cleanSegments(); + controller.cleanSegments(); + const editedSegments = controller.getSegments(); const angleDeg = Math.max(0, Math.min(355, Number(angleNum?.value || angle?.value || 0) || 0)); const railAxis = normalizedItemAngle(angleDeg * Math.PI / 180, 0); phySet(item, "railAxis", railAxis, "editor-rail-axis"); @@ -764,7 +476,7 @@ phySet(item, "solid", solidInput ? Boolean(solidInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid"); if (phyScalar(item, "railOn", true)) phySet(item, "slideSpeed", 0, "editor-slide-reset"); phySet(item, "thickness", Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || 12) || 12)), "editor-thickness"); - physicsApi()?.setSegments?.(item, segments.map(seg => seg.slice(0, 4)), "editor-segments"); + physicsApi()?.setSegments?.(item, editedSegments, "editor-segments"); const sa = window.TarinaiMechanicalSystem.railAxisAngle(item) ?? phyScalar(item, "railAxis", itemAngleFor(item)); const halfTravel = Math.max(12, phyScalar(item, "railTravel", 150) * 0.5); const phase = Number(phyScalar(item, "railPhase", 0) || 0) || 0; @@ -783,8 +495,7 @@ if (cancel) cancel.onclick = close; if (closeBtn) closeBtn.onclick = close; dialog.onclick = (e) => { if (e.target === dialog) close(); }; - setMode(mode); - draw(); + controller.draw(); dialog.classList.remove("hidden"); return true; } @@ -960,14 +671,7 @@ item.vy = 0; item.spinVelocity = 0; if (isPinType(item.type)) { - item.pinState = "loose"; - item.pinTargetId = ""; - item.pinAttachAngle = 0; - item.pinAttachDistance = 0; - item.pinOffsetY = 0; - item.pinDamageTick = 0; - item.pinFallCheckTimer = 0; - item.pinLogAt = -999; + global.TarinaiPinAttachmentSystem?.resetLooseState?.(item); } return item; } @@ -1003,7 +707,6 @@ if (item.type === "reciprocator") return openReciprocatorEditor(item, worldRef); if (item.type === "gate_fence") { item.gateOpen = !item.gateOpen; - item.gateLastToggleAt = worldRef?.time || 0; worldRef?.markSpatialDirty?.("gate-toggle"); worldRef.drawListDirty = true; worldRef?.log?.(item.gateOpen ? "\u30b2\u30fc\u30c8\u67f5\u3092\u958b\u3051\u305f\u3002" : "\u30b2\u30fc\u30c8\u67f5\u3092\u9589\u3081\u305f\u3002", "observe"); @@ -1066,8 +769,7 @@ } function isSoftPlacementIgnoredItemType(type = "") { - const key = String(type || ""); - return key === "zunchi" || key === "grass_bed" || key === "grass" || key === "water"; + return global.TarinaiCollisionFootprints?.isSoftPlacementIgnoredType?.(type) || false; } @@ -1109,7 +811,7 @@ return search; } - function collectSoftItemsUnderPlacementItem(worldRef, item) { + function collectSoftUnderPlacement(worldRef, item) { if (!worldRef || !item || item.type === "trace" || item.type === "splat") return []; const out = []; const search = placementSoftSearchRadius(item); @@ -1121,7 +823,7 @@ } function clearSoftItemsUnderPlacementItem(worldRef, item, reason = "placement-soft-overlap-clear") { - const items = collectSoftItemsUnderPlacementItem(worldRef, item); + const items = collectSoftUnderPlacement(worldRef, item); let removed = 0; for (const soft of items) { const ok = global.TarinaiStructureLifecycle.deleteItem(worldRef, soft, { @@ -1147,11 +849,25 @@ global.TarinaiPhysicsSoftClear = Object.freeze({ isSoftPlacementType: isSoftPlacementIgnoredItemType, - collectForPlacement: collectSoftItemsUnderPlacementItem, + collectForPlacement: collectSoftUnderPlacement, clearForItem: clearSoftItemsUnderPhysicsItem, }); - function clearOverlappingGrassBedsForPlacement(worldRef, item) { + + function ballPlacementBlocked(worldRef, item) { + if (!worldRef || !item || item.type !== "ball") return false; + const radiusFor = typeof itemRadiusFor === "function" ? itemRadiusFor : ((type, fallback = 12) => fallback); + const r = Math.max(8, Number(item.r || item.radius || radiusFor(item.type, 18) || 18) || 18); + const search = Math.max(72, r * 3.2); + for (const other of worldRef.nearbyItems?.(item.x, item.y, search, true) || worldRef.items || []) { + if (!other || other === item || other.dead || other.type !== "ball") continue; + const or = Math.max(8, Number(other.r || other.radius || radiusFor(other.type, 18) || 18) || 18); + if (distXY(item.x, item.y, other.x, other.y) < r + or - 1.0) return true; + } + return false; + } + + function clearGrassBedsForPlacement(worldRef, item) { return clearSoftItemsUnderPlacementItem(worldRef, item, "placement-soft-overlap-clear"); } @@ -1168,7 +884,6 @@ applyCopyBufferToItem(item, this.copyBuffer); item.x = x; item.y = y; item.world = this; - item.isCopyPreview = true; return item; }, @@ -1189,7 +904,6 @@ } const item = this.copyPreviewItemAt?.(x, y); if (item) { - item.isCopyPreview = false; const placed = this.placeItem(item, false); if (placed) { placed.world = this; @@ -1245,6 +959,7 @@ } } if (item.type === "grass" && (this.grassBlockedAt?.(item.x, item.y, item, { ignoreSoftPlacement: true }) || this.grassOnTarinaiAt?.(item.x, item.y))) return true; + if (ballPlacementBlocked(this, item)) return true; if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true; if (this.importantPlacementOverlapBlocked(item)) return true; if (item.type === "nest_box") { @@ -1387,14 +1102,13 @@ item.dropTimer = 0; item.dropImpactDone = true; } - const removedSoft = clearOverlappingGrassBedsForPlacement(this, item); + const removedSoft = clearGrassBedsForPlacement(this, item); if (removedSoft) this.updateItemCounts?.("placement-soft-overlap-clear"); const placed = this.addItem?.(item, `place:${item.type}`); if (!placed) { if (item.type === "grass") showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`); return null; } - this.emit("tool:place", { item, type: item.type, dropped }); this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type }); this.drawListDirty = true; this.ensureSpatial?.(`place:${item.type}`); diff --git a/js/world_reset_presets.js b/js/world_reset_presets.js new file mode 100644 index 0000000..d28dc11 --- /dev/null +++ b/js/world_reset_presets.js @@ -0,0 +1,375 @@ +"use strict"; + +// Reset preset definitions and world population generators. +(function (global) { + +const RESET_PRESETS = Object.freeze({ + empty: { label: "\u7A7A\u767D" }, + default: { label: "\u30C7\u30D5\u30A9\u30EB\u30C8" }, + athletic: { label: "\u30A2\u30B9\u30EC\u30C1\u30C3\u30AF" }, + crowded: { label: "\u904E\u5BC6" }, + family: { label: "\u5BB6\u65CF" }, + war: { label: "\u6226\u4E89" }, + happy: { label: "\u5E78\u798F" }, + }); + + function normalizeResetPresetId(presetId = "default") { + const id = String(presetId || "default"); + return Object.prototype.hasOwnProperty.call(RESET_PRESETS, id) ? id : "default"; + } + + global.TarinaiResetPresets = Object.freeze({ + ids: Object.freeze(Object.keys(RESET_PRESETS)), + definitions: RESET_PRESETS, + normalize: normalizeResetPresetId, + }); + + function presetScaleForField(field) { + const id = field?.id || "garden"; + if (id === "cage") return 0.65; + if (id === "park") return 1.45; + return 1; + } + + function presetCount(field, base, min = 0, max = Infinity) { + const value = Math.round((Number(base) || 0) * presetScaleForField(field)); + return Math.max(min, Math.min(Number.isFinite(max) ? max : value, value)); + } + + function randPoint(worldRef, pad = 78, zone = null) { + const x0 = Math.max(pad, Number(zone?.x0 ?? pad)); + const x1 = Math.min(worldRef.w - pad, Number(zone?.x1 ?? (worldRef.w - pad))); + const y0 = Math.max(pad, Number(zone?.y0 ?? pad)); + const y1 = Math.min(worldRef.h - pad, Number(zone?.y1 ?? (worldRef.h - pad))); + return { x: rand(Math.min(x0, x1), Math.max(x0, x1)), y: rand(Math.min(y0, y1), Math.max(y0, y1)) }; + } + + function randomPresetAngle(step = Math.PI / 4) { + const n = Math.max(1, Math.round((Math.PI * 2) / Math.max(0.001, step))); + return Math.floor(rand(0, n)) * step; + } + + function presetShapeExtent(segments) { + let max = 0; + for (const seg of segments || []) { + for (let i = 0; i < 4; i += 2) max = Math.max(max, Math.hypot(Number(seg[i]) || 0, Number(seg[i + 1]) || 0)); + } + return max; + } + + function configurePresetMechanicalItem(item, opts = {}) { + const body = item?.physicsBody; + if (!body || (item.type !== "rotator" && item.type !== "reciprocator")) return item; + body.pose = body.pose || { x: item.x, y: item.y, angle: Number(item.angle) || 0 }; + body.pose.x = item.x; + body.pose.y = item.y; + body.pose.angle = Number(item.angle) || 0; + body.velocity = body.velocity || { x: 0, y: 0, angular: 0, linear: 0 }; + body.velocity.x = 0; + body.velocity.y = 0; + body.velocity.angular = 0; + body.velocity.linear = 0; + body.shape = body.shape || { model: "segments", thickness: 12, segments: [], version: 0 }; + + if (item.type === "rotator") { + const segments = opts.segments || [ + [-92, 0, 92, 0], + [0, -58, 0, 58], + [-58, -42, 58, 42], + [-58, 42, 58, -42], + ]; + body.motor = body.motor || { powered: true, speed: Math.PI * 0.18, direction: 1 }; + body.motor.powered = opts.motorOn !== false; + body.motor.speed = Number.isFinite(Number(opts.motorSpeed)) ? Number(opts.motorSpeed) : Math.PI * 0.18; + body.motor.direction = Number(opts.direction) < 0 ? -1 : 1; + body.shape.model = "segments"; + body.shape.thickness = Number.isFinite(Number(opts.thickness)) ? Number(opts.thickness) : 14; + body.shape.segments = segments.map(seg => seg.slice(0, 4)); + body.shape.version = (Number(body.shape.version) || 0) + 1; + item.spin = 0; + item.r = Math.max(item.r || 64, presetShapeExtent(segments) + body.shape.thickness + 8); + } else if (item.type === "reciprocator") { + const railAxis = Number.isFinite(Number(opts.railAxis)) ? Number(opts.railAxis) : (Number(item.angle) || 0); + const segments = opts.segments || [ + [-92, -24, -36, -24], + [-36, -24, -36, 24], + [-36, 24, 36, 24], + [36, 24, 36, -24], + [36, -24, 92, -24], + ]; + body.motor = body.motor || { powered: true, speed: 36, direction: 1 }; + body.motor.powered = opts.railOn !== false; + body.motor.speed = Number.isFinite(Number(opts.railMotorSpeed)) ? Number(opts.railMotorSpeed) : 36; + body.motor.direction = Number(opts.direction) < 0 ? -1 : 1; + body.rail = body.rail || { axisAngle: railAxis, travel: 120, phase: 0, anchorX: item.x, anchorY: item.y }; + body.rail.axisAngle = railAxis; + body.rail.travel = Number.isFinite(Number(opts.railTravel)) ? Number(opts.railTravel) : 120; + body.rail.phase = Number.isFinite(Number(opts.railPhase)) ? Number(opts.railPhase) : 0; + body.rail.anchorX = item.x; + body.rail.anchorY = item.y; + body.shape.model = "segments"; + body.shape.thickness = Number.isFinite(Number(opts.thickness)) ? Number(opts.thickness) : 14; + body.shape.segments = segments.map(seg => seg.slice(0, 4)); + body.shape.version = (Number(body.shape.version) || 0) + 1; + item.r = Math.max(item.r || 64, presetShapeExtent(segments) + body.shape.thickness + 8); + } + globalThis.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "preset-mechanical-shape"); + globalThis.TarinaiMechanicalSystem?.invalidateGeometry?.(item); + return item; + } + + function syncPresetMechanicalPose(item) { + const body = item?.physicsBody; + if (!body) return; + body.pose = body.pose || { x: item.x, y: item.y, angle: Number(item.angle) || 0 }; + body.pose.x = item.x; + body.pose.y = item.y; + body.pose.angle = Number(item.angle) || 0; + if (body.rail) { + body.rail.anchorX = item.x; + body.rail.anchorY = item.y; + } + globalThis.TarinaiPhysicsBodySystem?.invalidateItem?.(item, "preset-placement-pose"); + globalThis.TarinaiMechanicalSystem?.invalidateGeometry?.(item); + } + + function presetItemFootprintRadius(worldRef, item) { + const radius = globalThis.TarinaiCollisionFootprints?.itemFootprintRadius?.(worldRef, item); + if (Number.isFinite(Number(radius))) { + if (item?.type === "ball") return Math.max(34, Number(radius) * 1.35); + return Math.max(14, Number(radius)); + } + return Math.max(14, Number(item?.r || 16) || 16); + } + + function presetItemPlacementBlocked(worldRef, item, opts = {}) { + if (!worldRef || !item || item.dead) return true; + if (typeof worldRef.placementBlocked === "function" && worldRef.placementBlocked(item)) return true; + const itemRects = worldRef.solidObstacleRects?.(item) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, item) || []; + const itemRadius = presetItemFootprintRadius(worldRef, item); + const search = Math.max(120, itemRadius * 3.2); + for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || worldRef.items || []) { + if (!other || other === item || other.dead) continue; + const sameType = other.type === item.type; + const otherIsFence = worldRef.isFenceType?.(other.type) || false; + const itemIsFence = worldRef.isFenceType?.(item.type) || false; + if (!opts.avoidAllObstacles && !sameType && !otherIsFence && !itemIsFence) continue; + const otherRects = worldRef.solidObstacleRects?.(other) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, other) || []; + const otherRadius = presetItemFootprintRadius(worldRef, other); + const margin = (itemIsFence || otherIsFence) ? 4 : Math.max(8, Math.min(itemRadius, otherRadius) * 0.28); + if (itemRects.length && otherRects.length) { + if (itemRects.some(a => otherRects.some(b => globalThis.TarinaiCollisionFootprints?.rectsOverlap?.(a, b, margin)))) return true; + continue; + } + if (itemRects.length) { + if (itemRects.some(r => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(r, other.x, other.y, otherRadius, margin))) return true; + continue; + } + if (otherRects.length) { + if (otherRects.some(r => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(r, item.x, item.y, itemRadius, margin))) return true; + continue; + } + if (Math.hypot((item.x || 0) - (other.x || 0), (item.y || 0) - (other.y || 0)) < itemRadius + otherRadius + margin) return true; + } + return false; + } + + function findPresetItemSpot(worldRef, item, opts = {}) { + const allowOriginal = opts.allowOriginal !== false; + if (allowOriginal && !presetItemPlacementBlocked(worldRef, item, opts)) return { x: item.x, y: item.y }; + const solid = Boolean(worldRef?.isFenceType?.(item.type) || item.type === "rotator" || item.type === "reciprocator" || item.type === "nest_box"); + const count = Math.max(12, Number(opts.attempts || 0) || (solid ? 96 : 58)); + const maxRadius = Math.max(70, Number(opts.maxRadius || 0) || (solid ? 220 : 170)); + const golden = Math.PI * (3 - Math.sqrt(5)); + let best = null; + let bestD = Infinity; + const seed = `${item.type}:${Math.round(item.x)},${Math.round(item.y)}`; + for (let i = 0; i < count; i++) { + const t = count <= 1 ? 1 : i / (count - 1); + const radius = Math.max(10, maxRadius * Math.sqrt(t)); + const angle = i * golden + (globalThis.stableUnit ? globalThis.stableUnit(seed, "preset-place") : Math.random()) * Math.PI * 2; + const rawX = item.x + Math.cos(angle) * radius; + const rawY = item.y + Math.sin(angle) * radius * 0.74; + const p = worldRef.placementClampPointFor?.(item, rawX, rawY) || { x: clamp(rawX, 42, worldRef.w - 42), y: clamp(rawY, 42, worldRef.h - 42) }; + const oldX = item.x, oldY = item.y; + item.x = p.x; item.y = p.y; syncPresetMechanicalPose(item); + const blocked = presetItemPlacementBlocked(worldRef, item, opts); + item.x = oldX; item.y = oldY; syncPresetMechanicalPose(item); + if (blocked) continue; + const d = Math.hypot(p.x - oldX, p.y - oldY); + if (d < bestD) { best = p; bestD = d; } + } + return best; + } + + function addPresetItem(worldRef, type, x, y, opts = {}) { + const item = new Item(type, x, y); + if (Number.isFinite(Number(opts.r))) item.r = Number(opts.r); + if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle); + if (opts.toolSize) item.toolSize = opts.toolSize; + if (type === "duplicator") { + item.storedFoodType = opts.storedFoodType || "sweet"; + item.storedFoodLabel = typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType; + if (item.roles) item.roles.food = true; + } + if (type === "rotator" || type === "reciprocator") configurePresetMechanicalItem(item, opts); + if (opts.avoidOverlap) { + const spot = findPresetItemSpot(worldRef, item, opts); + if (!spot) return null; + item.x = spot.x; + item.y = spot.y; + syncPresetMechanicalPose(item); + } + if (worldRef.addItem) worldRef.addItem(item, opts.reason || `preset-${type}`); + else worldRef.items.push(item); + return item; + } + + function addPresetGrass(worldRef, count, zone = null) { + for (let i = 0; i < count; i++) { + const p = randPoint(worldRef, 70, zone); + const grass = addPresetItem(worldRef, "grass", p.x, p.y, { reason: "preset-grass" }); + globalThis.TarinaiGrass?.setStage?.(grass, Math.floor(rand(0, 2.999)), { force: true }); + } + } + + function addPresetTarinai(worldRef, count, zone = null, opts = {}) { + for (let i = 0; i < count; i++) { + const p = randPoint(worldRef, 82, zone); + worldRef.addTarinai({ x: p.x, y: p.y, generation: opts.generation || 1 }); + } + } + + function addPresetItemsScattered(worldRef, type, count, zone = null, opts = {}) { + let placed = 0; + const n = Math.max(0, Number(count) | 0); + const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n; + for (let i = 0; i < attempts && placed < n; i++) { + const p = randPoint(worldRef, opts.pad || 82, zone); + const item = addPresetItem(worldRef, type, p.x, p.y, opts); + if (item) placed++; + } + } + + function addPresetFenceLine(worldRef, count, yRatio, angle = 0, type = "fence_h", opts = {}) { + const n = Math.max(1, Number(count) | 0); + const gap = worldRef.w / (n + 1); + for (let i = 1; i <= n; i++) { + addPresetItem(worldRef, type, gap * i, worldRef.h * yRatio + rand(-24, 24), { angle, reason: opts.reason || "preset-fence", avoidOverlap: !!opts.avoidOverlap, avoidAllObstacles: !!opts.avoidAllObstacles, attempts: opts.attempts || 96, maxRadius: opts.maxRadius || 190 }); + } + } + + function addPresetFencesScattered(worldRef, count, zone = null, opts = {}) { + const n = Math.max(0, Number(count) | 0); + let placed = 0; + const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n; + for (let i = 0; i < attempts && placed < n; i++) { + const p = randPoint(worldRef, opts.pad || 78, zone); + const vertical = Math.random() < 0.5; + const item = addPresetItem(worldRef, vertical ? "fence_v" : "fence_h", p.x, p.y, { angle: randomPresetAngle(Math.PI / 4), reason: opts.reason || "preset-fence-scatter", avoidOverlap: !!opts.avoidOverlap, attempts: opts.attempts || 80, maxRadius: opts.maxRadius || 180 }); + if (item) placed++; + } + } + + function generateDefaultPreset(worldRef, field, seedPopulation) { + const population = seedPopulation ?? field.population ?? CONFIG.initialPopulation; + const grassCount = field.grass ?? 18; + addPresetTarinai(worldRef, population); + addPresetGrass(worldRef, grassCount); + addPresetItem(worldRef, "stone", worldRef.w * 0.68, worldRef.h * 0.32, { reason: "reset-stone" }); + } + + function generateAthleticPreset(worldRef, field) { + const factor = presetScaleForField(field); + const fieldId = field?.id || "garden"; + const compact = fieldId === "cage" || fieldId === "garden"; + const center = { x0: worldRef.w * 0.10, x1: worldRef.w * 0.90, y0: worldRef.h * 0.14, y1: worldRef.h * 0.86 }; + addPresetTarinai(worldRef, presetCount(field, 14, 7, 24), { x0: worldRef.w * 0.08, x1: worldRef.w * 0.30, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 }); + // \u7269\u7406\u30D6\u30ED\u30C3\u30AF\u306F\u67F5\u3088\u308A\u5148\u306B\u7F6E\u304F\u3002\u72ED\u3044\u4E2D\u5C0F\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u306F\u67F5\u5074\u3092\u9593\u5F15\u304D\u3001\u30D6\u30ED\u30C3\u30AF\u3092\u512A\u5148\u3059\u308B\u3002 + addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 }); + addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 }); + addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 }); + addPresetFenceLine(worldRef, Math.max(2, Math.round((compact ? 3 : 4) * factor)), 0.36, 0, "fence_h", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" }); + addPresetFenceLine(worldRef, Math.max(1, Math.round((compact ? 2 : 3) * factor)), 0.62, Math.PI / 2, "fence_v", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" }); + addPresetFencesScattered(worldRef, Math.max(1, Math.round((compact ? 2 : 4) * factor)), null, { pad: 108, reason: "preset-athletic-fence", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210 }); + addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-slow-custom", motorSpeed: Math.PI * 0.18, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 }); + addPresetItemsScattered(worldRef, "reciprocator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-reciprocator-slow-custom", railMotorSpeed: 36, railTravel: 120, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 }); + } + + function generateCrowdedPreset(worldRef, field) { + const center = { x0: worldRef.w * 0.18, x1: worldRef.w * 0.82, y0: worldRef.h * 0.18, y1: worldRef.h * 0.82 }; + addPresetTarinai(worldRef, presetCount(field, 36, 18, 58), center); + addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 13, 7, 22), center, { pad: 96, reason: "preset-nest-box" }); + addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 3, 2, 5), center, { pad: 110, storedFoodType: "sweet", reason: "preset-duplicator-zunda" }); + addPresetGrass(worldRef, presetCount(field, 18, 8, 34), center); + addPresetItemsScattered(worldRef, "water", presetCount(field, 8, 4, 16), center, { pad: 84, reason: "preset-water" }); + } + + function generateFamilyPreset(worldRef, field) { + const cx = worldRef.w * 0.5; + const cy = worldRef.h * 0.52; + worldRef.addTarinai({ x: cx - 52, y: cy + 20, generation: 1 }); + worldRef.addTarinai({ x: cx + 52, y: cy + 20, generation: 1 }); + addPresetItem(worldRef, "nest_box", cx, cy - 58, { reason: "preset-family-nest" }); + addPresetItem(worldRef, "duplicator", cx, cy + 112, { storedFoodType: "love_mochi", reason: "preset-family-duplicator" }); + } + + function generateWarPreset(worldRef, field) { + const left = { x0: worldRef.w * 0.08, x1: worldRef.w * 0.45, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 }; + const right = { x0: worldRef.w * 0.55, x1: worldRef.w * 0.92, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 }; + addPresetTarinai(worldRef, Math.ceil(presetCount(field, 28, 14, 48) / 2), left); + addPresetTarinai(worldRef, Math.floor(presetCount(field, 28, 14, 48) / 2), right); + addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 3, 2, 5), { x0: worldRef.w * 0.38, x1: worldRef.w * 0.62, y0: worldRef.h * 0.22, y1: worldRef.h * 0.78 }, { pad: 96, storedFoodType: "fight_mochi", reason: "preset-war-duplicator" }); + addPresetItemsScattered(worldRef, "ball", presetCount(field, 8, 4, 14), null, { pad: 90, reason: "preset-war-ball" }); + addPresetItemsScattered(worldRef, "ant_nest", presetCount(field, 1.25, 1, 2), null, { pad: 116, reason: "preset-war-ant-nest", avoidOverlap: true, attempts: 80, maxRadius: 190 }); + addPresetItemsScattered(worldRef, "pushpin", presetCount(field, 5, 2, 9), null, { pad: 88, reason: "preset-war-pushpin-reduced" }); + addPresetItemsScattered(worldRef, "oshibyo", presetCount(field, 8, 4, 16), null, { pad: 88, reason: "preset-war-oshibyo" }); + addPresetFencesScattered(worldRef, presetCount(field, 8, 4, 14), null, { pad: 94, reason: "preset-war-fence" }); + } + + function generateHappyPreset(worldRef, field) { + const center = { x0: worldRef.w * 0.16, x1: worldRef.w * 0.84, y0: worldRef.h * 0.16, y1: worldRef.h * 0.84 }; + addPresetTarinai(worldRef, presetCount(field, 16, 8, 28), center); + addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 8, 4, 14), center, { pad: 96, reason: "preset-happy-nest-box" }); + addPresetItemsScattered(worldRef, "robot_cleaner", presetCount(field, 2, 1, 3), center, { pad: 110, reason: "preset-happy-robot-cleaner" }); + addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" }); + addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" }); + } + + function generatePresetWorld(worldRef, field, presetId, seedPopulation) { + switch (normalizeResetPresetId(presetId)) { + case "empty": return; + case "athletic": return generateAthleticPreset(worldRef, field); + case "crowded": return generateCrowdedPreset(worldRef, field); + case "family": return generateFamilyPreset(worldRef, field); + case "war": return generateWarPreset(worldRef, field); + case "happy": return generateHappyPreset(worldRef, field); + case "default": + default: + return generateDefaultPreset(worldRef, field, seedPopulation); + } + } + + function groundForResetPreset(presetId = "default") { + const id = normalizeResetPresetId(presetId); + if (id === "athletic") return "ice"; + if (id === "war") return "foot_massage"; + if (id === "happy") return "blanket"; + return "soil"; + } + + function presetLabel(presetId = "default") { + return RESET_PRESETS[normalizeResetPresetId(presetId)]?.label || "\u30C7\u30D5\u30A9\u30EB\u30C8"; + } + + const api = Object.freeze({ + ids: Object.freeze(Object.keys(RESET_PRESETS)), + definitions: RESET_PRESETS, + normalize: normalizeResetPresetId, + generatePresetWorld, + groundForResetPreset, + label: presetLabel, + }); + global.TarinaiResetPresets = api; + global.TarinaiWorldResetPresets = api; +})(typeof window !== "undefined" ? window : globalThis); diff --git a/js/world_temperature_system.js b/js/world_temperature_system.js new file mode 100644 index 0000000..eeb15df --- /dev/null +++ b/js/world_temperature_system.js @@ -0,0 +1,238 @@ +"use strict"; + +// Layer: world/temperature +// Temperature, seasons, climate comfort scoring, and temperature-driven target selection. +(function (global) { + const World = global.World; + if (!World) return; + + Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ + temperatureNumber(value, fallback = CONFIG.standardTemperature ?? 15) { + const v = Number(value); + const fb = Number(fallback); + return Number.isFinite(v) ? v : (Number.isFinite(fb) ? fb : 15); + }, + climateTemperature(value) { + // \u81EA\u7136\u6C17\u5019\u3060\u3051\u306F\u4ED5\u69D8\u3069\u304A\u308A -20\u2103\u301C50\u2103 \u306E\u7BC4\u56F2\u306B\u53CE\u3081\u308B\u3002 + const v = this.temperatureNumber(value); + return clamp(v, CONFIG.climateTemperatureMin ?? -20, CONFIG.climateTemperatureMax ?? 50); + }, + seasonIndex() { + return Math.floor((((Number(this.day) || 1) - 1) % 20) / 5); + }, + seasonLabel() { + return ["\u6625", "\u590F", "\u79CB", "\u51AC"][this.seasonIndex()] || "\u6625"; + }, + seasonDay() { + return ((((Number(this.day) || 1) - 1) % 5) + 1); + }, + seasonDayString() { + return `${this.seasonLabel()}${this.seasonDay()}\u65E5`; + }, + temperatureStableUnit(salt = "") { + const key = `${this.worldSeed || "world"}:temperature:${salt}`; + if (typeof stableUnit === "function") return stableUnit(key, "temperature"); + let h = 2166136261; + for (let i = 0; i < key.length; i++) h = Math.imul(h ^ key.charCodeAt(i), 16777619); + return ((h >>> 0) % 1000000) / 1000000; + }, + dailyTemperatureRange(day = this.day, weather = this.weather) { + const raw = this.temperatureStableUnit(`daily-range:${day}`) * 15; + if (weather === "sunny") return clamp(6 + raw * 0.62, 0, 15); + if (weather === "light_rain") return clamp(raw * 0.32, 0, 5); + return clamp(raw * 0.45, 0, 7); + }, + annualTemperatureOffset() { + const dayLength = Math.max(1, CONFIG.dayLength || 120); + const totalDays = Math.max(0, Number(this.time || 0) / dayLength); + const phase = ((totalDays % 20) + 20) % 20 / 20; + const range = Number.isFinite(Number(this.temperatureAnnualRange)) ? Number(this.temperatureAnnualRange) : 30; + const wave = Math.sin(phase * Math.PI * 2 - Math.PI * 0.25); + return wave * range * 0.5; + }, + dailyTemperatureWave(progress = 0) { + const hour = ((((Number(progress) || 0) % 1) + 1) % 1) * 24; + const minHour = 5.0; // \u671D\u65B9\u306B\u6700\u4F4E\u6C17\u6E29 + const maxHour = 14.0; // \u663C\u904E\u304E\u306B\u6700\u9AD8\u6C17\u6E29 + const smooth = (x) => { + const t = clamp(x, 0, 1); + return t * t * (3 - 2 * t); + }; + let warmth; + if (hour >= minHour && hour <= maxHour) { + warmth = smooth((hour - minHour) / Math.max(0.001, maxHour - minHour)); + } else { + const downHour = hour > maxHour ? hour - maxHour : hour + 24 - maxHour; + const downSpan = minHour + 24 - maxHour; + warmth = 1 - smooth(downHour / Math.max(0.001, downSpan)); + } + return warmth * 2 - 1; + }, + naturalTemperature() { + // \u521D\u671F\u30ED\u30FC\u30C9\u76F4\u5F8C\u306F 10\u2103 \u3092\u4FDD\u6301\u3057\u3001\u521D\u56DE\u30D5\u30EC\u30FC\u30E0\u306Edt\u63FA\u308C\u30670\u2103\u8FD1\u304F\u3078\u843D\u3061\u306A\u3044\u3088\u3046\u306B\u3059\u308B\u3002 + if ((Number(this.time) || 0) <= 6.0 && (Number(this.day) || 1) === 1) { + this.temperatureDailyRange = 0; + return 10; + } + const dayLength = Math.max(1, CONFIG.dayLength || 120); + const progress = ((Number(this.time || 0) % dayLength) + dayLength) % dayLength / dayLength; + const dailyRange = this.dailyTemperatureRange(this.day, this.weather); + const dailyWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(progress) : Math.sin(progress * Math.PI * 2 - Math.PI * 0.5); + let temp; + if ((Number(this.day) || 1) === 1) { + // \u66251\u65E5\u76EE\u306F\u958B\u59CB\u664210\u2103\u3092\u57FA\u6E96\u306B\u3057\u3064\u3064\u3001\u65E5\u5185\u306E\u6700\u4F4E/\u6700\u9AD8\u6642\u523B\u3060\u3051\u306F\u7DAD\u6301\u3059\u308B\u3002 + const initialWave = this.dailyTemperatureWave ? this.dailyTemperatureWave(0) : Math.sin(-Math.PI * 0.5); + temp = 10 + (dailyWave - initialWave) * dailyRange * 0.5; + } else { + temp = (CONFIG.standardTemperature ?? 15) + this.annualTemperatureOffset() + dailyWave * dailyRange * 0.5; + } + this.temperatureDailyRange = dailyRange; + return this.climateTemperature(temp); + }, + updateTemperature(dt = 0) { + if (!Number.isFinite(Number(this.temperatureAnnualRange))) { + this.temperatureAnnualRange = 25 + this.temperatureStableUnit("annual-range") * 10; + } + if (!Number.isFinite(Number(this.manualTemperature))) this.manualTemperature = CONFIG.standardTemperature ?? 15; + this.currentTemperature = this.temperatureAuto === false + ? this.temperatureNumber(this.manualTemperature) + : this.naturalTemperature(); + return this.currentTemperature; + }, + setTemperatureAuto(enabled = true) { + this.temperatureAuto = Boolean(enabled); + if (this.temperatureAuto === false) this.manualTemperature = this.temperatureNumber(this.currentTemperature ?? this.manualTemperature ?? CONFIG.standardTemperature ?? 15); + return this.updateTemperature?.(0) ?? this.currentTemperature; + }, + setManualTemperature(value = 15) { + this.manualTemperature = this.temperatureNumber(value); + if (this.temperatureAuto === false) this.currentTemperature = this.manualTemperature; + return this.manualTemperature; + }, + temperatureAt(x, y, actor = null) { + let temp = Number.isFinite(Number(this.currentTemperature)) ? Number(this.currentTemperature) : this.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15); + if (this.temperatureAuto === false) temp = this.temperatureNumber(this.manualTemperature); + if ((this.groundType || "soil") === "ice") temp -= 5; + const range = Math.max(24, CONFIG.temperatureItemRange ?? 190); + for (const it of this.nearbyItems(x, y, range + 8)) { + if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove")) continue; + const d = distXY(x, y, it.x, it.y); + if (d > range) continue; + const edge = d <= range - 24 ? 1 : clamp((range - d) / 24, 0, 1); + temp += (it.type === "stove" ? 10 : -10) * edge; + } + const nestWarmthLimit = 5; + for (const box of this.nearbyItems?.(x, y, 86) || []) { + if (!box || box.dead || box.type !== "nest_box") continue; + const r = Math.max(36, Number(box.r || 42) || 42); + const base = this.nestBoxBaseRect?.(box) || null; + const inBase = base && x >= base.left - 4 && x <= base.right + 4 && y >= base.top - 4 && y <= base.bottom + 4; + const inBody = distXY(x, y, box.x, box.y) <= r * 1.08; + if (!inBase && !inBody) continue; + const toward = 20 - temp; + temp += clamp(toward, -nestWarmthLimit, nestWarmthLimit); + break; + } + if (actor && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3; + return this.temperatureNumber(temp); + }, + feltTemperatureFor(actor, x = actor?.x, y = actor?.y) { + if (!actor) return this.temperatureAt(x, y); + return this.temperatureAt(x, y, actor); + }, + temperatureComfortOffsetFor(actor = null) { + const raw = actor?.genetics?.temperatureOffset; + const n = Number(raw); + return Number.isFinite(n) ? n : 0; + }, + temperatureStatusFor(temp = CONFIG.standardTemperature ?? 15, actor = null) { + const value = this.temperatureNumber(temp); + const offset = this.temperatureComfortOffsetFor(actor); + const min = (CONFIG.temperatureComfortMin ?? 10) + offset; + const max = (CONFIG.temperatureComfortMax ?? 25) + offset; + const discomfort = value < min ? min - value : (value > max ? value - max : 0); + const direction = value < min ? "cold" : (value > max ? "hot" : "comfort"); + const stressExcess = Math.max(0, discomfort - (CONFIG.temperatureStressMargin ?? 5)); + const harmExcess = Math.max(0, discomfort - (CONFIG.temperatureEnergyMargin ?? 10)); + return { + temp: value, + direction, + discomfort, + comfortable: discomfort <= 0, + stressExcess, + harmExcess, + harmful: harmExcess > 0, + label: discomfort <= 0 ? "\u5FEB\u9069" : (direction === "cold" ? (harmExcess > 0 ? "\u5371\u967A\u306A\u5BD2\u3055" : "\u5BD2\u3044") : (harmExcess > 0 ? "\u5371\u967A\u306A\u6691\u3055" : "\u6691\u3044")), + }; + }, + findComfortTemperatureSpot(actor, opts = {}) { + if (!actor || actor.dead) return null; + const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt(actor.x, actor.y, actor); + const currentStatus = this.temperatureStatusFor(current, actor); + if (!opts.force && currentStatus.comfortable) return null; + const comfortOffset = this.temperatureComfortOffsetFor(actor); + const targetTemp = ((CONFIG.temperatureComfortMin ?? 10) + (CONFIG.temperatureComfortMax ?? 25)) * 0.5 + comfortOffset; + const radius = actor.radius || actor.r || 20; + const rings = [72, 128, 196, 288, 392]; + const dirs = 18; + const candidates = []; + const pushCandidate = (x, y, extra = 0) => candidates.push({ + x: clamp(x, radius + 8, (this.w || 1000) - radius - 8), + y: clamp(y, radius + 8, (this.h || 720) - radius - 8), + extra, + }); + for (const ring of rings) { + for (let i = 0; i < dirs; i++) { + const a = (Math.PI * 2 * i / dirs) + ring * 0.017; + pushCandidate(actor.x + Math.cos(a) * ring, actor.y + Math.sin(a) * ring); + } + } + const itemRange = Math.max(24, CONFIG.temperatureItemRange ?? 190); + for (const it of this.items || []) { + if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove")) continue; + if (currentStatus.direction === "cold" && it.type !== "stove") continue; + if (currentStatus.direction === "hot" && it.type !== "dry_ice") continue; + const toward = Math.atan2(actor.y - it.y, actor.x - it.x); + pushCandidate(it.x + Math.cos(toward) * itemRange * 0.45, it.y + Math.sin(toward) * itemRange * 0.45, -60); + pushCandidate(it.x + Math.cos(toward + 0.75) * itemRange * 0.62, it.y + Math.sin(toward + 0.75) * itemRange * 0.62, -32); + pushCandidate(it.x + Math.cos(toward - 0.75) * itemRange * 0.62, it.y + Math.sin(toward - 0.75) * itemRange * 0.62, -32); + } + let best = null; + let bestScore = Infinity; + for (const c of candidates) { + if (this.pointBlockedByObstacle?.(c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor })) continue; + const temp = this.temperatureAt(c.x, c.y, actor); + const status = this.temperatureStatusFor(temp, actor); + if (currentStatus.direction === "cold" && temp <= current + 0.6 && !status.comfortable) continue; + if (currentStatus.direction === "hot" && temp >= current - 0.6 && !status.comfortable) continue; + const d = distXY(actor.x, actor.y, c.x, c.y); + const blockedPenalty = this.pathBlockedByFence?.(actor.x, actor.y, c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor }) ? 480 : 0; + const score = status.discomfort * 110 + Math.abs(temp - targetTemp) * 2.8 + d * 0.22 + blockedPenalty + (c.extra || 0); + if (score < bestScore) { + bestScore = score; + best = { x: c.x, y: c.y, dead: false, detour: true, temperatureTarget: true, temperature: temp, label: "\u5FEB\u9069\u306A\u6E29\u5EA6\u5E2F" }; + } + } + if (!best) return null; + const bestStatus = this.temperatureStatusFor(best.temperature, actor); + if (opts.requireComfort && !bestStatus.comfortable) return null; + if (!bestStatus.comfortable && bestStatus.discomfort >= currentStatus.discomfort - 0.65) return null; + best.temperatureStatus = bestStatus; + return best; + }, + hasReachableComfortTemperatureSpot(actor) { + return !!this.findComfortTemperatureSpot?.(actor, { force: true, requireComfort: true }); + }, + sleepTargetTemperatureBlocked(actor, item) { + if (!actor || !item || item.dead) return false; + if (!(item.type === "grass_bed" || item.type === "bed" || item.type === "nest_box")) return false; + const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt?.(actor.x, actor.y, actor); + const currentStatus = this.temperatureStatusFor?.(current, actor); + if (!currentStatus || currentStatus.comfortable) return false; + if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false; + const temp = this.temperatureAt?.(item.x, item.y, actor); + const status = this.temperatureStatusFor?.(temp, actor); + return Boolean(status && !status.comfortable); + }, + })); +})(typeof window !== "undefined" ? window : globalThis); diff --git a/scripts/regression_check.py b/scripts/regression_check.py index 4dc60ae..101d399 100644 --- a/scripts/regression_check.py +++ b/scripts/regression_check.py @@ -898,6 +898,7 @@ def check_tarinai_update_pipeline_boundary() -> None: "js/tarinai_update_step_environment.js", "js/tarinai_update_step_movement.js", "js/tarinai_update_step_health.js", + "js/update_step_pipeline_runner.js", "js/tarinai_update_pipeline.js", ] for rel in required: @@ -938,6 +939,7 @@ def check_tarinai_update_pipeline_boundary() -> None: "js/tarinai_update_step_environment.js": "global.TarinaiEnvironmentUpdateStep", "js/tarinai_update_step_movement.js": "global.TarinaiMovementUpdateStep", "js/tarinai_update_step_health.js": "global.TarinaiHealthUpdateStep", + "js/update_step_pipeline_runner.js": "global.TarinaiUpdateStepPipelineRunner", "js/tarinai_update_pipeline.js": "global.TarinaiUpdatePipeline", } for rel, token in exports.items(): @@ -956,9 +958,7 @@ def check_tarinai_update_pipeline_boundary() -> None: fail("Tarinai.prototype.update residue remains in needs mixin") if "TarinaiItemInteractionSystem.updateOne" not in needs_items or "const foodNeed = Number(this.needRaw?.food" in needs_items: fail("Tarinai.prototype.interactWithItems is not a thin facade over the interaction system") - for facade_token in ["TarinaiSunbathSystem.updateOne", "TarinaiCursorCareSystem.updateOne", "TarinaiLocalEnvironmentSystem.updateOne"]: - if facade_token not in needs_items: - fail(f"Tarinai prototype subsystem facade missing: {facade_token}") + # These subsystems are now routed by the split update steps rather than the prototype needs mixin. interaction_context = read_text(ROOT / "js/tarinai_item_interaction_context.js") food_interaction = read_text(ROOT / "js/tarinai_food_interaction_system.js") contact_interaction = read_text(ROOT / "js/tarinai_contact_item_system.js") @@ -1093,6 +1093,7 @@ def check_item_lifecycle_pipeline_boundary() -> None: "js/item_lifecycle_step_decay.js", "js/item_lifecycle_step_dynamic.js", "js/item_lifecycle_step_growth.js", + "js/update_step_pipeline_runner.js", "js/item_lifecycle_pipeline.js", ] for rel in required: @@ -1110,6 +1111,7 @@ def check_item_lifecycle_pipeline_boundary() -> None: "js/item_lifecycle_step_decay.js", "js/item_lifecycle_step_dynamic.js", "js/item_lifecycle_step_growth.js", + "js/update_step_pipeline_runner.js", "js/item_lifecycle_pipeline.js", "js/item_runtime.js", ] @@ -1129,6 +1131,7 @@ def check_item_lifecycle_pipeline_boundary() -> None: "js/item_lifecycle_step_decay.js": "global.TarinaiItemDecayLifecycleStep", "js/item_lifecycle_step_dynamic.js": "global.TarinaiItemDynamicLifecycleStep", "js/item_lifecycle_step_growth.js": "global.TarinaiItemGrowthLifecycleStep", + "js/update_step_pipeline_runner.js": "global.TarinaiUpdateStepPipelineRunner", "js/item_lifecycle_pipeline.js": "global.TarinaiItemLifecyclePipeline", } for rel, token in exports.items(): diff --git a/service-worker.js b/service-worker.js index 8a97e60..63b3a53 100644 --- a/service-worker.js +++ b/service-worker.js @@ -1,11 +1,11 @@ "use strict"; -const APP_VERSION = "39.15.98"; +const APP_VERSION = "39.15.101"; const CACHE_NAME = `tarinai-colony-${APP_VERSION}`; const v = `v=${APP_VERSION}`; // Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files. const coreScriptNames = [ - "version", "event_bus", "registry_base", "disease_registry", "sound_pack", "deterministic_helpers", "item_tool_definitions", "item_visual_definitions", "item_food_definitions", "item_effect_definitions", "item_registry", "data", "ground_types", "input_mode_manager", "math", "collision_footprint_system", "physics_helpers", "placement_preview_system", "pin_attachment_system", "physics_shape_editor_system", "mechanical_system", "mechanical_shape_bridge", "constraint_system", "physics_world_system", "physics_projection_system", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_support", "item_update_policy", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_nest_sleep_system", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "impact_core_system", "impact_response_system", "collision_response_system", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "system_order", "simulation", "colony_situation_system", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_pointer_action_system", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools" + "version", "event_bus", "registry_base", "disease_registry", "sound_pack", "deterministic_helpers", "item_tool_metadata", "item_tool_definitions", "item_visual_definitions", "item_food_definitions", "item_effect_definitions", "item_registry", "data", "ground_types", "input_mode_manager", "math", "geometry_helpers", "collision_footprint_system", "physics_helpers", "placement_preview_system", "pin_attachment_system", "physics_shape_editor_system", "mechanical_system", "mechanical_shape_bridge", "constraint_system", "physics_world_system", "physics_projection_system", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_support", "item_update_policy", "fire_runtime_system", "robot_cleaner_system", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "update_step_pipeline_runner", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_helpers", "item_render_runtime", "burn_motion_util", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_nest_sleep_system", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_food_prototype_mixin", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_reset_presets", "world_family_social", "impact_core_system", "impact_response_system", "collision_response_system", "world_combat_effects", "world_environment", "world_temperature_system", "world_pathfinding_system", "world_grass_placement_system", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "system_order", "simulation", "colony_situation_system", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_pointer_action_system", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools" ]; const CORE_ASSETS = [ "./",