not baaad

This commit is contained in:
33333-33333 2026-06-25 22:30:35 +09:00
commit fbdac9ebf1
44 changed files with 2152 additions and 454 deletions

View file

@ -1,5 +1,5 @@
{
"version": "16.07.20",
"version": "16.07.32",
"css": [
"css/base.css",
"css/layout.css",
@ -24,6 +24,7 @@
"js/perf_profiler.js",
"js/render.js",
"js/sim_core.js",
"js/tarinai_seed_factory.js",
"js/structures.js",
"js/items.js",
"js/item_type_initializers.js",

View file

@ -351,34 +351,36 @@ h1 {
}
.tool {
--tool-main-icon-size: 30px;
--tool-main-icon-size: 38px;
--tool-icon-zoom: 1.12;
--tool-watermark-size: 0px;
border-radius: 12px;
display: grid;
grid-template-rows: 27px minmax(1.05em, auto);
align-items: start;
justify-items: center;
gap: 0;
padding: 3px 5px 4px;
min-height: 36px;
grid-template-columns: 42px minmax(0,1fr);
align-items: center;
justify-items: stretch;
gap: 4px;
padding: 4px 7px 4px 3px;
min-height: 48px;
font-size: 12px;
line-height: 1.08;
letter-spacing: -0.015em;
text-align: center;
text-align: left;
position: relative;
overflow: hidden;
background-repeat: no-repeat;
background-position: center 4px, center;
background-position: left 3px center, center;
background-size: var(--tool-main-icon-size) var(--tool-main-icon-size), cover;
}
.tool::before {
content: "";
position: absolute;
left: 50%;
top: 3px;
left: 4px;
top: 50%;
width: var(--tool-main-icon-size);
height: var(--tool-main-icon-size);
transform: translateX(-50%);
transform: translateY(-50%) scale(var(--tool-icon-zoom, 1));
transform-origin: center;
background: var(--tool-icon) center / contain no-repeat;
opacity: 0.98;
z-index: 1;
@ -397,8 +399,9 @@ h1 {
position: relative;
z-index: 2;
display: block;
grid-row: 2;
align-self: start;
grid-column: 2;
align-self: center;
justify-self: start;
max-width: 100%;
min-width: 0;
overflow-wrap: anywhere;
@ -408,22 +411,26 @@ h1 {
text-shadow: 0 1px 0 rgba(255,255,255,0.62);
}
.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-rows: 34px minmax(1.05em, auto); min-height: 44px; padding-top: 1px; }
.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-columns: 42px minmax(0,1fr); min-height: 48px; padding-top: 4px; }
.tool.tool-has-canvas-icon::before,
.tool.tool-has-canvas-icon::after { background: none !important; }
.tool-sprite-preview {
position: absolute;
left: 50%;
top: -5px;
width: 48px;
height: 48px;
transform: translateX(-50%);
left: 2px;
top: 50%;
width: 42px;
height: 42px;
transform: translateY(-50%);
pointer-events: none;
z-index: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool-sprite-watermark { display: none !important; }
.tool[data-tool="giant_drug"] .tool-sprite-preview { width: 56px; height: 56px; left: -5px; }
.tool[data-tool="dwarf_drug"] .tool-sprite-preview,
.tool[data-tool="oshibyo"] .tool-sprite-preview { width: 28px; height: 28px; left: 9px; }
.tool[data-tool="poke"], .tool[data-tool="water_hose"] { --tool-icon-zoom: 0.92; }
.tool[data-tool="observe"] { --tool-icon: url("../assets/ui/tool_observe.webp"); }
.tool[data-tool="zunchi"] { --tool-icon: url("../assets/ui/tool_zunchi.webp"); }
.tool[data-tool="sweet"] { --tool-icon: url("../assets/ui/tool_sweet.webp"); }

View file

@ -16,11 +16,21 @@
display: grid;
place-items: center;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
font-size: 29px;
font-size: 34px;
line-height: 1;
filter: drop-shadow(0 2px 3px rgba(52,40,26,0.12));
}
.tool.tool-emoji-icon::after { content: none; display: none; }
.tool[data-tool="poke"].tool-emoji-icon::before,
.tool[data-tool="water_hose"].tool-emoji-icon::before { font-size: 25px; }
.tool[data-tool="delete"] { --tool-icon-zoom: 1.42; }
.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; }
.tool[data-tool="dwarf_drug"],
.tool[data-tool="oshibyo"] { --tool-icon-zoom: 0.62; }
.tool.selected {
background: linear-gradient(180deg, #ffffff, #efffde);
border-color: rgba(103, 178, 44, 0.52);

View file

@ -7,11 +7,11 @@
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
<link rel="stylesheet" href="css/base.css?v=16.07.20" />
<link rel="stylesheet" href="css/layout.css?v=16.07.20" />
<link rel="stylesheet" href="css/panel.css?v=16.07.20" />
<link rel="stylesheet" href="css/components.css?v=16.07.20" />
<link rel="stylesheet" href="css/mobile.css?v=16.07.20" />
<link rel="stylesheet" href="css/base.css?v=16.07.32" />
<link rel="stylesheet" href="css/layout.css?v=16.07.32" />
<link rel="stylesheet" href="css/panel.css?v=16.07.32" />
<link rel="stylesheet" href="css/components.css?v=16.07.32" />
<link rel="stylesheet" href="css/mobile.css?v=16.07.32" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -84,7 +84,6 @@
<div class="panel-card-body">
<div class="tool-body">
<div id="toolPalette" class="tool-palette" aria-live="polite"></div>
<p class="hint">&#x540C;&#x3058;&#x9053;&#x5177;&#x30DC;&#x30BF;&#x30F3;&#x3092;&#x3082;&#x3046;&#x4E00;&#x5EA6;&#x62BC;&#x3059;&#x3068;&#x3001;&#x5BFE;&#x5FDC;&#x9053;&#x5177;&#x306E;&#x5927;&#x304D;&#x3055;&#x304C;&#x5C0F;&#x30FB;&#x4E2D;&#x30FB;&#x5927;&#x3067;&#x5207;&#x308A;&#x66FF;&#x308F;&#x308A;&#x307E;&#x3059;&#x3002;</p>
</div>
</div>
</section>
@ -98,7 +97,7 @@
<div class="stat"><span>状態</span><strong id="statColonyMood">安定</strong></div>
<div class="stat ground-stat" data-ground-toggle role="button" tabindex="0"><span>地面</span><button id="groundTypeBtn" class="ground-type-btn" type="button" data-ground-toggle aria-label="地面&#x3092;&#x304D;&#x308A;&#x304B;&#x3048;&#x308B;"><strong id="statGroundType">&#x571F;</strong></button></div>
</div>
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">個体数</button><button data-chart="life">欲求</button><button data-chart="objects"></button></div>
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">個体数</button><button data-chart="life">欲求</button><button data-chart="objects"></button></div>
<div id="colonyChartLegend" class="colony-chart-legend"></div>
<div id="colonyChart" class="colony-chart" aria-label="&#x30B3;&#x30ED;&#x30CB;&#x30FC;&#x306E;&#x63A8;&#x79FB;&#x30B0;&#x30E9;&#x30D5;" role="img"></div>
</div>
@ -171,7 +170,7 @@
<div id="archiveResetDialog" class="field-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="archiveResetDialogTitle">
<div class="field-dialog-panel archive-reset-dialog-panel">
<h2 id="archiveResetDialogTitle">&#x5BB6;&#x7CFB;&#x56F3;&#x3092;&#x30EA;&#x30BB;&#x30C3;&#x30C8;&#x3057;&#x307E;&#x3059;&#x304B;&#xFF1F;</h2>
<p class="hint">&#x73FE;&#x5728;&#x306E;&#x89AA;&#x5B50;&#x95A2;&#x4FC2;&#x3068;&#x4E16;&#x4EE3;&#x60C5;&#x5831;&#x3092;&#x6D88;&#x53BB;&#x3057;&#x307E;&#x3059;&#x3002;&#x500B;&#x4F53;&#x305D;&#x306E;物量&#x3084;&#x5EAD;&#x306E;&#x72B6;&#x614B;&#x306F;&#x6D88;&#x3048;&#x307E;&#x305B;&#x3093;&#x3002;</p>
<p class="hint">&#x73FE;&#x5728;&#x306E;&#x89AA;&#x5B50;&#x95A2;&#x4FC2;&#x3068;&#x4E16;&#x4EE3;&#x60C5;&#x5831;&#x3092;&#x6D88;&#x53BB;&#x3057;&#x307E;&#x3059;&#x3002;&#x500B;&#x4F53;&#x305D;&#x306E;&#x3082;&#x306E;&#x3084;&#x5EAD;&#x306E;&#x72B6;&#x614B;&#x306F;&#x6D88;&#x3048;&#x307E;&#x305B;&#x3093;&#x3002;</p>
<div class="field-dialog-actions archive-reset-actions">
<button id="archiveResetCancelBtn" class="btn" type="button">&#x30AD;&#x30E3;&#x30F3;&#x30BB;&#x30EB;</button>
<button id="archiveResetConfirmBtn" class="btn danger" type="button">&#x30EA;&#x30BB;&#x30C3;&#x30C8;&#x3059;&#x308B;</button>
@ -190,93 +189,94 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=16.07.20" defer></script>
<script src="js/event_bus.js?v=16.07.20" defer></script>
<script src="js/registry_base.js?v=16.07.20" defer></script>
<script src="js/disease_registry.js?v=16.07.20" defer></script>
<script src="js/sound_pack.js?v=16.07.20" defer></script>
<script src="js/item_registry.js?v=16.07.20" defer></script>
<script src="js/data.js?v=16.07.20" defer></script>
<script src="js/ground_types.js?v=16.07.20" defer></script>
<script src="js/input_mode_manager.js?v=16.07.20" defer></script>
<script src="js/math.js?v=16.07.20" defer></script>
<script src="js/physics_helpers.js?v=16.07.20" defer></script>
<script src="js/assets.js?v=16.07.20" defer></script>
<script src="js/audio.js?v=16.07.20" defer></script>
<script src="js/perf_profiler.js?v=16.07.20" defer></script>
<script src="js/render.js?v=16.07.20" defer></script>
<script src="js/sim_core.js?v=16.07.20" defer></script>
<script src="js/structures.js?v=16.07.20" defer></script>
<script src="js/items.js?v=16.07.20" defer></script>
<script src="js/item_type_initializers.js?v=16.07.20" defer></script>
<script src="js/item_lifecycle_runtime.js?v=16.07.20" defer></script>
<script src="js/structure_lifecycle.js?v=16.07.20" defer></script>
<script src="js/item_render_runtime.js?v=16.07.20" defer></script>
<script src="js/ants.js?v=16.07.20" defer></script>
<script src="js/health.js?v=16.07.20" defer></script>
<script src="js/tarinai.js?v=16.07.20" defer></script>
<script src="js/tarinai_action_spec.js?v=16.07.20" defer></script>
<script src="js/tarinai_behavior_state.js?v=16.07.20" defer></script>
<script src="js/tarinai_identity_social.js?v=16.07.20" defer></script>
<script src="js/tarinai_action_state.js?v=16.07.20" defer></script>
<script src="js/tarinai_disease_nest.js?v=16.07.20" defer></script>
<script src="js/tarinai_item_effects.js?v=16.07.20" defer></script>
<script src="js/tarinai_needs_core.js?v=16.07.20" defer></script>
<script src="js/tarinai_behavior_text.js?v=16.07.20" defer></script>
<script src="js/tarinai_forced_behavior.js?v=16.07.20" defer></script>
<script src="js/tarinai_item_targeting.js?v=16.07.20" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=16.07.20" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=16.07.20" defer></script>
<script src="js/tarinai_building_behavior.js?v=16.07.20" defer></script>
<script src="js/tarinai_action_definitions.js?v=16.07.20" defer></script>
<script src="js/tarinai_needs_items.js?v=16.07.20" defer></script>
<script src="js/tarinai_social_move_life.js?v=16.07.20" defer></script>
<script src="js/tarinai_render.js?v=16.07.20" defer></script>
<script src="js/world.js?v=16.07.20" defer></script>
<script src="js/world_view.js?v=16.07.20" defer></script>
<script src="js/family_graph.js?v=16.07.20" defer></script>
<script src="js/world_family_social.js?v=16.07.20" defer></script>
<script src="js/world_combat_effects.js?v=16.07.20" defer></script>
<script src="js/world_environment.js?v=16.07.20" defer></script>
<script src="js/world_spatial_budget.js?v=16.07.20" defer></script>
<script src="js/world_ants_system.js?v=16.07.20" defer></script>
<script src="js/weather_system.js?v=16.07.20" defer></script>
<script src="js/item_update_scheduler.js?v=16.07.20" defer></script>
<script src="js/world_update_phases.js?v=16.07.20" defer></script>
<script src="js/world_update.js?v=16.07.20" defer></script>
<script src="js/world_tool_actions.js?v=16.07.20" defer></script>
<script src="js/world_placement_log.js?v=16.07.20" defer></script>
<script src="js/world_event_effects.js?v=16.07.20" defer></script>
<script src="js/text_catalog.js?v=16.07.20" defer></script>
<script src="js/ui.js?v=16.07.20" defer></script>
<script src="js/ui_log.js?v=16.07.20" defer></script>
<script src="js/ui_helpers.js?v=16.07.20" defer></script>
<script src="js/ui_selected.js?v=16.07.20" defer></script>
<script src="js/snapshot_system.js?v=16.07.20" defer></script>
<script src="js/restore_coordinator.js?v=16.07.20" defer></script>
<script src="js/save_codec.js?v=16.07.20" defer></script>
<script src="js/save_storage.js?v=16.07.20" defer></script>
<script src="js/save_system.js?v=16.07.20" defer></script>
<script src="js/patch_helpers.js?v=16.07.20" defer></script>
<script src="js/freeze_store.js?v=16.07.20" defer></script>
<script src="js/freeze_snapshot_adapter.js?v=16.07.20" defer></script>
<script src="js/freeze_panel.js?v=16.07.20" defer></script>
<script src="js/freeze_system.js?v=16.07.20" defer></script>
<script src="js/ui_tooltips.js?v=16.07.20" defer></script>
<script src="js/ui_layout_dialogs.js?v=16.07.20" defer></script>
<script src="js/ui_ground.js?v=16.07.20" defer></script>
<script src="js/ui_tools.js?v=16.07.20" defer></script>
<script src="js/ui_input_shared.js?v=16.07.20" defer></script>
<script src="js/ui_input_touch.js?v=16.07.20" defer></script>
<script src="js/ui_input_mouse.js?v=16.07.20" defer></script>
<script src="js/ui_bind.js?v=16.07.20" defer></script>
<script src="js/ui_charts.js?v=16.07.20" defer></script>
<script src="js/ui_family_data.js?v=16.07.20" defer></script>
<script src="js/ui_family_async.js?v=16.07.20" defer></script>
<script src="js/ui_family_layout.js?v=16.07.20" defer></script>
<script src="js/ui_family_paths.js?v=16.07.20" defer></script>
<script src="js/ui_family_render.js?v=16.07.20" defer></script>
<script src="js/main.js?v=16.07.20" defer></script>
<script src="js/debug_tools.js?v=16.07.20" defer></script>
<script src="js/version.js?v=16.07.32" defer></script>
<script src="js/event_bus.js?v=16.07.32" defer></script>
<script src="js/registry_base.js?v=16.07.32" defer></script>
<script src="js/disease_registry.js?v=16.07.32" defer></script>
<script src="js/sound_pack.js?v=16.07.32" defer></script>
<script src="js/item_registry.js?v=16.07.32" defer></script>
<script src="js/data.js?v=16.07.32" defer></script>
<script src="js/ground_types.js?v=16.07.32" defer></script>
<script src="js/input_mode_manager.js?v=16.07.32" defer></script>
<script src="js/math.js?v=16.07.32" defer></script>
<script src="js/physics_helpers.js?v=16.07.32" defer></script>
<script src="js/assets.js?v=16.07.32" defer></script>
<script src="js/audio.js?v=16.07.32" defer></script>
<script src="js/perf_profiler.js?v=16.07.32" defer></script>
<script src="js/render.js?v=16.07.32" defer></script>
<script src="js/sim_core.js?v=16.07.32" defer></script>
<script src="js/tarinai_seed_factory.js?v=16.07.32" defer></script>
<script src="js/structures.js?v=16.07.32" defer></script>
<script src="js/items.js?v=16.07.32" defer></script>
<script src="js/item_type_initializers.js?v=16.07.32" defer></script>
<script src="js/item_lifecycle_runtime.js?v=16.07.32" defer></script>
<script src="js/structure_lifecycle.js?v=16.07.32" defer></script>
<script src="js/item_render_runtime.js?v=16.07.32" defer></script>
<script src="js/ants.js?v=16.07.32" defer></script>
<script src="js/health.js?v=16.07.32" defer></script>
<script src="js/tarinai.js?v=16.07.32" defer></script>
<script src="js/tarinai_action_spec.js?v=16.07.32" defer></script>
<script src="js/tarinai_behavior_state.js?v=16.07.32" defer></script>
<script src="js/tarinai_identity_social.js?v=16.07.32" defer></script>
<script src="js/tarinai_action_state.js?v=16.07.32" defer></script>
<script src="js/tarinai_disease_nest.js?v=16.07.32" defer></script>
<script src="js/tarinai_item_effects.js?v=16.07.32" defer></script>
<script src="js/tarinai_needs_core.js?v=16.07.32" defer></script>
<script src="js/tarinai_behavior_text.js?v=16.07.32" defer></script>
<script src="js/tarinai_forced_behavior.js?v=16.07.32" defer></script>
<script src="js/tarinai_item_targeting.js?v=16.07.32" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=16.07.32" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=16.07.32" defer></script>
<script src="js/tarinai_building_behavior.js?v=16.07.32" defer></script>
<script src="js/tarinai_action_definitions.js?v=16.07.32" defer></script>
<script src="js/tarinai_needs_items.js?v=16.07.32" defer></script>
<script src="js/tarinai_social_move_life.js?v=16.07.32" defer></script>
<script src="js/tarinai_render.js?v=16.07.32" defer></script>
<script src="js/world.js?v=16.07.32" defer></script>
<script src="js/world_view.js?v=16.07.32" defer></script>
<script src="js/family_graph.js?v=16.07.32" defer></script>
<script src="js/world_family_social.js?v=16.07.32" defer></script>
<script src="js/world_combat_effects.js?v=16.07.32" defer></script>
<script src="js/world_environment.js?v=16.07.32" defer></script>
<script src="js/world_spatial_budget.js?v=16.07.32" defer></script>
<script src="js/world_ants_system.js?v=16.07.32" defer></script>
<script src="js/weather_system.js?v=16.07.32" defer></script>
<script src="js/item_update_scheduler.js?v=16.07.32" defer></script>
<script src="js/world_update_phases.js?v=16.07.32" defer></script>
<script src="js/world_update.js?v=16.07.32" defer></script>
<script src="js/world_tool_actions.js?v=16.07.32" defer></script>
<script src="js/world_placement_log.js?v=16.07.32" defer></script>
<script src="js/world_event_effects.js?v=16.07.32" defer></script>
<script src="js/text_catalog.js?v=16.07.32" defer></script>
<script src="js/ui.js?v=16.07.32" defer></script>
<script src="js/ui_log.js?v=16.07.32" defer></script>
<script src="js/ui_helpers.js?v=16.07.32" defer></script>
<script src="js/ui_selected.js?v=16.07.32" defer></script>
<script src="js/snapshot_system.js?v=16.07.32" defer></script>
<script src="js/restore_coordinator.js?v=16.07.32" defer></script>
<script src="js/save_codec.js?v=16.07.32" defer></script>
<script src="js/save_storage.js?v=16.07.32" defer></script>
<script src="js/save_system.js?v=16.07.32" defer></script>
<script src="js/patch_helpers.js?v=16.07.32" defer></script>
<script src="js/freeze_store.js?v=16.07.32" defer></script>
<script src="js/freeze_snapshot_adapter.js?v=16.07.32" defer></script>
<script src="js/freeze_panel.js?v=16.07.32" defer></script>
<script src="js/freeze_system.js?v=16.07.32" defer></script>
<script src="js/ui_tooltips.js?v=16.07.32" defer></script>
<script src="js/ui_layout_dialogs.js?v=16.07.32" defer></script>
<script src="js/ui_ground.js?v=16.07.32" defer></script>
<script src="js/ui_tools.js?v=16.07.32" defer></script>
<script src="js/ui_input_shared.js?v=16.07.32" defer></script>
<script src="js/ui_input_touch.js?v=16.07.32" defer></script>
<script src="js/ui_input_mouse.js?v=16.07.32" defer></script>
<script src="js/ui_bind.js?v=16.07.32" defer></script>
<script src="js/ui_charts.js?v=16.07.32" defer></script>
<script src="js/ui_family_data.js?v=16.07.32" defer></script>
<script src="js/ui_family_async.js?v=16.07.32" defer></script>
<script src="js/ui_family_layout.js?v=16.07.32" defer></script>
<script src="js/ui_family_paths.js?v=16.07.32" defer></script>
<script src="js/ui_family_render.js?v=16.07.32" defer></script>
<script src="js/main.js?v=16.07.32" defer></script>
<script src="js/debug_tools.js?v=16.07.32" defer></script>
</body>
</html>

View file

@ -26,7 +26,7 @@ const SPRITES = [
{ id: "stress_dizzy", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u3050\u308b\u3050\u308b", path: "assets/sprites/tarinai_24_stress_dizzy.webp", actionOnly: true },
{ id: "stress_sweat", label: "\u9ad8\u30b9\u30c8\u30ec\u30b9\u305f\u308a\u306a\u3044\u30fb\u6c57", path: "assets/sprites/tarinai_25_stress_sweat.webp", actionOnly: true },
{ id: "intimidate", label: "\u5a01\u5687\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_26_intimidate.webp", actionOnly: true },
{ id: "birth_ritual", label: "\u7e41\u6b96\u524d\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true },
{ id: "birth_ritual", label: "へこへこたりない", path: "assets/sprites/tarinai_27_birth_ritual.webp", actionOnly: true },
{ id: "zunchi_slave", label: "\u305a\u3093\u3061\u3069\u308c\u3044", path: "assets/sprites/tarinai_28_zunchi_slave.webp", displayOnly: true },
{ id: "hungry_70", label: "\u7a7a\u817970\u4ee5\u4e0a\u305f\u308a\u306a\u3044", path: "assets/sprites/tarinai_29_hungry_70.webp", displayOnly: true },
{ id: "zunchi_slave_alt", label: "\u305a\u3093\u3061\u3069\u308c\u3044 2", path: "assets/sprites/tarinai_30_zunchi_slave_alt.webp", displayOnly: true },

View file

@ -1,11 +1,12 @@
"use strict";
const GROUND_TYPE_ORDER = Object.freeze(["soil", "concrete", "blanket", "foot_massage"]);
const GROUND_TYPE_ORDER = Object.freeze(["soil", "concrete", "blanket", "foot_massage", "ice"]);
const GROUND_TYPES = Object.freeze({
soil: Object.freeze({ id: "soil", label: "土", description: "いつものじめん", stressMultiplier: 1.0, grassMultiplier: 1.0 }),
concrete: Object.freeze({ id: "concrete", label: "コンクリート", description: "草が生えない", stressMultiplier: 1.0, grassMultiplier: 0.0, grassLimit: 0 }),
blanket: Object.freeze({ id: "blanket", label: "ふわふわ毛布", description: "ストレスが増えにくい", stressMultiplier: 0.5, grassMultiplier: 1.0 }),
foot_massage: Object.freeze({ id: "foot_massage", label: "足つぼ", description: "青いまだら地面。ストレスが増えやすい", stressMultiplier: 1.5, grassMultiplier: 1.0 }),
ice: Object.freeze({ id: "ice", label: "氷", description: "つるつる滑る。草は生えにくい", stressMultiplier: 1.1, grassMultiplier: 0.15 }),
});
function groundDefinition(id = "soil") {

View file

@ -5,13 +5,14 @@
const DUPLICATOR_STORABLE_TYPES = new Set([
"food", "sweet", "love_mochi", "fight_mochi", "sleep_drug",
"laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug",
"zunda_juice", "grass", "water"
"zunda_juice", "grass", "water", "pushpin", "oshibyo"
]);
function duplicatorLoadTypeForItem(item) {
if (!item || item.dead || item.type === "duplicator") return "";
const type = String(item.type || "");
if (!type || type === "water_bowl") return "";
if ((typeof isPinType === "function" && isPinType(type)) && item.pinState === "lodged") return "";
// Explicit list first. これで「ずんだ餅」「へこ餅」「けんか餅」が
// serving-food 判定や medicine role の揺れに左右されず複製機へ入る。
if (DUPLICATOR_STORABLE_TYPES.has(type)) return type;
@ -24,6 +25,91 @@ function duplicatorLoadTypeForItem(item) {
}
function itemAngleForRuntime(item) {
return typeof itemAngleFor === "function" ? itemAngleFor(item) : (Number(item?.angle) || 0);
}
function fanAffectsPoint(fan, x, y) {
const angle = itemAngleForRuntime(fan);
const ux = Math.cos(angle);
const uy = Math.sin(angle);
const dx = x - (fan.x || 0);
const dy = y - (fan.y || 0);
const forward = dx * ux + dy * uy;
const side = -dx * uy + dy * ux;
const range = Math.max(170, (fan.r || 32) * 7.2);
const width = Math.max(42, (fan.r || 32) * (1.0 + Math.max(0, forward) / Math.max(1, range) * 3.1));
if (forward < -(fan.r || 32) * 0.3 || forward > range || Math.abs(side) > width) return null;
const forwardFalloff = 1 - clamp(forward / range, 0, 1);
const sideFalloff = 1 - clamp(Math.abs(side) / width, 0, 1);
const strength = forwardFalloff * forwardFalloff * (0.25 + sideFalloff * 0.75);
return strength > 0.04 ? { ux, uy, strength, range, forward, side } : null;
}
function fanApplyToBody(fan, body, dt, worldRef, scale = 1) {
if (!fan || !body || body.dead || body === fan || body.playerHeld || body._heldByPlayer) return false;
const hit = fanAffectsPoint(fan, body.x || 0, body.y || 0);
if (!hit) return false;
const force = Math.max(120, (fan.r || 32) * 5.6) * hit.strength * scale;
body.vx = clamp((body.vx || 0) + hit.ux * force * dt, -720, 720);
body.vy = clamp((body.vy || 0) + hit.uy * force * dt, -720, 720);
if ((typeof Tarinai !== "undefined" && body instanceof Tarinai) || body.radius) {
body.impulseVx = clamp((body.impulseVx || 0) + hit.ux * force * dt * 0.42, -420, 420);
body.impulseVy = clamp((body.impulseVy || 0) + hit.uy * force * dt * 0.42, -420, 420);
if ((worldRef?.time || 0) >= (body.lastFanThoughtAt || -999) + 1.4) {
body.lastFanThoughtAt = worldRef?.time || 0;
body.thought = "風に流されている。";
body.pinchThoughtTimer = Math.max(body.pinchThoughtTimer || 0, 1.1);
}
} else {
body.prevX = Number.isFinite(body.prevX) ? body.prevX : body.x;
body.prevY = Number.isFinite(body.prevY) ? body.prevY : body.y;
if (Number.isFinite(body.spinVelocity)) body.spinVelocity = clamp((body.spinVelocity || 0) + hit.side * 0.012 * force * dt, -48, 48);
}
return true;
}
function isMagnetTargetItem(item) {
if (!item || item.dead) return false;
return item.type === "pushpin" || item.type === "oshibyo" || item.type === "ball";
}
function magnetPolePoint(magnet) {
const angle = itemAngleForRuntime(magnet);
return { x: (magnet.x || 0) + Math.cos(angle) * (magnet.r || 30) * 1.10, y: (magnet.y || 0) + Math.sin(angle) * (magnet.r || 30) * 1.10 };
}
function magnetApplyToBody(magnet, body, dt, worldRef, scale = 1) {
if (!magnet || !body || body.dead || body === magnet || body.playerHeld || body._heldByPlayer) return false;
const pole = magnetPolePoint(magnet);
const dx = pole.x - (body.x || 0);
const dy = pole.y - (body.y || 0);
const d = Math.max(1, Math.hypot(dx, dy));
const range = Math.max(150, (magnet.r || 30) * 7.2);
if (d > range) return false;
const falloff = Math.pow(1 - d / range, 1.35);
const force = Math.max(180, (magnet.r || 30) * 7.8) * falloff * scale;
const nx = dx / d;
const ny = dy / d;
body.vx = clamp((body.vx || 0) + nx * force * dt, -740, 740);
body.vy = clamp((body.vy || 0) + ny * force * dt, -740, 740);
if ((typeof Tarinai !== "undefined" && body instanceof Tarinai) || body.radius) {
body.impulseVx = clamp((body.impulseVx || 0) + nx * force * dt * 0.35, -420, 420);
body.impulseVy = clamp((body.impulseVy || 0) + ny * force * dt * 0.35, -420, 420);
if ((worldRef?.time || 0) >= (body.lastMagnetThoughtAt || -999) + 1.8) {
body.lastMagnetThoughtAt = worldRef?.time || 0;
body.thought = "磁石に引っぱられている。";
body.pinchThoughtTimer = Math.max(body.pinchThoughtTimer || 0, 1.1);
}
} else {
body.prevX = Number.isFinite(body.prevX) ? body.prevX : body.x;
body.prevY = Number.isFinite(body.prevY) ? body.prevY : body.y;
if (Number.isFinite(body.spinVelocity)) body.spinVelocity = clamp((body.spinVelocity || 0) + (nx - ny) * force * dt * 0.08, -54, 54);
}
return true;
}
const DUPLICATOR_LOAD_DWELL_SECONDS = 0.38;
const DUPLICATOR_REPLACE_DWELL_SECONDS = 0.70;
@ -48,9 +134,93 @@ function duplicatorCanLoadNow(duplicator, worldRef) {
function syncDuplicatorRoles(item) {
if (!item || item.type !== "duplicator") return;
const type = String(item.storedFoodType || "");
item.roles.food = Boolean(type);
item.roles.medicine = Boolean(type && (type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))));
item.roles.drink = Boolean(type === "water" || type === "zunda_juice");
const pinLoaded = Boolean(type && typeof isPinType === "function" && isPinType(type));
item.roles.food = Boolean(type && !pinLoaded);
item.roles.medicine = Boolean(type && !pinLoaded && (type === "sweet" || type === "water" || type === "zunda_juice" || type === "sleep_drug" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type))));
item.roles.drink = Boolean(!pinLoaded && (type === "water" || type === "zunda_juice"));
item.roles.danger = pinLoaded;
}
function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, options = {}) {
if (!duplicator || duplicator.type !== "duplicator") return false;
const type = String(storedType || "");
if (!type) return false;
const probe = { type, amount: 1, foodServingsRemaining: 1, dead: false };
const loadType = duplicatorLoadTypeForItem(probe);
if (!loadType) return false;
const changed = duplicator.storedFoodType !== loadType;
if (!changed && duplicator.storedFoodType && !options.force) return true;
const def = typeof itemDefinition === "function" ? itemDefinition(loadType) : null;
const newLabel = def?.label || (typeof toolLabel === "function" ? toolLabel(loadType) : loadType);
duplicator.storedFoodType = loadType;
duplicator.storedFoodLabel = newLabel;
syncDuplicatorRoles(duplicator);
duplicator.loadedAt = Number(worldRef?.time || 0);
duplicator._duplicatorCandidateKey = "";
duplicator._duplicatorCandidateSince = 0;
duplicator.duplicatorPinFlash = typeof isPinType === "function" && isPinType(loadType) ? 0.28 : (duplicator.duplicatorPinFlash || 0);
const consume = options.consumeItem;
if (consume && typeof consume === "object") {
consume.amount = 0;
consume.foodServingsRemaining = 0;
}
worldRef?.markItemBucketsDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
worldRef?.markSpatialDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
worldRef?.markTerrainDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
if (options.emit !== false) worldRef?.emit?.("duplicator:loaded", { duplicator, foodType: loadType, replaced: changed, direct: Boolean(options.direct) });
if (options.log !== false) worldRef?.log?.(`複製機に${duplicator.storedFoodLabel}をセットした。`, typeof isPinType === "function" && isPinType(loadType) ? "observe" : "food");
return true;
}
if (typeof window !== "undefined") {
window.TarinaiDuplicatorRuntime = Object.freeze({
loadTypeForItem: duplicatorLoadTypeForItem,
loadPoint: duplicatorLoadPoint,
setStoredType: duplicatorSetStoredType,
syncRoles: syncDuplicatorRoles,
});
}
function duplicatorApplyStoredPin(duplicator, worldRef) {
const storedType = String(duplicator?.storedFoodType || "");
if (!duplicator || duplicator.type !== "duplicator" || !(typeof isPinType === "function" && isPinType(storedType))) return false;
const now = Number(worldRef?.time || 0);
if (now < Number(duplicator.nextDuplicatedPinAt || 0)) return false;
const dish = duplicatorLoadPoint(duplicator);
const reach = Math.max(24, (duplicator.r || 34) * 0.56);
const candidates = worldRef?.nearbyTarinai?.(dish.x, dish.y, reach + 72, true) || [];
for (const t of candidates) {
if (!t || t.dead || worldRef?.isTarinaiHiddenInNestBox?.(t)) continue;
if (t.stuckPushpinId || t.currentLodgedPin?.()) continue;
const hit = reach + Math.max(10, (t.radius || 18) * 0.78);
if (distXY(dish.x, dish.y, t.x, t.y) > hit) continue;
const pin = new Item(storedType, dish.x, dish.y);
pin.amount = 999;
pin.noStickUntil = 0;
pin.noStickTargetId = "";
pin.noStickTargetUntil = 0;
pin.duplicatedFromId = duplicator.id || "";
const added = worldRef?.addItem?.(pin, "duplicator-pin", { terrain: false }) || null;
if (!added && worldRef?.items) {
pin.world = worldRef;
worldRef.items.push(pin);
worldRef.markItemBucketsDirty?.("duplicator-pin");
worldRef.markSpatialDirty?.("duplicator-pin");
}
const ok = pin.attachPushpin?.(t, worldRef, distXY(dish.x, dish.y, t.x, t.y));
if (!ok) {
pin.amount = 0;
continue;
}
duplicator.usedCount = (duplicator.usedCount || 0) + 1;
duplicator.nextDuplicatedPinAt = now + 0.82;
duplicator.duplicatorPinFlash = 0.24;
worldRef.effects?.push(new Effect("ring", dish.x, dish.y, { size: Math.max(15, (duplicator.r || 34) * 0.62), life: 0.18, color: storedType === "oshibyo" ? "rgba(73,119,205,0.40)" : "rgba(214,72,72,0.42)" }));
worldRef.drawListDirty = true;
worldRef.markSpatialDirty?.("duplicator-pin-lodged");
return true;
}
return false;
}
Object.assign(Item.prototype, {
@ -118,6 +288,9 @@ update(dt, worldRef = world) {
}
}
if (this.type === "ball") this.updateBall(dt, worldRef);
if (this.type === "fan") this.updateFan(dt, worldRef);
if (this.type === "magnet") this.updateMagnet(dt, worldRef);
if (this.type === "gate_fence") this.amount = 999;
if (this.type === "duplicator") this.updateDuplicator(dt, worldRef);
if (isPinType(this.type)) this.updatePushpin(dt, worldRef);
if (this.type === "zunchi") this.updateZunchiMotion(dt, worldRef);
@ -136,9 +309,70 @@ update(dt, worldRef = world) {
}
},
updateFan(dt, worldRef) {
this.amount = 999;
if (!worldRef) return;
let changed = false;
const range = Math.max(190, (this.r || 32) * 7.4);
for (const t of worldRef.nearbyTarinai?.(this.x, this.y, range + 80, true) || []) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
changed = fanApplyToBody(this, t, dt, worldRef, 1.0) || changed;
}
const kinematicTypes = new Set(["ball", "zunchi", "pushpin", "oshibyo"]);
for (const it of worldRef.nearbyItems?.(this.x, this.y, range + 80, true) || []) {
if (!it || it === this || it.dead || !kinematicTypes.has(it.type)) continue;
changed = fanApplyToBody(this, it, dt, worldRef, it.type === "ball" ? 1.12 : 0.82) || changed;
}
const now = worldRef.time || 0;
if (changed && (this.fanPulseAt || -999) + 0.12 <= now) {
this.fanPulseAt = now;
const angle = itemAngleForRuntime(this);
const x = this.x + Math.cos(angle) * (this.r || 32) * rand(0.8, 2.8);
const y = this.y + Math.sin(angle) * (this.r || 32) * rand(0.8, 2.8) + rand(-10, 10);
worldRef.effects?.push(new Effect("wind", x, y, { vx: Math.cos(angle) * rand(16, 40), vy: Math.sin(angle) * rand(16, 40), size: rand(8, 18), life: rand(0.18, 0.32), color: "rgba(160,210,245,0.28)" }));
}
if (changed) {
worldRef.drawListDirty = true;
worldRef.markSpatialDirty?.("fan-force");
}
},
updateMagnet(dt, worldRef) {
this.amount = 999;
if (!worldRef) return;
let changed = false;
const range = Math.max(170, (this.r || 30) * 7.4);
for (const it of worldRef.nearbyItems?.(this.x, this.y, range + 80, true) || []) {
if (!isMagnetTargetItem(it)) continue;
if (it.type === "pushpin" || it.type === "oshibyo") {
if (it.pinState === "lodged") continue;
}
changed = magnetApplyToBody(this, it, dt, worldRef, it.type === "ball" ? 0.42 : 1.0) || changed;
}
for (const t of worldRef.nearbyTarinai?.(this.x, this.y, range + 80, true) || []) {
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
const lodged = t.currentLodgedPin?.() || (worldRef.items || []).find(it => it && isPinType(it.type) && it.pinState === "lodged" && it.pinTargetId === t.id);
if (!lodged) continue;
changed = magnetApplyToBody(this, t, dt, worldRef, 0.55) || changed;
}
const now = worldRef.time || 0;
if (changed && (this.magnetPulseAt || -999) + 0.18 <= now) {
this.magnetPulseAt = now;
const pole = magnetPolePoint(this);
worldRef.effects?.push(new Effect("ring", pole.x, pole.y, { size: Math.max(12, (this.r || 30) * 0.85), life: 0.16, color: "rgba(120,150,230,0.34)" }));
}
if (changed) {
worldRef.drawListDirty = true;
worldRef.markSpatialDirty?.("magnet-force");
}
},
updateDuplicator(dt, worldRef) {
this.amount = 999;
syncDuplicatorRoles(this);
duplicatorApplyStoredPin(this, worldRef);
this.duplicatorPinFlash = Math.max(0, (this.duplicatorPinFlash || 0) - dt);
if (!worldRef?.nearbyItems) return;
if (!duplicatorCanLoadNow(this, worldRef)) {
this._duplicatorCandidateKey = "";
@ -204,22 +438,13 @@ updateDuplicator(dt, worldRef) {
return;
}
const def = typeof itemDefinition === "function" ? itemDefinition(bestType) : null;
const newLabel = def?.label || (typeof toolLabel === "function" ? toolLabel(bestType) : bestType);
this.storedFoodType = bestType;
this.storedFoodLabel = newLabel;
syncDuplicatorRoles(this);
this.loadedAt = now;
this._duplicatorCandidateKey = "";
this._duplicatorCandidateSince = 0;
best.amount = 0;
best.foodServingsRemaining = 0;
duplicatorSetStoredType(this, bestType, worldRef, { consumeItem: best, direct: false, force: false, log: false, emit: false });
// Item.dead is a getter based on amount. Do not assign to it.
worldRef.markItemBucketsDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded");
worldRef.markSpatialDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded");
worldRef.markTerrainDirty?.(changed ? "duplicator-reloaded" : "duplicator-loaded");
worldRef.emit?.("duplicator:loaded", { duplicator: this, foodType: this.storedFoodType, replaced: changed, slotDistance: bestSlotD });
worldRef.log?.(`複製機に${this.storedFoodLabel}をセットした。`, "food");
worldRef.log?.(`複製機に${this.storedFoodLabel}をセットした。`, typeof isPinType === "function" && isPinType(this.storedFoodType) ? "observe" : "food");
},
updateZunchiMotion(dt, worldRef) {
@ -390,6 +615,17 @@ resolveBallCircleBounce(obstacle, hitRadius, restitution, worldRef) {
resolveBallRectBounce(rect, worldRef, source = null) {
if (!rect) return;
const hitRadius = (this.r || 18) + 2;
const restitution = Number.isFinite(rect.restitution) ? rect.restitution : 0.78;
const velocityDamp = rect.bounce ? 1.0 : 0.94;
const minBounceSpeed = Math.max(0, Number(rect.minBounceSpeed || 0) || 0);
const ensureMinNormalSpeed = (nx, ny) => {
if (!(rect.bounce && minBounceSpeed > 0)) return;
const normalSpeed = (this.vx || 0) * nx + (this.vy || 0) * ny;
if (normalSpeed >= minBounceSpeed) return;
const add = minBounceSpeed - normalSpeed;
this.vx = (this.vx || 0) + nx * add;
this.vy = (this.vy || 0) + ny * add;
};
const cx = clamp(this.x, rect.left, rect.right);
const cy = clamp(this.y, rect.top, rect.bottom);
let dx = this.x - cx;
@ -428,14 +664,15 @@ resolveBallRectBounce(rect, worldRef, source = null) {
if (nx) this.y = clamp(this.y, expanded.top, expanded.bottom);
if (ny) this.x = clamp(this.x, expanded.left, expanded.right);
if (toward < 0) {
this.vx = vx - 1.78 * toward * nx;
this.vy = vy - 1.78 * toward * ny;
this.vx = vx - (1 + restitution) * toward * nx;
this.vy = vy - (1 + restitution) * toward * ny;
} else {
this.vx = vx + nx * 18;
this.vy = vy + ny * 18;
}
this.vx *= 0.94;
this.vy *= 0.94;
ensureMinNormalSpeed(nx, ny);
this.vx *= velocityDamp;
this.vy *= velocityDamp;
this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38);
this.emitBallBounce(worldRef, source || rect.item || rect);
return;
@ -458,14 +695,15 @@ resolveBallRectBounce(rect, worldRef, source = null) {
this.x = cx + nx * (hitRadius + 0.5);
this.y = cy + ny * (hitRadius + 0.5);
if (toward < 0) {
this.vx = (this.vx || 0) - 1.78 * toward * nx;
this.vy = (this.vy || 0) - 1.78 * toward * ny;
this.vx = (this.vx || 0) - (1 + restitution) * toward * nx;
this.vy = (this.vy || 0) - (1 + restitution) * toward * ny;
} else {
this.vx = (this.vx || 0) + nx * 18;
this.vy = (this.vy || 0) + ny * 18;
}
this.vx *= 0.94;
this.vy *= 0.94;
ensureMinNormalSpeed(nx, ny);
this.vx *= velocityDamp;
this.vy *= velocityDamp;
this.spinVelocity = clamp((this.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38);
this.emitBallBounce(worldRef, source || rect.item || rect);
},
@ -474,7 +712,8 @@ emitBallBounce(worldRef, obstacle) {
const now = worldRef?.time || 0;
if ((this.lastObstacleBounceAt || -999) + 0.08 > now) return;
this.lastObstacleBounceAt = now;
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color: obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)" }));
const color = (obstacle?.type === "bounce_fence" || obstacle?.type === "bounce_fence_v") ? "rgba(82,153,230,0.46)" : (obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)");
worldRef?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(13, (this.r || 18) * 0.82), life: 0.18, color }));
},
updatePushpin(dt, worldRef) {

View file

@ -32,12 +32,17 @@ const TOOL_DEFINITIONS = Object.freeze({
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u89b3\u5bdf\u30c4\u30fc\u30eb\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u3084\u85ac\u3092\u30bb\u30c3\u30c8\u3059\u308b\u3068\u3001\u81ea\u52d5\u4f9b\u7d66\u3057\u3066\u304f\u308c\u308b\u3002" },
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u30fb\u85ac\u30fb\u753b\u92f2\u30fb\u304a\u3057\u308a\u92f2\u3092\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002\u92f2\u7cfb\u306f\u89e6\u308c\u305f\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3002" },
firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u7e26\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" },
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u6a2a\u306e\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u3002" },
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u89b3\u5bdf\u30e2\u30fc\u30c9\u3067\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
fan: { id: "fan", itemType: "fan", label: "\u6247\u98a8\u6a5f", placeable: true, scalable: true, radius: 32, amount: 999, collisionShape: "oriented_rect", tooltip: "\u524d\u65b9\u306b\u98a8\u3092\u51fa\u3057\u3001\u305f\u308a\u306a\u3044\u3084\u7269\u3092\u62bc\u3059\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
magnet: { id: "magnet", itemType: "magnet", label: "\u78c1\u77f3", placeable: true, scalable: true, radius: 30, amount: 999, collisionShape: "oriented_rect", tooltip: "\u524d\u65b9\u306e\u6975\u306b\u753b\u92f2\u30fb\u304a\u3057\u308a\u92f2\u30fb\u30dc\u30fc\u30eb\u3092\u5f15\u304d\u5bc4\u305b\u308b\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
water: { id: "water", itemType: "water", label: "水滴", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "💧", tooltip: "雨の日にも現れる水滴。たりないが飲める。複製機にもセットできる。" },
trace: { id: "trace", itemType: "trace", label: "\u8db3\u8de1", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true },
@ -47,15 +52,37 @@ const TOOL_DEFINITIONS = Object.freeze({
const ITEM_TRAITS = Object.freeze({
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h"]),
obstacle: new Set(["stone", "ball", "nest_box", "bed", "duplicator", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet"]),
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "sleep_drug"]),
hazard: new Set(["firecracker", "genkotsu", "pushpin", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
hazard: new Set(["firecracker", "genkotsu", "pushpin", "oshibyo", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
pin: new Set(["pushpin", "oshibyo"]),
kinematic: new Set(["ball", "pushpin", "oshibyo", "zunchi"]),
sleepFurniture: new Set(["bed", "nest_box"]),
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "pushpin", "oshibyo"]),
draggable: new Set(["ball", "stone", "bed", "nest_box", "signboard", "duplicator", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet"]),
});
const ROTATABLE_ITEM_TYPES = Object.freeze(new Set(["fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet"]));
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
function defaultItemAngle(type = "") {
const t = String(type || "");
if (t === "fence_v" || t === "bounce_fence_v") return Math.PI / 2;
return 0;
}
function normalizedItemAngle(angle = 0, fallback = 0) {
const twoPi = Math.PI * 2;
const n = Number(angle);
const base = Number.isFinite(n) ? n : (Number.isFinite(Number(fallback)) ? Number(fallback) : 0);
return ((base % twoPi) + twoPi) % twoPi;
}
function itemAngleFor(itemOrType = "") {
if (itemOrType && typeof itemOrType === "object") return normalizedItemAngle(itemOrType.angle, defaultItemAngle(itemOrType.type));
return defaultItemAngle(String(itemOrType || ""));
}
function rotatableToolIds() {
return Object.values(TOOL_DEFINITIONS).filter(def => isRotatableItemType(def.itemType || def.id)).map(def => def.id);
}
function itemHasTrait(type = "", trait = "") {
return Boolean(ITEM_TRAITS[trait]?.has?.(String(type || "")));
}
@ -91,7 +118,7 @@ const TOOL_CATEGORIES = Object.freeze([
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "delete", "poke", "pinch", "new", "water_hose"] },
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"] },
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ball", "signboard", "fence_v", "fence_h"] },
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "bed", "nest_box", "ball", "signboard", "fence_h", "bounce_fence", "gate_fence", "fan", "magnet"] },
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "pushpin", "oshibyo", "ant_nest"] },
]);
@ -679,3 +706,11 @@ function toolTipsFromDefinitions() {
pinBehavior: pinBehaviorFor,
});
})(typeof window !== "undefined" ? window : globalThis);
if (typeof window !== "undefined") {
window.isRotatableItemType = isRotatableItemType;
window.defaultItemAngle = defaultItemAngle;
window.normalizedItemAngle = normalizedItemAngle;
window.itemAngleFor = itemAngleFor;
window.rotatableToolIds = rotatableToolIds;
}

View file

@ -2,6 +2,61 @@
// Item field rendering. Kept separate from construction and lifecycle logic.
function drawFanInfluenceLocal(ctx, r = 32, styleNight = false) {
const range = Math.max(170, r * 7.2);
const nearW = Math.max(42, r * 1.0);
const farW = Math.max(42, r * 4.1);
ctx.save();
ctx.globalAlpha *= styleNight ? 0.18 : 0.22;
ctx.fillStyle = styleNight ? "rgba(120,190,235,0.22)" : "rgba(110,190,240,0.24)";
ctx.strokeStyle = styleNight ? "rgba(160,220,255,0.48)" : "rgba(66,150,215,0.52)";
ctx.lineWidth = 1.4;
ctx.setLineDash([9, 7]);
ctx.beginPath();
ctx.moveTo(0, -nearW * 0.55);
ctx.lineTo(range, -farW * 0.5);
ctx.quadraticCurveTo(range + Math.max(10, farW * 0.08), 0, range, farW * 0.5);
ctx.lineTo(0, nearW * 0.55);
ctx.quadraticCurveTo(-Math.max(4, nearW * 0.10), 0, 0, -nearW * 0.55);
ctx.closePath();
ctx.fill();
ctx.globalAlpha *= 1.9;
ctx.stroke();
ctx.setLineDash([3, 8]);
for (const yy of [-0.34, 0, 0.34]) {
ctx.beginPath();
ctx.moveTo(Math.max(12, nearW * 0.35), yy * nearW);
ctx.quadraticCurveTo(range * 0.46, yy * farW * 0.35, range * 0.92, yy * farW);
ctx.stroke();
}
ctx.restore();
}
function drawMagnetInfluenceLocal(ctx, r = 30, styleNight = false) {
const range = Math.max(150, r * 7.2);
const poleX = r * 1.10;
ctx.save();
ctx.translate(poleX, 0);
ctx.globalAlpha *= styleNight ? 0.15 : 0.18;
ctx.fillStyle = styleNight ? "rgba(138,126,232,0.20)" : "rgba(132,118,232,0.20)";
ctx.strokeStyle = styleNight ? "rgba(174,170,255,0.48)" : "rgba(86,88,205,0.50)";
ctx.lineWidth = 1.4;
ctx.setLineDash([10, 7]);
ctx.beginPath();
ctx.arc(0, 0, range, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha *= 2.0;
ctx.stroke();
ctx.setLineDash([3, 9]);
for (const scale of [0.38, 0.68]) {
ctx.beginPath();
ctx.arc(0, 0, range * scale, -Math.PI * 0.72, Math.PI * 0.72);
ctx.stroke();
}
ctx.restore();
}
Object.assign(Item.prototype, {
draw(ctx, t, lighting = null) {
const lightState = lighting || getLightingState(world);
@ -469,26 +524,166 @@ draw(ctx, t, lighting = null) {
ctx.font = "bold 10px ui-rounded, sans-serif";
ctx.textAlign = "center";
ctx.fillText(String(Math.ceil(this.fuseTimer ?? 5)), 0, this.r * 1.42);
} else if (this.type === "fence_v" || this.type === "fence_h") {
const vertical = this.type === "fence_v";
} else if (this.type === "fence_v" || this.type === "fence_h" || this.type === "bounce_fence" || this.type === "bounce_fence_v" || this.type === "gate_fence") {
const bounce = this.type === "bounce_fence" || this.type === "bounce_fence_v";
const gate = this.type === "gate_fence";
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
const len = Math.max(112, this.r * 3.55);
const thick = Math.max(10, this.r * 0.31);
const open = gate && this.gateOpen;
ctx.save();
ctx.rotate(vertical ? 0 : Math.PI / 2);
const wood = styleNight ? "#7a5b3c" : (styleWarm ? "#a96f35" : "#965f31");
const edge = styleNight ? "#4d3d2f" : "#5f3f25";
const hi = styleNight ? "rgba(198,166,116,0.20)" : "rgba(232,176,94,0.32)";
ctx.fillStyle = wood;
ctx.strokeStyle = edge;
ctx.lineWidth = 2.4;
ctx.rotate(angle);
const wood = bounce ? (styleNight ? "#4e6f9f" : "#5aa2e0") : gate ? (styleNight ? "#5d675f" : "#6f8b68") : (styleNight ? "#7a5b3c" : (styleWarm ? "#a96f35" : "#965f31"));
const edge = bounce ? (styleNight ? "#243755" : "#286093") : gate ? (styleNight ? "#334038" : "#375737") : (styleNight ? "#4d3d2f" : "#5f3f25");
const hi = bounce ? (styleNight ? "rgba(190,230,255,0.24)" : "rgba(235,252,255,0.54)") : gate ? (styleNight ? "rgba(214,240,210,0.18)" : "rgba(220,255,210,0.34)") : (styleNight ? "rgba(198,166,116,0.20)" : "rgba(232,176,94,0.32)");
const drawRail = (x0, y0, w, h, alpha = 1) => {
ctx.save();
ctx.globalAlpha *= alpha;
ctx.fillStyle = wood;
ctx.strokeStyle = edge;
ctx.lineWidth = bounce ? 2.8 : 2.4;
ctx.beginPath();
roundedRect(ctx, x0, y0, w, h, thick / 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = hi;
ctx.beginPath();
roundedRect(ctx, x0 + 6, y0 + thick * 0.25, Math.max(4, w - 12), thick * 0.20, thick / 3);
ctx.fill();
ctx.restore();
};
if (gate) {
const pivotX = -len / 2;
ctx.save();
drawRail(pivotX, -thick / 2, len, thick, open ? 0.34 : 1);
ctx.strokeStyle = styleNight ? "rgba(210,230,210,0.55)" : "rgba(255,255,255,0.54)";
ctx.lineWidth = Math.max(1.6, thick * 0.12);
for (let x = pivotX + len * 0.26; x < pivotX + len * 0.88; x += len * 0.24) {
ctx.beginPath();
ctx.moveTo(x, -thick * 0.42);
ctx.lineTo(x, thick * 0.42);
ctx.stroke();
}
ctx.restore();
ctx.fillStyle = styleNight ? "#c7d7bf" : "#e8f2d8";
ctx.strokeStyle = edge;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(pivotX, 0, thick * 0.54, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
if (open) {
ctx.fillStyle = "rgba(80,120,72,0.65)";
ctx.font = `bold ${Math.max(9, thick * 0.62)}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("OPEN", 0, -thick * 1.08);
}
} else {
drawRail(-len / 2, -thick / 2, len, thick, 1);
if (bounce) {
ctx.strokeStyle = styleNight ? "rgba(204,236,255,0.58)" : "rgba(255,255,255,0.72)";
ctx.lineWidth = Math.max(1.3, thick * 0.11);
ctx.beginPath();
const coils = 9;
for (let i = 0; i <= coils; i += 1) {
const x = -len * 0.42 + len * 0.84 * (i / coils);
const y = (i % 2 === 0 ? -1 : 1) * thick * 0.23;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.fillStyle = styleNight ? "rgba(35,53,84,0.78)" : "rgba(36,93,145,0.72)";
for (const xx of [-0.32, 0, 0.32]) {
ctx.beginPath();
ctx.ellipse(len * xx, 0, thick * 0.19, thick * 0.37, 0, 0, Math.PI * 2);
ctx.fill();
}
}
}
ctx.restore();
} else if (this.type === "fan") {
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
const spin = (t * 12 + (this.seed || 0));
ctx.save();
ctx.rotate(angle);
drawFanInfluenceLocal(ctx, this.r || 32, styleNight);
ctx.save();
ctx.globalAlpha *= styleNight ? 0.20 : 0.24;
ctx.strokeStyle = styleNight ? "rgba(170,220,255,0.50)" : "rgba(66,150,215,0.42)";
ctx.fillStyle = styleNight ? "rgba(90,150,190,0.08)" : "rgba(110,190,240,0.08)";
ctx.lineWidth = 1.25;
ctx.setLineDash([5, 5]);
roundedRect(ctx, -Math.max(22, this.r * 0.92), -Math.max(18, this.r * 0.72), Math.max(44, this.r * 1.84), Math.max(36, this.r * 1.44), 7);
ctx.fill(); ctx.stroke();
ctx.restore();
ctx.fillStyle = styleNight ? "#53606b" : "#b8c7d2";
ctx.strokeStyle = styleNight ? "#2f3842" : "#667684";
ctx.lineWidth = 2.2;
roundedRect(ctx, -this.r * 0.90, -this.r * 0.70, this.r * 1.32, this.r * 1.40, this.r * 0.25);
ctx.fill(); ctx.stroke();
ctx.fillStyle = styleNight ? "#34414c" : "#eaf2f6";
ctx.strokeStyle = styleNight ? "#9db2c1" : "#627585";
ctx.beginPath();
roundedRect(ctx, -thick / 2, -len / 2, thick, len, thick / 2);
ctx.fill();
ctx.arc(-this.r * 0.20, 0, this.r * 0.68, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.save();
ctx.translate(-this.r * 0.20, 0);
ctx.rotate(spin);
ctx.fillStyle = styleNight ? "rgba(166,205,230,0.72)" : "rgba(88,139,170,0.66)";
for (let i = 0; i < 3; i += 1) {
ctx.rotate(Math.PI * 2 / 3);
ctx.beginPath();
ctx.ellipse(this.r * 0.30, 0, this.r * 0.42, this.r * 0.15, 0.22, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
ctx.fillStyle = styleNight ? "rgba(170,220,250,0.34)" : "rgba(98,170,210,0.26)";
for (const off of [-0.42, 0, 0.42]) {
ctx.beginPath();
ctx.ellipse(this.r * 1.10, off * this.r, this.r * 0.62, this.r * 0.10, 0, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
} else if (this.type === "magnet") {
const angle = typeof itemAngleFor === "function" ? itemAngleFor(this) : (Number(this.angle) || 0);
ctx.save();
ctx.rotate(angle);
drawMagnetInfluenceLocal(ctx, this.r || 30, styleNight);
// Hitbox guide: same oriented body footprint used by collision/placement.
ctx.save();
ctx.globalAlpha *= styleNight ? 0.22 : 0.26;
ctx.strokeStyle = styleNight ? "rgba(190,195,255,0.50)" : "rgba(82,88,210,0.42)";
ctx.fillStyle = styleNight ? "rgba(128,120,220,0.08)" : "rgba(132,118,232,0.08)";
ctx.lineWidth = 1.25;
ctx.setLineDash([5, 5]);
roundedRect(ctx, -Math.max(22, this.r * 0.88), -Math.max(18, this.r * 0.78), Math.max(44, this.r * 1.76), Math.max(36, this.r * 1.56), 7);
ctx.fill(); ctx.stroke();
ctx.restore();
// The U-shape is flipped horizontally so its open poles face the magnetic range.
ctx.save();
ctx.scale(-1, 1);
ctx.strokeStyle = styleNight ? "#26303a" : "#5a3d40";
ctx.lineWidth = Math.max(5, this.r * 0.30);
ctx.lineCap = "round";
ctx.beginPath();
ctx.arc(-this.r * 0.05, 0, this.r * 0.75, -Math.PI * 0.62, Math.PI * 0.62);
ctx.stroke();
ctx.fillStyle = hi;
ctx.lineWidth = Math.max(7, this.r * 0.40);
ctx.strokeStyle = styleNight ? "#a64755" : "#d9555f";
ctx.beginPath();
roundedRect(ctx, -thick * 0.26, -len / 2 + 6, thick * 0.20, len - 12, thick / 3);
ctx.fill();
ctx.arc(-this.r * 0.05, 0, this.r * 0.75, -Math.PI * 0.62, -Math.PI * 0.10);
ctx.stroke();
ctx.strokeStyle = styleNight ? "#4d7fbe" : "#4d86d9";
ctx.beginPath();
ctx.arc(-this.r * 0.05, 0, this.r * 0.75, Math.PI * 0.10, Math.PI * 0.62);
ctx.stroke();
ctx.restore();
ctx.fillStyle = styleNight ? "#ffd7de" : "#fff2f4";
ctx.font = `bold ${Math.max(10, this.r * 0.54)}px ui-rounded, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("N", this.r * 0.56, -this.r * 0.56);
ctx.fillText("S", this.r * 0.56, this.r * 0.56);
ctx.restore();
} else if (this.type === "zunchi") {
const zunchiId = this.zunchiVariant || "zunchi";

View file

@ -8,7 +8,7 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "water" || type === "water_bowl" || type === "zunda_juice") item.roles.drink = true;
if (["sweet", "water", "water_bowl", "zunda_juice"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true;
if (type === "bed" || type === "nest_box") item.roles.sleepPlace = true;
if (["firecracker", "genkotsu", "pushpin", "splat"].includes(type)) item.roles.danger = true;
if (["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(type)) item.roles.danger = true;
if (type === "grass") item.roles.grassMaterial = true;
if (type === "grass") {
item.grassStage = Math.floor(rand(0, 2.999));
@ -101,6 +101,23 @@ function initializeItemTypeState(item, type, x, y) {
item.pinLogAt = -999;
item.deletable = true;
}
if (typeof isRotatableItemType === "function" && isRotatableItemType(type)) {
item.angle = typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0;
item.amount = Math.max(item.amount || 0, 999);
}
if (type === "gate_fence") {
item.gateOpen = false;
item.gateLastToggleAt = -999;
}
if (type === "fan") {
item.fanPhase = rand(0, Math.PI * 2);
item.fanPulseAt = rand(0, 0.3);
}
if (type === "magnet") {
item.magnetPulseAt = rand(0, 0.4);
}
if (isServingFoodType(type)) {
item.toolSize = "medium";
item.foodServingScale = 1;

View file

@ -1,13 +1,13 @@
"use strict";
(function (global) {
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker"]);
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker", "fan", "magnet"]);
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
const SCHEDULED_ITEM_TYPES = [
"ball", "genkotsu", "firecracker", "pushpin", "oshibyo",
"plushie", "grass_bed", "zunchi", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h",
"stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet",
];
function itemUpdateInterval(item) {

View file

@ -389,14 +389,44 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
}
function drawStoredConsumableSprite(ctx, type = "", r = 10, seed = 1) {
drawConsumableFieldSprite(ctx, type || "food", r, seed, { compact: true });
const key = String(type || "food");
if (typeof isPinType === "function" && isPinType(key)) {
const behavior = typeof pinBehaviorFor === "function" ? pinBehaviorFor(key) : null;
const assetId = behavior?.looseAsset || key;
const img = typeof getRenderableImage === "function" ? getRenderableImage(assetId, assetId) : images.get(assetId);
ctx.save();
ctx.rotate(-0.35);
if (img) {
const metrics = typeof getImageMetrics === "function" ? getImageMetrics(assetId) : null;
const ratio = metrics?.ratio || 2.0;
const w = key === "oshibyo" ? r * 1.30 : r * 1.70;
const h = w * ratio;
ctx.drawImage(img, -w * 0.5, -h * 0.5, w, h);
} else {
ctx.fillStyle = key === "oshibyo" ? "#4d86d9" : "#d92736";
ctx.strokeStyle = "rgba(84,36,42,0.72)";
ctx.lineWidth = Math.max(1, r * 0.10);
ctx.beginPath();
ctx.ellipse(0, -r * 0.18, r * 0.58, r * 0.38, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
ctx.strokeStyle = "rgba(220,220,226,0.92)";
ctx.beginPath();
ctx.moveTo(0, r * 0.18);
ctx.lineTo(0, r * 1.25);
ctx.stroke();
}
ctx.restore();
return;
}
drawConsumableFieldSprite(ctx, key, r, seed, { compact: true });
}
function previewItemRadiusFor(type = "") {
const def = typeof itemDefinition === "function" ? itemDefinition(type) : null;
const base = Number(def?.radius || itemRadiusFor?.(type, 16) || 16);
if (type === "genkotsu") return 30;
if (type === "fence_v" || type === "fence_h") return 24;
if (type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") return 24;
if (type === "fan" || type === "magnet") return 24;
if (type === "nest_box") return 26;
if (type === "duplicator") return 24;
if (type === "signboard") return 22;
@ -405,6 +435,60 @@ function previewItemRadiusFor(type = "") {
return clamp(base, 8, 24);
}
function canvasAlphaBounds(canvas, alphaThreshold = 8) {
const w = Math.max(1, Math.floor(canvas?.width || 0));
const h = Math.max(1, Math.floor(canvas?.height || 0));
if (!w || !h) return null;
const ctx = canvas.getContext?.("2d", { willReadFrequently: true });
if (!ctx) return null;
let data = null;
try {
data = ctx.getImageData(0, 0, w, h).data;
} catch (_) {
return null;
}
let minX = w;
let minY = h;
let maxX = -1;
let maxY = -1;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (data[(y * w + x) * 4 + 3] <= alphaThreshold) continue;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
}
if (maxX < minX || maxY < minY) return null;
return { x: minX, y: minY, w: maxX - minX + 1, h: maxY - minY + 1 };
}
function normalizeToolPreviewCanvas(targetCtx, sourceCanvas, w, h, opts = {}) {
if (!targetCtx || !sourceCanvas) return false;
const box = canvasAlphaBounds(sourceCanvas);
targetCtx.clearRect(0, 0, w, h);
if (!box) return false;
const watermark = !!opts.watermark;
const targetScale = Number.isFinite(Number(opts.targetScale)) ? Math.max(0.35, Number(opts.targetScale)) : 1;
const maxW = w * (watermark ? 0.68 : 0.86) * targetScale;
const maxH = h * (watermark ? 0.68 : 0.82) * targetScale;
const scale = Math.min(maxW / Math.max(1, box.w), maxH / Math.max(1, box.h));
const drawW = box.w * scale;
const drawH = box.h * scale;
const centerX = watermark ? w * 0.78 : w * 0.50;
const centerY = watermark ? h * 0.80 : h * 0.48;
const dx = centerX - drawW / 2;
const dy = centerY - drawH / 2;
targetCtx.save();
targetCtx.imageSmoothingEnabled = true;
targetCtx.imageSmoothingQuality = "high";
targetCtx.drawImage(sourceCanvas, box.x, box.y, box.w, box.h, dx, dy, drawW, drawH);
targetCtx.restore();
return true;
}
function drawToolItemPreview(ctx, type = "", opts = {}) {
if (!ctx || !type) return false;
const w = Math.max(1, Number(opts.width || ctx.canvas?.width || 72));
@ -443,9 +527,25 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
toolSize: "medium",
});
if (type === "water") item.amount = 50;
const outputCtx = ctx;
let sourceCanvas = null;
let drawCtx = ctx;
try {
const ownerDocument = ctx.canvas?.ownerDocument || (typeof document !== "undefined" ? document : null);
sourceCanvas = ownerDocument?.createElement?.("canvas") || null;
if (sourceCanvas) {
sourceCanvas.width = w;
sourceCanvas.height = h;
drawCtx = sourceCanvas.getContext("2d") || ctx;
}
} catch (_) {
sourceCanvas = null;
drawCtx = ctx;
}
ctx = drawCtx;
ctx.save();
ctx.clearRect(0, 0, w, h);
const baseOffsetY = type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" ? 1 : 4;
const baseOffsetY = type === "signboard" ? 11 : type === "genkotsu" ? 12 : type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence" ? 1 : 4;
const centerX = watermark ? w * 0.78 : w / 2;
const centerY = watermark ? (h * 0.80 + baseOffsetY * 0.15) : (h / 2 + baseOffsetY);
ctx.translate(centerX, centerY);
@ -457,17 +557,24 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
duplicator: 0.82,
fence_v: 0.70,
fence_h: 0.70,
bounce_fence: 0.74,
bounce_fence_v: 0.74,
gate_fence: 0.74,
fan: 0.86,
magnet: 0.88,
ant_nest: 0.92,
ball: 0.92,
pushpin: 0.92,
oshibyo: 0.92,
oshibyo: 0.62,
firecracker: 0.90,
giant_drug: 1.22,
dwarf_drug: 0.56,
};
let scale = scaleMap[type] || 0.92;
if (isConsumableSpriteType(type)) scale *= 1.95;
else if (["grass", "water", "water_bowl", "zunda_juice"].includes(type)) scale *= 1.85;
else if (type === "duplicator") scale *= 1.55;
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
else if (["signboard", "bed", "nest_box", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet", "firecracker", "genkotsu"].includes(type)) scale *= 1.08;
else if (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)) scale *= 1.35;
if (watermark) scale *= 1.42;
ctx.scale(scale, scale);
@ -478,6 +585,10 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
return false;
}
ctx.restore();
if (sourceCanvas && outputCtx !== ctx) {
const previewScaleOverrides = { giant_drug: 1.55, dwarf_drug: 0.55, oshibyo: 0.58 };
normalizeToolPreviewCanvas(outputCtx, sourceCanvas, w, h, { watermark, targetScale: previewScaleOverrides[type] || 1 });
}
return true;
}

View file

@ -1,6 +1,93 @@
"use strict";
(function (global) {
function rectLocalPoint(rect, x, y) {
const dx = x - (rect.cx ?? 0);
const dy = y - (rect.cy ?? 0);
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(rect.angle || 0);
return { x: dx * c + dy * ss, y: -dx * ss + dy * c };
}
function rectWorldVector(rect, x, y) {
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(rect.angle || 0);
const ss = Number.isFinite(rect.sin) ? rect.sin : Math.sin(rect.angle || 0);
return { x: x * c - y * ss, y: x * ss + y * c };
}
function circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts = {}) {
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
const minBounceSpeed = Math.max(0, Number(opts.minBounceSpeed ?? rect.minBounceSpeed ?? 0) || 0);
const local = rectLocalPoint(rect, obj.x, obj.y);
let clx = clamp(local.x, -(rect.halfW || 0), rect.halfW || 0);
let cly = clamp(local.y, -(rect.halfH || 0), rect.halfH || 0);
let dx = local.x - clx;
let dy = local.y - cly;
let d = Math.hypot(dx, dy);
if (d >= hitRadius) {
const px = obj.prevX;
const py = obj.prevY;
if (!Number.isFinite(px) || !Number.isFinite(py)) return false;
const prev = rectLocalPoint(rect, px, py);
const expanded = { left: -(rect.halfW || 0) - hitRadius, right: (rect.halfW || 0) + hitRadius, top: -(rect.halfH || 0) - hitRadius, bottom: (rect.halfH || 0) + hitRadius };
const insideNow = local.x >= expanded.left && local.x <= expanded.right && local.y >= expanded.top && local.y <= expanded.bottom;
const insidePrev = prev.x >= expanded.left && prev.x <= expanded.right && prev.y >= expanded.top && prev.y <= expanded.bottom;
if (!insideNow && !insidePrev) return false;
const sx = local.x - prev.x;
const sy = local.y - prev.y;
const candidates = [];
if (prev.x < expanded.left && sx > 0) candidates.push({ nx: -1, ny: 0, t: (expanded.left - prev.x) / Math.max(sx, 0.001) });
if (prev.x > expanded.right && sx < 0) candidates.push({ nx: 1, ny: 0, t: (prev.x - expanded.right) / Math.max(-sx, 0.001) });
if (prev.y < expanded.top && sy > 0) candidates.push({ nx: 0, ny: -1, t: (expanded.top - prev.y) / Math.max(sy, 0.001) });
if (prev.y > expanded.bottom && sy < 0) candidates.push({ nx: 0, ny: 1, t: (prev.y - expanded.bottom) / Math.max(-sy, 0.001) });
if (!candidates.length) return false;
candidates.sort((a, b) => a.t - b.t);
dx = candidates[0].nx;
dy = candidates[0].ny;
d = 1;
} else if (d < 0.001) {
const left = Math.abs(local.x + (rect.halfW || 0));
const right = Math.abs((rect.halfW || 0) - local.x);
const top = Math.abs(local.y + (rect.halfH || 0));
const bottom = Math.abs((rect.halfH || 0) - local.y);
const m = Math.min(left, right, top, bottom);
if (m === left) { dx = -1; dy = 0; clx = -(rect.halfW || 0); cly = local.y; }
else if (m === right) { dx = 1; dy = 0; clx = rect.halfW || 0; cly = local.y; }
else if (m === top) { dx = 0; dy = -1; clx = local.x; cly = -(rect.halfH || 0); }
else { dx = 0; dy = 1; clx = local.x; cly = rect.halfH || 0; }
d = 1;
}
const nxLocal = dx / d;
const nyLocal = dy / d;
const n = rectWorldVector(rect, nxLocal, nyLocal);
const nx = n.x;
const ny = n.y;
const worldClosest = rectWorldVector(rect, clx, cly);
obj.x = (rect.cx || 0) + worldClosest.x + nx * (hitRadius + 0.5);
obj.y = (rect.cy || 0) + worldClosest.y + ny * (hitRadius + 0.5);
const vx = obj.vx || 0;
const vy = obj.vy || 0;
const toward = vx * nx + vy * ny;
if (toward < 0) {
obj.vx = vx - (1 + restitution) * toward * nx;
obj.vy = vy - (1 + restitution) * toward * ny;
} else {
obj.vx = vx + nx * 36;
obj.vy = vy + ny * 36;
}
if (minBounceSpeed > 0) {
const normalSpeed = (obj.vx || 0) * nx + (obj.vy || 0) * ny;
if (normalSpeed < minBounceSpeed) {
const add = minBounceSpeed - normalSpeed;
obj.vx = (obj.vx || 0) + nx * add;
obj.vy = (obj.vy || 0) + ny * add;
}
}
if (Number.isFinite(obj.spinVelocity)) obj.spinVelocity = clamp((obj.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 7.0, -46, 46);
return true;
}
function bounceInsideWorld(obj, worldRef, opts = {}) {
if (!obj || !worldRef) return false;
const p = Math.max(opts.padding ?? 28, CONFIG.worldPadding || 30);
@ -16,6 +103,120 @@
return bounced;
}
function bounceCircleOffRect(obj, rect, radius, restitution = 1.08, worldRef = null, opts = {}) {
if (!obj || !rect) return false;
if (rect.oriented) return circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts);
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
const minBounceSpeed = Math.max(0, Number(opts.minBounceSpeed ?? rect.minBounceSpeed ?? 0) || 0);
const ensureMinNormalSpeed = (nx, ny) => {
if (!(minBounceSpeed > 0)) return;
const normalSpeed = (obj.vx || 0) * nx + (obj.vy || 0) * ny;
if (normalSpeed >= minBounceSpeed) return;
const add = minBounceSpeed - normalSpeed;
obj.vx = (obj.vx || 0) + nx * add;
obj.vy = (obj.vy || 0) + ny * add;
};
const cx = clamp(obj.x, rect.left, rect.right);
const cy = clamp(obj.y, rect.top, rect.bottom);
let dx = obj.x - cx;
let dy = obj.y - cy;
let d = Math.hypot(dx, dy);
if (d >= hitRadius) {
const px = obj.prevX;
const py = obj.prevY;
const expanded = { left: rect.left - hitRadius, right: rect.right + hitRadius, top: rect.top - hitRadius, bottom: rect.bottom + hitRadius };
if (!Number.isFinite(px) || !Number.isFinite(py) || !worldRef?.segmentIntersectsRect?.(px, py, obj.x, obj.y, expanded)) return false;
const vx = obj.vx || 0;
const vy = obj.vy || 0;
const candidates = [];
const sx = obj.x - px;
const sy = obj.y - py;
if (px < expanded.left && sx > 0) candidates.push({ nx: -1, ny: 0, t: (expanded.left - px) / Math.max(sx, 0.001) });
if (px > expanded.right && sx < 0) candidates.push({ nx: 1, ny: 0, t: (px - expanded.right) / Math.max(-sx, 0.001) });
if (py < expanded.top && sy > 0) candidates.push({ nx: 0, ny: -1, t: (expanded.top - py) / Math.max(sy, 0.001) });
if (py > expanded.bottom && sy < 0) candidates.push({ nx: 0, ny: 1, t: (py - expanded.bottom) / Math.max(-sy, 0.001) });
let nx = 0;
let ny = 0;
if (candidates.length) {
candidates.sort((a, b) => a.t - b.t);
nx = candidates[0].nx;
ny = candidates[0].ny;
} else if (Math.abs(vx) >= Math.abs(vy)) nx = vx >= 0 ? -1 : 1;
else ny = vy >= 0 ? -1 : 1;
const toward = vx * nx + vy * ny;
if (nx < 0) obj.x = expanded.left - 0.5;
else if (nx > 0) obj.x = expanded.right + 0.5;
if (ny < 0) obj.y = expanded.top - 0.5;
else if (ny > 0) obj.y = expanded.bottom + 0.5;
if (nx) obj.y = clamp(obj.y, expanded.top, expanded.bottom);
if (ny) obj.x = clamp(obj.x, expanded.left, expanded.right);
if (toward < 0) {
obj.vx = vx - (1 + restitution) * toward * nx;
obj.vy = vy - (1 + restitution) * toward * ny;
} else {
obj.vx = vx + nx * 36;
obj.vy = vy + ny * 36;
}
ensureMinNormalSpeed(nx, ny);
if (Number.isFinite(obj.spinVelocity)) obj.spinVelocity = clamp((obj.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 7.0, -46, 46);
return true;
}
if (d < 0.001) {
const left = Math.abs(obj.x - rect.left);
const right = Math.abs(rect.right - obj.x);
const top = Math.abs(obj.y - rect.top);
const bottom = Math.abs(rect.bottom - obj.y);
const m = Math.min(left, right, top, bottom);
if (m === left) { dx = -1; dy = 0; }
else if (m === right) { dx = 1; dy = 0; }
else if (m === top) { dx = 0; dy = -1; }
else { dx = 0; dy = 1; }
d = 1;
}
const nx = dx / d;
const ny = dy / d;
const vx = obj.vx || 0;
const vy = obj.vy || 0;
const toward = vx * nx + vy * ny;
obj.x = cx + nx * (hitRadius + 0.5);
obj.y = cy + ny * (hitRadius + 0.5);
if (toward < 0) {
obj.vx = vx - (1 + restitution) * toward * nx;
obj.vy = vy - (1 + restitution) * toward * ny;
} else {
obj.vx = vx + nx * 36;
obj.vy = vy + ny * 36;
}
ensureMinNormalSpeed(nx, ny);
if (Number.isFinite(obj.spinVelocity)) obj.spinVelocity = clamp((obj.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 7.0, -46, 46);
return true;
}
function resolveKinematicBounceFenceCollision(item, worldRef, dt = 0.016, opts = {}) {
if (!item || !worldRef?.nearbySolidObstacleRects) return false;
const speed = Math.hypot(item.vx || 0, item.vy || 0);
const radius = Math.max(4, (item.r || item.radius || 12) + (opts.obstaclePadding ?? 2));
const searchRadius = Math.max(96, radius + speed * Math.max(dt || 0.016, 0.016) + 90);
let bounced = false;
const rects = worldRef.nearbySolidObstacleRects(item.x, item.y, searchRadius, { include: it => it?.type === "bounce_fence" || it?.type === "bounce_fence_v", exclude: item, maxChecks: 16 }) || [];
for (const rect of rects) {
if (!rect?.bounce) continue;
if (bounceCircleOffRect(item, rect, radius, Number(rect.restitution || 1.08), worldRef, opts)) {
bounced = true;
const maxSpeed = opts.bounceFenceMaxSpeed ?? 760;
item.vx = clamp(item.vx || 0, -maxSpeed, maxSpeed);
item.vy = clamp(item.vy || 0, -maxSpeed, maxSpeed);
const now = worldRef.time || 0;
if ((item.lastBounceFenceEffectAt || -999) + 0.08 <= now) {
item.lastBounceFenceEffectAt = now;
worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(12, radius * 1.15), life: 0.16, color: "rgba(82,153,230,0.45)" }));
}
}
}
return bounced;
}
function applyKinematicItemMotion(item, worldRef, dt, opts = {}) {
if (!item || !worldRef) return 0;
const stopSpeed = opts.stopSpeed ?? 0.05;
@ -34,7 +235,11 @@
item.x += (item.vx || 0) * dt;
item.y += (item.vy || 0) * dt;
bounceInsideWorld(item, worldRef, opts);
const friction = Math.pow(opts.frictionBase ?? 0.68, dt * (opts.frictionRate ?? 1));
resolveKinematicBounceFenceCollision(item, worldRef, dt, opts);
const ice = (worldRef?.groundType || "soil") === "ice";
const baseFriction = ice ? Math.max(Number(opts.frictionBase ?? 0.68), 0.93) : (opts.frictionBase ?? 0.68);
const frictionRate = ice ? Math.min(Number(opts.frictionRate ?? 1), 0.55) : (opts.frictionRate ?? 1);
const friction = Math.pow(baseFriction, dt * frictionRate);
item.vx *= friction;
item.vy *= friction;
if (opts.spin !== false) {
@ -50,5 +255,5 @@
return speed;
}
global.TarinaiPhysics = { bounceInsideWorld, applyKinematicItemMotion };
global.TarinaiPhysics = { bounceInsideWorld, bounceCircleOffRect, resolveKinematicBounceFenceCollision, applyKinematicItemMotion };
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -124,6 +124,63 @@ function drawItemGlow(ctx, item, lighting, visibleAlpha = 1) {
return;
}
function drawDeviceInfluenceShape(ctx, influence, blocked = false, alphaScale = 1) {
if (!influence) return;
ctx.save();
ctx.setLineDash([10, 7]);
ctx.lineWidth = 1.6;
if (influence.type === "fan") {
const x = influence.x || 0;
const y = influence.y || 0;
const angle = influence.angle || 0;
const range = Math.max(1, influence.range || 170);
const nearW = Math.max(1, influence.nearWidth || 42);
const farW = Math.max(nearW, influence.farWidth || nearW * 3.4);
ctx.translate(x, y);
ctx.rotate(angle);
ctx.globalAlpha = (blocked ? 0.18 : 0.30) * alphaScale;
ctx.fillStyle = blocked ? "rgba(232,84,72,0.18)" : "rgba(115,190,235,0.22)";
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.48)" : "rgba(78,158,220,0.56)";
ctx.beginPath();
ctx.moveTo(0, -nearW * 0.55);
ctx.lineTo(range, -farW * 0.5);
ctx.quadraticCurveTo(range + Math.max(10, farW * 0.08), 0, range, farW * 0.5);
ctx.lineTo(0, nearW * 0.55);
ctx.quadraticCurveTo(-Math.max(4, nearW * 0.10), 0, 0, -nearW * 0.55);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = (blocked ? 0.46 : 0.64) * alphaScale;
ctx.stroke();
ctx.setLineDash([4, 8]);
ctx.beginPath();
for (const yy of [-0.34, 0, 0.34]) {
ctx.moveTo(Math.max(12, nearW * 0.35), yy * nearW);
ctx.quadraticCurveTo(range * 0.46, yy * farW * 0.35, range * 0.92, yy * farW);
}
ctx.stroke();
} else if (influence.type === "magnet") {
const range = Math.max(1, influence.range || 150);
ctx.translate(influence.x || 0, influence.y || 0);
ctx.rotate(influence.angle || 0);
ctx.globalAlpha = (blocked ? 0.16 : 0.26) * alphaScale;
ctx.fillStyle = blocked ? "rgba(232,84,72,0.16)" : "rgba(143,122,235,0.18)";
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.46)" : "rgba(94,94,215,0.52)";
ctx.beginPath();
ctx.arc(0, 0, range, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = (blocked ? 0.42 : 0.58) * alphaScale;
ctx.stroke();
ctx.setLineDash([3, 9]);
for (const scale of [0.38, 0.68]) {
ctx.beginPath();
ctx.arc(0, 0, range * scale, -Math.PI * 0.72, Math.PI * 0.72);
ctx.stroke();
}
}
ctx.restore();
}
function placementPreviewFor(world) {
const p = world?.pointer;
const type = toolItemType(world?.tool || "");
@ -138,10 +195,16 @@ function placementPreviewFor(world) {
const circleOutside = (cx, cy, radius) => cx - radius < pad || cy - radius < pad || cx + radius > world.w - pad || cy + radius > world.h - pad;
let blocked = false;
let tmp = { type, x, y, r, toolSize: sizeName, foodServingScale: sizeScale, dead: false };
if ((type === "fence_v" || type === "fence_h") && world.fenceRect) {
if (typeof isRotatableItemType === "function" && isRotatableItemType(type)) tmp.angle = world?.toolAngleFor ? world.toolAngleFor(type) : (typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
if ((type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence") && world.fenceRect) {
const rawRect = world.fenceRect(tmp);
return { type, x, y, r, rect: rawRect, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) || world.fencePlacementBlocked?.(tmp) };
}
if ((type === "fan" || type === "magnet") && world.deviceRect) {
const rawRect = world.deviceRect(tmp);
const influence = type === "fan" ? world.fanInfluenceShape?.(tmp) : world.magnetInfluenceShape?.(tmp);
return { type, x, y, r, rect: rawRect, influence, blocked: rectOutside(rawRect) || world.placementBlocked?.(tmp) };
}
if (type === "nest_box" && world.nestBoxBaseRect) {
const rect = world.nestBoxBaseRect(tmp);
return { type, x, y, r, rect, blocked: rectOutside(rect) || world.placementBlocked?.(tmp) };
@ -156,7 +219,7 @@ function placementPreviewFor(world) {
function drawPlacementPreview(ctx, world) {
const preview = placementPreviewFor(world);
if (!preview) return;
const { type, x, y, r, rect, blocked } = preview;
const { type, x, y, r, rect, influence, blocked } = preview;
const pulse = 0.55 + Math.sin((world.time || 0) * 7.0) * 0.10;
ctx.save();
ctx.globalAlpha = blocked ? 0.44 : 0.72;
@ -164,10 +227,21 @@ function drawPlacementPreview(ctx, world) {
ctx.setLineDash(blocked ? [5, 4] : [7, 5]);
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.86)" : "rgba(110,178,55,0.88)";
ctx.fillStyle = blocked ? "rgba(232,84,72,0.14)" : "rgba(190,236,92,0.15)";
if (influence) drawDeviceInfluenceShape(ctx, influence, blocked, 1.15);
if (rect) {
roundedRect(ctx, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, 8);
ctx.fill();
ctx.stroke();
if (rect.oriented) {
ctx.save();
ctx.translate(rect.cx || x, rect.cy || y);
ctx.rotate(rect.angle || 0);
roundedRect(ctx, -(rect.halfW || (rect.right - rect.left) / 2), -(rect.halfH || (rect.bottom - rect.top) / 2), (rect.halfW || (rect.right - rect.left) / 2) * 2, (rect.halfH || (rect.bottom - rect.top) / 2) * 2, 8);
ctx.fill();
ctx.stroke();
ctx.restore();
} else {
roundedRect(ctx, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, 8);
ctx.fill();
ctx.stroke();
}
if (type === "nest_box" && world.nestBoxSolidRects) {
ctx.setLineDash([3, 4]);
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.38)" : "rgba(110,178,55,0.42)";
@ -433,7 +507,8 @@ function visibleWorldRect(worldRef, margin = 160) {
function renderRadiusForEntity(entity) {
if (!entity) return 40;
if (entity.type === "genkotsu") return Math.max(420, (entity.r || 88) * 6.2);
if (entity.type === "fence_v" || entity.type === "fence_h") return Math.max(96, (entity.r || 24) * 4.2);
if (entity.type === "fence_v" || entity.type === "fence_h" || entity.type === "bounce_fence" || entity.type === "bounce_fence_v" || entity.type === "gate_fence") return Math.max(96, (entity.r || 24) * 4.2);
if (entity.type === "fan" || entity.type === "magnet") return Math.max(86, (entity.r || 28) * 3.4);
if (entity.type === "nest_box") return Math.max(86, (entity.r || 34) * 3.2);
if (entity.type === "ant_nest") return Math.max(72, (entity.r || 28) * 3.0);
if (entity.kind === "queen") return 42;
@ -648,8 +723,8 @@ function drawGardenBed(w, h, lighting) {
const groundType = world.groundType || "soil";
const x = 18, y = 22, bw = w - 36, bh = h - 44;
const border = activeCtx.createLinearGradient(0, y, 0, y + bh);
const borderTop = groundType === "concrete" ? "#8b8f98" : groundType === "blanket" ? "#d6a6bc" : groundType === "foot_massage" ? "#79a9cf" : fieldType === "park" ? "#6f8f55" : fieldType === "cage" ? "#8b8f98" : (lighting.goldenStrength > 0.25 ? "#bf8f5e" : "#a88c66");
const borderBottom = groundType === "concrete" ? "#5f6670" : groundType === "blanket" ? "#b77d9f" : groundType === "foot_massage" ? "#4f79a3" : fieldType === "park" ? "#486d3d" : fieldType === "cage" ? "#5f6670" : (lighting.goldenStrength > 0.25 ? "#9c7043" : "#8a6f4b");
const borderTop = groundType === "ice" ? "#bfe6f4" : groundType === "concrete" ? "#8b8f98" : groundType === "blanket" ? "#d6a6bc" : groundType === "foot_massage" ? "#79a9cf" : fieldType === "park" ? "#6f8f55" : fieldType === "cage" ? "#8b8f98" : (lighting.goldenStrength > 0.25 ? "#bf8f5e" : "#a88c66");
const borderBottom = groundType === "ice" ? "#77b7d2" : groundType === "concrete" ? "#5f6670" : groundType === "blanket" ? "#b77d9f" : groundType === "foot_massage" ? "#4f79a3" : fieldType === "park" ? "#486d3d" : fieldType === "cage" ? "#5f6670" : (lighting.goldenStrength > 0.25 ? "#9c7043" : "#8a6f4b");
border.addColorStop(0, light > 0.42 ? borderTop : "#6f6c72");
border.addColorStop(1, light > 0.42 ? borderBottom : "#565a68");
activeCtx.fillStyle = border;
@ -669,9 +744,11 @@ function drawGardenBed(w, h, lighting) {
? ["#d7d2c5", "#c7c2b6", "#aaa79e"]
: groundType === "blanket"
? ["#ffe1ec", "#f6c6dc", "#e3a2c6"]
: groundType === "foot_massage"
? ["#d2e6f4", "#a8c8e6", "#7fa9cd"]
: fieldType === "park"
: groundType === "ice"
? ["#dff8ff", "#b8e7f5", "#8bcfe4"]
: groundType === "foot_massage"
? ["#d2e6f4", "#a8c8e6", "#7fa9cd"]
: fieldType === "park"
? ["#cfe5a8", "#b8d990", "#91bd74"]
: fieldType === "cage"
? ["#d7d2c5", "#c7c2b6", "#aaa79e"]
@ -688,25 +765,64 @@ function drawGardenBed(w, h, lighting) {
roundedRect(activeCtx, x + 16, y + 14, bw - 32, bh - 30, 28);
activeCtx.clip();
const speckCount = groundType === "blanket" ? 28 : groundType === "foot_massage" ? 86 : fieldType === "park" ? 72 : fieldType === "cage" ? 34 : 46;
const speckCount = groundType === "ice" ? 54 : groundType === "blanket" ? 28 : groundType === "foot_massage" ? 180 : fieldType === "park" ? 72 : fieldType === "cage" ? 34 : 46;
for (let i = 0; i < speckCount; i++) {
const px = randSeed(900 + i, x + 24, x + bw - 24);
const py = randSeed(1200 + i, y + 22, y + bh - 24);
const r = randSeed(1600 + i, 1.2, 3.8);
const a = randSeed(1900 + i, 0.03, 0.09) * (light > 0.4 ? 1.05 + lighting.goldenStrength * 0.35 : 0.70);
const r = randSeed(1600 + i, 1.2, groundType === "foot_massage" ? 5.2 : 3.8);
const a = randSeed(1900 + i, groundType === "foot_massage" ? 0.065 : 0.03, groundType === "foot_massage" ? 0.18 : 0.09) * (light > 0.4 ? 1.05 + lighting.goldenStrength * 0.35 : 0.70);
if (groundType === "foot_massage") {
activeCtx.fillStyle = `rgba(${Math.round(randSeed(2000 + i, 76, 126))}, ${Math.round(randSeed(2300 + i, 132, 186))}, ${Math.round(randSeed(2600 + i, 186, 236))}, ${a * 1.7})`;
const warm = i % 5 === 0;
activeCtx.fillStyle = warm
? `rgba(${Math.round(randSeed(2100 + i, 176, 222))}, ${Math.round(randSeed(2400 + i, 206, 236))}, ${Math.round(randSeed(2700 + i, 238, 255))}, ${a * 0.92})`
: `rgba(${Math.round(randSeed(2000 + i, 52, 108))}, ${Math.round(randSeed(2300 + i, 118, 184))}, ${Math.round(randSeed(2600 + i, 182, 242))}, ${a * 1.45})`;
activeCtx.beginPath();
activeCtx.arc(px, py, r * 1.25, 0, Math.PI * 2);
activeCtx.ellipse(px, py, r * randSeed(3100 + i, 1.1, 2.0), r * randSeed(3400 + i, 0.65, 1.25), randSeed(3700 + i, -1.2, 1.2), 0, Math.PI * 2);
activeCtx.fill();
} else {
activeCtx.fillStyle = groundType === "blanket" ? `rgba(255,255,255,${a * 1.35})` : (groundType === "foot_massage" ? `rgba(${Math.round(randSeed(2000 + i, 86, 128))}, ${Math.round(randSeed(2300 + i, 142, 196))}, ${Math.round(randSeed(2600 + i, 186, 235))}, ${a})` : `rgba(${Math.round(randSeed(2000 + i, 166, 196))}, ${Math.round(randSeed(2300 + i, 156, 182))}, ${Math.round(randSeed(2600 + i, 128, 150))}, ${a})`);
activeCtx.fillStyle = groundType === "blanket" ? `rgba(255,255,255,${a * 1.35})` : (groundType === "ice" ? `rgba(255,255,255,${a * 1.65})` : (groundType === "foot_massage" ? `rgba(${Math.round(randSeed(2000 + i, 86, 128))}, ${Math.round(randSeed(2300 + i, 142, 196))}, ${Math.round(randSeed(2600 + i, 186, 235))}, ${a})` : `rgba(${Math.round(randSeed(2000 + i, 166, 196))}, ${Math.round(randSeed(2300 + i, 156, 182))}, ${Math.round(randSeed(2600 + i, 128, 150))}, ${a})`));
activeCtx.beginPath();
activeCtx.ellipse(px, py, r * 1.5, r, randSeed(3000 + i, -0.8, 0.8), 0, Math.PI * 2);
activeCtx.fill();
}
}
if (groundType === "foot_massage") {
activeCtx.save();
activeCtx.globalCompositeOperation = "multiply";
const largePatchCount = 28;
for (let i = 0; i < largePatchCount; i++) {
const px = randSeed(8100 + i, x + 30, x + bw - 30);
const py = randSeed(8400 + i, y + 30, y + bh - 32);
const rw = randSeed(8700 + i, 12, 36);
const rh = randSeed(9000 + i, 7, 22);
const alpha = (light > 0.42 ? 0.105 : 0.070) + randSeed(9300 + i, 0, 0.055);
activeCtx.fillStyle = i % 3 === 0 ? `rgba(72, 132, 190, ${alpha})` : `rgba(118, 178, 224, ${alpha})`;
activeCtx.beginPath();
activeCtx.ellipse(px, py, rw, rh, randSeed(9600 + i, -1.1, 1.1), 0, Math.PI * 2);
activeCtx.fill();
}
activeCtx.restore();
}
if (groundType === "ice") {
activeCtx.save();
activeCtx.globalAlpha = 0.20 * (light > 0.4 ? 1.0 : 0.55);
activeCtx.strokeStyle = "rgba(255,255,255,0.72)";
activeCtx.lineWidth = 1.1;
for (let i = 0; i < 22; i += 1) {
const x = randSeed(4100 + i, 30, world.w - 30);
const y = randSeed(4300 + i, 30, world.h - 30);
const len = randSeed(4500 + i, 24, 78);
const a = randSeed(4700 + i, -0.9, 0.9);
activeCtx.beginPath();
activeCtx.moveTo(x - Math.cos(a) * len * 0.5, y - Math.sin(a) * len * 0.5);
activeCtx.lineTo(x + Math.cos(a) * len * 0.5, y + Math.sin(a) * len * 0.5);
activeCtx.stroke();
}
activeCtx.restore();
}
if (groundType === "blanket") {
activeCtx.save();
activeCtx.strokeStyle = light > 0.42 ? "rgba(255,255,255,0.28)" : "rgba(255,255,255,0.16)";
@ -738,7 +854,7 @@ function drawGardenBed(w, h, lighting) {
activeCtx.restore();
}
const patchCount = groundType === "concrete" ? 3 : groundType === "blanket" ? 0 : fieldType === "park" ? 14 : fieldType === "cage" ? 3 : 8;
const patchCount = groundType === "concrete" ? 3 : groundType === "blanket" || groundType === "ice" ? 0 : fieldType === "park" ? 14 : fieldType === "cage" ? 3 : 8;
for (let i = 0; i < patchCount; i++) {
const px = randSeed(3300 + i, x + 40, x + bw - 40);
const py = randSeed(3600 + i, y + 44, y + bh - 46);
@ -751,7 +867,7 @@ function drawGardenBed(w, h, lighting) {
}
// edge grasses and tiny flowers
const edgeGrassCount = groundType === "concrete" || groundType === "blanket" ? 0 : fieldType === "park" ? 24 : fieldType === "cage" ? 5 : 14;
const edgeGrassCount = groundType === "concrete" || groundType === "blanket" || groundType === "ice" ? 0 : fieldType === "park" ? 24 : fieldType === "cage" ? 5 : 14;
for (let i = 0; i < edgeGrassCount; i++) {
const side = i % 2;
const px = side === 0 ? randSeed(5000 + i, x + 36, x + bw - 36) : randSeed(5400 + i, x + 30, x + bw - 30);
@ -771,7 +887,7 @@ function drawGardenBed(w, h, lighting) {
activeCtx.restore();
}
const flowerCount = groundType === "concrete" || groundType === "blanket" || groundType === "foot_massage" ? 0 : fieldType === "park" ? 18 : fieldType === "cage" ? 0 : 9;
const flowerCount = groundType === "concrete" || groundType === "blanket" || groundType === "foot_massage" || groundType === "ice" ? 0 : fieldType === "park" ? 18 : fieldType === "cage" ? 0 : 9;
for (let i = 0; i < flowerCount; i++) {
const px = randSeed(7000 + i, x + 44, x + bw - 44);
const py = randSeed(7400 + i, y + 46, y + bh - 44);
@ -790,7 +906,7 @@ function drawGardenBed(w, h, lighting) {
}
}
const leafCount = groundType === "concrete" || groundType === "blanket" ? 0 : fieldType === "park" ? 18 : fieldType === "cage" ? 4 : 10;
const leafCount = groundType === "concrete" || groundType === "blanket" || groundType === "ice" ? 0 : fieldType === "park" ? 18 : fieldType === "cage" ? 4 : 10;
for (let i = 0; i < leafCount; i++) {
const px = randSeed(7800 + i, x + 36, x + bw - 36);
const py = randSeed(8200 + i, y + 34, y + bh - 34);

View file

@ -6,18 +6,20 @@
const EXPORT_PREFIX = "たり";
const RAW_CODEC = "生";
const DEFLATE_CODEC = "縮";
const BINARY_SCHEMA_VERSION = 1;
const BINARY_SCHEMA_VERSION = 5;
const MIN_BINARY_SCHEMA_VERSION = 4;
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
const GROUND_IDS = Object.freeze(["soil", "concrete", "blanket", "foot_massage"]);
const GROUND_IDS = Object.freeze(["soil", "concrete", "blanket", "foot_massage", "ice"]);
const ITEM_TYPE_IDS = Object.freeze([
"grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi",
"sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h",
"splat", "food", "bed", "genkotsu"
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet"
]);
const STRUCTURE_TYPE_IDS = new Set([3, 4]);
const SERVING_FOOD_TYPE_IDS = new Set([7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 31]);
const PIN_TYPE_IDS = new Set([26, 27]);
const ROTATABLE_ITEM_TYPE_IDS = new Set([28, 29, 34, 35, 36, 37, 38]);
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
@ -170,6 +172,67 @@
}
}
function clampToBits(value = 0, bits = 8) {
const max = (2 ** Math.max(0, bits)) - 1;
return Math.max(0, Math.min(max, toSafeUInt(value)));
}
function writeBitFields(writer, values = [], bits = []) {
let acc = 0;
let accBits = 0;
for (let i = 0; i < bits.length; i++) {
const width = Math.max(0, Number(bits[i]) || 0);
if (!width) continue;
let value = clampToBits(values?.[i] || 0, width);
let left = width;
while (left > 0) {
const room = 8 - accBits;
const take = Math.min(room, left);
const mask = (1 << take) - 1;
acc |= (value & mask) << accBits;
accBits += take;
value = Math.floor(value / (2 ** take));
left -= take;
if (accBits === 8) {
writer.b(acc);
acc = 0;
accBits = 0;
}
}
}
if (accBits > 0) writer.b(acc);
}
function readBitFields(reader, bits = []) {
const out = [];
let acc = 0;
let accBits = 0;
for (const rawWidth of bits) {
const width = Math.max(0, Number(rawWidth) || 0);
let value = 0;
let shift = 0;
let left = width;
while (left > 0) {
if (accBits === 0) { acc = reader.b(); accBits = 8; }
const take = Math.min(accBits, left);
const mask = (1 << take) - 1;
value += (acc & mask) * (2 ** shift);
acc = Math.floor(acc / (2 ** take));
accBits -= take;
shift += take;
left -= take;
}
out.push(value);
}
return out;
}
function writeQuantizedBitFields(writer, values = [], count = 0, bits = 6, step = 1) {
const list = [];
for (let i = 0; i < count; i++) list.push(Math.round((Number(values?.[i]) || 0) / Math.max(1, step)));
writeBitFields(writer, list, Array.from({ length: count }, () => bits));
}
function readQuantizedBitFields(reader, count = 0, bits = 6, step = 1) {
return readBitFields(reader, Array.from({ length: count }, () => bits)).map(v => v * Math.max(1, step));
}
function makeProfile(snapshot = {}) {
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
@ -257,6 +320,35 @@
for (let i = 0; i < count; i++) out.push(signed ? reader.s() : reader.u());
return out;
}
function writeFixedPacked(writer, arr = [], count = 0, bits = 8, step = 1) {
writeQuantizedBitFields(writer, arr, count, bits, step);
}
function readFixedPacked(reader, count = 0, bits = 8, step = 1) {
return readQuantizedBitFields(reader, count, bits, step);
}
function writeFixedDiffPacked(writer, base = [], current = [], count = 0, bits = 8) {
let mask = 0;
const values = [];
const widths = [];
for (let i = 0; i < count; i++) {
if (valuesDiffer(current?.[i], base?.[i])) {
mask |= (1 << i);
values.push(current?.[i] || 0);
widths.push(bits);
}
}
writeBitFields(writer, [mask], [count]);
if (widths.length) writeBitFields(writer, values, widths);
}
function readFixedDiffPacked(reader, base = [], count = 0, bits = 8) {
const [mask] = readBitFields(reader, [count]);
const out = Array.from({ length: count }, (_, i) => base?.[i] || 0);
const changed = [];
for (let i = 0; i < count; i++) if (mask & (1 << i)) changed.push(i);
const values = changed.length ? readBitFields(reader, changed.map(() => bits)) : [];
changed.forEach((index, i) => { out[index] = values[i]; });
return out;
}
function writeIndexList(writer, arr = []) {
const list = Array.isArray(arr) ? arr : [];
writer.u(list.length);
@ -323,27 +415,21 @@
const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100];
let mask = 0;
for (let i = 0; i < defaults.length; i++) if (valuesDiffer(timers?.[i], defaults[i])) mask |= (1 << i);
writer.u(mask);
writeBitFields(writer, [mask], [defaults.length]);
for (let i = 0; i < defaults.length; i++) if (mask & (1 << i)) writer.s(timers[i] || 0);
}
function readTimers(reader) {
const defaults = [0, 0, 0, 0, 0, 0, 0, 0, 100];
const out = defaults.slice();
const mask = reader.u();
const [mask] = readBitFields(reader, [defaults.length]);
for (let i = 0; i < defaults.length; i++) if (mask & (1 << i)) out[i] = reader.s();
return out;
}
function writeModes(writer, modes = []) {
let mask = 0;
for (let i = 0; i < 3; i++) if ((Number(modes?.[i]) || 0) !== 0) mask |= (1 << i);
writer.u(mask);
for (let i = 0; i < 3; i++) if (mask & (1 << i)) writer.u(modes[i] || 0);
writeBitFields(writer, [modes?.[0] || 0, modes?.[1] || 0, modes?.[2] || 0], [2, 2, 1]);
}
function readModes(reader) {
const out = [0, 0, 0];
const mask = reader.u();
for (let i = 0; i < 3; i++) if (mask & (1 << i)) out[i] = reader.u();
return out;
return readBitFields(reader, [2, 2, 1]);
}
function commonItemAmountNeeded(typeId) {
if (STRUCTURE_TYPE_IDS.has(typeId)) return false;
@ -368,97 +454,84 @@
}
function writeWorld(writer, w = []) {
writer.u(w[0] || 0); // field
writer.u(w[1] || 0); // ground
writer.u(w[2] || 0); // mood
writeBitFields(writer, [w[0] || 0, w[1] || 0, w[2] || 0, w[4] || 0], [2, 2, 4, 3]);
writer.u(w[3] || 0); // worldTick10
writer.u(w[4] || 0); // weather
writer.s(w[5] || 0); // lastBirthAt10 can be negative
writer.u(w[6] || 1); // generation
writer.u(w[7] || 0); // dead count
writer.str(w[8] || ""); // worldSeed
writer.u(w[9] || 0); // birthSerial
}
function readWorld(reader) {
return [reader.u(), reader.u(), reader.u(), reader.u(), reader.u(), reader.s(), reader.u(), reader.u()];
const [field, ground, mood, weather] = readBitFields(reader, [2, 2, 4, 3]);
return [field, ground, mood, reader.u(), weather, reader.s(), reader.u(), reader.u(), reader.str(), reader.u()];
}
function writeTarinai(writer, row = [], profile = null) {
const baseSprite = row[2] || 0;
const goodMode = row[24] || 0;
const pinIdx = row[21] ?? -1;
const nestIdx = row[22] ?? -1;
const stateId = row[23]?.[0] || 0;
const stateTarget = row[23]?.[1] ?? -1;
trackBytes(writer, profile, "tarinai.flags", () => writer.u(row[0] || 0));
trackBytes(writer, profile, "tarinai.name", () => writeName(writer, row[1] || ""));
function seedPersonalityArray(seed = "", flags = 0) {
// ずんちどれいは誕生時性格も全軸 -1 なので、差分基準も0固定にする。
if ((Number(flags) || 0) & (1 << 5)) return [0, 0, 0, 0];
return global.TarinaiSeedFactory?.personalityArray?.(seed) || [100, 100, 100, 100];
}
function writeTarinai(writer, row = [], profile = null, coordState = null) {
const pinIdx = row[15] ?? -1;
const nestIdx = row[16] ?? -1;
const stateId = row[17]?.[0] || 0;
const stateTarget = row[17]?.[1] ?? -1;
const birthSeed = row[1] || "";
trackBytes(writer, profile, "tarinai.flags", () => writeBitFields(writer, [row[0] || 0], [10]));
trackBytes(writer, profile, "tarinai.seed", () => writer.str(birthSeed));
trackBytes(writer, profile, "tarinai.core", () => {
writer.u(baseSprite);
writer.s(row[3] || 0);
writer.s(row[4] || 0);
writer.u(row[5] || 0);
writer.u(row[6] || 0);
writer.u(row[7] || 0);
writer.u(row[8] || 1);
writer.u(row[9] || 0);
writer.u(row[10] || 0);
writer.u(row[11] || 0);
const x = toSafeInt(row[2] || 0);
const y = toSafeInt(row[3] || 0);
writer.s(x - (coordState?.x || 0));
writer.s(y - (coordState?.y || 0));
if (coordState) { coordState.x = x; coordState.y = y; }
writer.u(row[4] || 0);
writer.u(row[5] || 1);
writeBitFields(writer, [row[6] || 0, row[7] || 0, row[8] || 0], [8, 8, 8]);
});
trackBytes(writer, profile, "tarinai.needs", () => writeFixed(writer, row[12], 6, false));
trackBytes(writer, profile, "tarinai.needs", () => writeFixedPacked(writer, row[9], 6, 6, 2));
trackBytes(writer, profile, "tarinai.traits", () => {
writeFixed(writer, row[13], 4, false);
writeFixedDiff(writer, row[13], row[14], 4, false);
writeFixed(writer, row[15], 4, false);
writeFixedDiffPacked(writer, seedPersonalityArray(birthSeed, row[0] || 0), row[10], 4, 8);
});
trackBytes(writer, profile, "tarinai.family", () => {
writeIndexList(writer, row[16]);
writeIndexList(writer, row[17]);
writeIndexList(writer, row[11]);
});
trackBytes(writer, profile, "tarinai.relationships", () => writeRelationships(writer, row[18]));
trackBytes(writer, profile, "tarinai.timers", () => writeTimers(writer, row[19]));
trackBytes(writer, profile, "tarinai.modes", () => writeModes(writer, row[20]));
trackBytes(writer, profile, "tarinai.relationships", () => writeRelationships(writer, row[12]));
trackBytes(writer, profile, "tarinai.timers", () => writeTimers(writer, row[13]));
trackBytes(writer, profile, "tarinai.modes", () => writeModes(writer, row[14]));
trackBytes(writer, profile, "tarinai.tail", () => {
let tailMask = 0;
if (pinIdx >= 0) tailMask |= 1;
if (nestIdx >= 0) tailMask |= 2;
if (stateId || stateTarget >= 0) tailMask |= 4;
if (goodMode) tailMask |= 8;
writer.u(tailMask);
writeBitFields(writer, [tailMask], [3]);
if (tailMask & 1) writer.u(pinIdx);
if (tailMask & 2) writer.u(nestIdx);
if (tailMask & 4) { writer.u(stateId); writer.s(stateTarget); }
if (tailMask & 8) writer.u(goodMode);
if (tailMask & 4) { writeBitFields(writer, [stateId], [3]); writer.s(stateTarget); }
});
}
function readTarinai(reader) {
const flags = reader.u();
const name = readName(reader);
const baseSprite = reader.u();
const x = reader.s();
const y = reader.s();
const adultScale = reader.u();
function readTarinai(reader, coordState = null) {
const flags = readBitFields(reader, [10])[0];
const birthSeed = reader.str();
const x = (coordState?.x || 0) + reader.s();
const y = (coordState?.y || 0) + reader.s();
if (coordState) { coordState.x = x; coordState.y = y; }
const age = reader.u();
const lifeSpan = reader.u();
const generation = reader.u();
const hunger = reader.u();
const energy = reader.u();
const sleepPressure = reader.u();
const needs = readFixed(reader, 6, false);
const birth = readFixed(reader, 4, false);
const current = readFixedDiff(reader, birth, 4, false);
const genetics = readFixed(reader, 4, false);
const [hunger, energy, sleepPressure] = readBitFields(reader, [8, 8, 8]);
const needs = readFixedPacked(reader, 6, 6, 2);
const current = readFixedDiffPacked(reader, seedPersonalityArray(birthSeed, flags), 4, 8);
const parents = readIndexList(reader);
const children = readIndexList(reader);
const relationships = readRelationships(reader);
const timers = readTimers(reader);
const modes = readModes(reader);
const tailMask = reader.u();
const [tailMask] = readBitFields(reader, [3]);
const pinIdx = (tailMask & 1) ? reader.u() : -1;
const nestIdx = (tailMask & 2) ? reader.u() : -1;
const stateInfo = (tailMask & 4) ? [reader.u(), reader.s()] : [0, -1];
const goodMode = (tailMask & 8) ? reader.u() : 0;
return [
flags, name, baseSprite, x, y, adultScale, age, lifeSpan, generation, hunger, energy, sleepPressure,
needs, birth, current, genetics, parents, children, relationships, timers, modes, pinIdx, nestIdx, stateInfo, goodMode
];
const stateInfo = (tailMask & 4) ? [readBitFields(reader, [3])[0], reader.s()] : [0, -1];
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo];
}
function writeItemExtra(writer, typeId, extra = []) {
@ -482,6 +555,8 @@
writer.s(extra[0] || 0);
} else if (PIN_TYPE_IDS.has(typeId)) {
writer.u(extra[0] || 0); writer.s(extra[1] ?? -1); writer.s(extra[2] || 0); writer.s(extra[3] || 0); writer.s(extra[4] || 0);
} else if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
writer.s(extra[0] || 0); writer.u(extra[1] || 0);
} else if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
}
@ -497,25 +572,37 @@
if (typeId === 24) return [reader.s()];
if (typeId === 25) return [reader.s()];
if (PIN_TYPE_IDS.has(typeId)) return [reader.u(), reader.s(), reader.s(), reader.s(), reader.s()];
if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) return (reader.schemaVersion || BINARY_SCHEMA_VERSION) >= 5 ? [reader.s(), reader.u()] : [];
if (SERVING_FOOD_TYPE_IDS.has(typeId)) return [reader.s(), reader.s()];
return [];
}
function writeItem(writer, row = [], profile = null) {
const typeId = row[0] || 0;
function itemCoordKey(row = []) {
const x = Math.max(0, Math.min(65535, toSafeUInt(row[1] || 0)));
const y = Math.max(0, Math.min(65535, toSafeUInt(row[2] || 0)));
return mortonKey(x, y);
}
function mortonKey(x = 0, y = 0) {
let out = 0;
for (let i = 0; i < 16; i++) out += (((x >> i) & 1) * (2 ** (2 * i))) + (((y >> i) & 1) * (2 ** (2 * i + 1)));
return out;
}
function writeItemPayload(writer, row = [], typeId = 0, coordState = null, profile = null) {
const start = writer.bytes.length;
trackBytes(writer, profile, "items.core", () => {
writer.u(typeId);
writer.s(row[1] || 0);
writer.s(row[2] || 0);
const x = toSafeInt(row[1] || 0);
const y = toSafeInt(row[2] || 0);
writer.s(x - (coordState?.x || 0));
writer.s(y - (coordState?.y || 0));
if (coordState) { coordState.x = x; coordState.y = y; }
if (commonItemAmountNeeded(typeId)) writer.s(row[3] || 0);
});
trackBytes(writer, profile, "items.extra", () => writeItemExtra(writer, typeId, row[4] || []));
addItemTypeProfile(profile, typeId, writer.bytes.length - start);
}
function readItem(reader) {
const typeId = reader.u();
const x = reader.s();
const y = reader.s();
function readItemPayload(reader, typeId = 0, coordState = null) {
const x = (coordState?.x || 0) + reader.s();
const y = (coordState?.y || 0) + reader.s();
if (coordState) { coordState.x = x; coordState.y = y; }
const amount = commonItemAmountNeeded(typeId) ? reader.s() : 0;
const extra = readItemExtra(reader, typeId);
let restoredAmount = amount;
@ -523,6 +610,41 @@
else if (SERVING_FOOD_TYPE_IDS.has(typeId)) restoredAmount = extra[0] || 0;
return [typeId, x, y, restoredAmount, extra];
}
function itemBlocksFromRows(rows = []) {
const groups = new Map();
for (const row of rows || []) {
const typeId = row?.[0] || 0;
if (!groups.has(typeId)) groups.set(typeId, []);
groups.get(typeId).push(row);
}
const blocks = [];
for (const [typeId, list] of [...groups.entries()].sort((a, b) => a[0] - b[0])) {
list.sort((a, b) => itemCoordKey(a) - itemCoordKey(b) || (a[1] || 0) - (b[1] || 0) || (a[2] || 0) - (b[2] || 0));
blocks.push([typeId, list]);
}
return blocks;
}
function writeItemBlocks(writer, rows = [], profile = null) {
const blocks = itemBlocksFromRows(rows);
writer.u(blocks.length);
for (const [typeId, list] of blocks) {
writer.u(typeId);
writer.u(list.length);
const coordState = { x: 0, y: 0 };
for (const row of list) writeItemPayload(writer, row, typeId, coordState, profile);
}
}
function readItemBlocks(reader) {
const blockCount = reader.u();
const items = [];
for (let b = 0; b < blockCount; b++) {
const typeId = reader.u();
const count = reader.u();
const coordState = { x: 0, y: 0 };
for (let i = 0; i < count; i++) items.push(readItemPayload(reader, typeId, coordState));
}
return items;
}
function fieldValue(index = 0) { return FIELD_IDS[Number(index) | 0] || "garden"; }
function groundValue(index = 0) { return GROUND_IDS[Number(index) | 0] || "soil"; }
@ -547,12 +669,12 @@
const tarinaiRows = Array.isArray(snapshot.t) ? snapshot.t : [];
trackBytes(writer, profile, "tarinai.count", () => writer.u(tarinaiRows.length));
const tarinaiStart = writer.bytes.length;
for (const row of tarinaiRows) writeTarinai(writer, row || [], profile);
const tarCoordState = { x: 0, y: 0 };
for (const row of tarinaiRows) writeTarinai(writer, row || [], profile, tarCoordState);
addProfileBytes(profile, "tarinai.total", writer.bytes.length - tarinaiStart);
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
trackBytes(writer, profile, "items.count", () => writer.u(itemRows.length));
const itemStart = writer.bytes.length;
for (const row of itemRows) writeItem(writer, row || [], profile);
trackBytes(writer, profile, "items.blocks", () => writeItemBlocks(writer, itemRows, profile));
addProfileBytes(profile, "items.total", writer.bytes.length - itemStart);
const bytes = writer.out();
if (profile) return { bytes, profile };
@ -562,16 +684,17 @@
function decodeBinarySnapshot(bytes) {
const reader = new BinaryReader(bytes);
const schema = reader.b();
if (schema !== BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
if (schema < MIN_BINARY_SCHEMA_VERSION || schema > BINARY_SCHEMA_VERSION) throw new Error("unsupported binary save schema");
reader.schemaVersion = schema;
const snapshotVersion = reader.u();
if (snapshotVersion !== Snapshot.version) throw new Error("unsupported binary save version");
const w = readWorld(reader);
const tCount = reader.u();
const t = [];
for (let i = 0; i < tCount; i++) t.push(readTarinai(reader));
const itemCount = reader.u();
const items = [];
for (let i = 0; i < itemCount; i++) items.push(readItem(reader));
const tarCoordState = { x: 0, y: 0 };
for (let i = 0; i < tCount; i++) t.push(readTarinai(reader, tarCoordState));
const items = readItemBlocks(reader);
const itemCount = items.length;
if (reader.pos !== reader.bytes.length) throw new Error("trailing bytes in binary save");
return { v: Snapshot.version, a: "tj1", c: Date.now(), m: summaryFromPayload(w, t.length, items.length), w, t, i: items };
}

View file

@ -98,10 +98,10 @@
${profileRow("ヘッダ", sections.header, raw)}
${profileRow("ワールド", sections.world, raw)}
${profileRow("たりない合計", sections["tarinai.total"], raw)}
${profileRow("名前", sections["tarinai.name"], raw)}
${profileRow("生成seed", sections["tarinai.seed"], raw)}
${profileRow("基本値", sections["tarinai.core"], raw)}
${profileRow("欲求", sections["tarinai.needs"], raw)}
${profileRow("性格/遺伝", sections["tarinai.traits"], raw)}
${profileRow("性格差分", sections["tarinai.traits"], raw)}
${profileRow("家族", sections["tarinai.family"], raw)}
${profileRow("関係", sections["tarinai.relationships"], raw)}
${profileRow("タイマー/効果", (sections["tarinai.timers"] || 0) + (sections["tarinai.modes"] || 0) + (sections["tarinai.tail"] || 0), raw)}
@ -123,15 +123,14 @@
dialog.innerHTML = `
<div class="field-dialog-panel save-dialog-panel">
<h2 id="saveDialogTitle">\u30bb\u30fc\u30d6 / \u30ed\u30fc\u30c9</h2>
<p class="hint save-warning">日本語混在の専用バイナリハッシュ保存のみを使用します</p>
<p class="hint">9つのスロットには日本語混在ハッシュテキストを保存します下の書き出し/読み込み欄はブラウザ外へ保存移動する時に使います</p>
<p class="hint save-warning">セーブは実験的機能であり保証対象外です</p>
<div id="saveSlotList" class="save-slot-list"></div>
<div class="save-export-panel">
<div class="save-export-actions">
<button id="saveExportBtn" class="btn" type="button">\u73fe\u5728\u306e\u72b6\u614b\u3092\u66f8\u304d\u51fa\u3057</button>
<button id="saveImportBtn" class="btn primary" type="button">\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8aad\u307f\u8fbc\u307f</button>
<button id="saveExportBtn" class="btn" type="button">呪文をセーブ</button>
<button id="saveImportBtn" class="btn primary" type="button">呪文をロード</button>
</div>
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="たり縮 から始まる日本語混在ハッシュテキストをここで書き出し/読み込みします。"></textarea>
<textarea id="saveHashText" class="save-hash-text" spellcheck="false" placeholder="セーブした呪文はコピペして保存してください。"></textarea>
<div id="saveHashProfile" class="save-hash-profile" aria-live="polite"></div>
</div>
<div class="field-dialog-actions">
@ -155,9 +154,9 @@
<div class="save-slot-title">\u30b9\u30ed\u30c3\u30c8 ${i}</div>
<div class="save-slot-meta">${htmlEscape(formatSlotSummary(snapshot))}</div>
<div class="save-slot-actions">
<button class="btn mini" data-save-slot="${i}" type="button">\u4fdd\u5b58</button>
<button class="btn mini" data-load-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>\u8aad\u8fbc</button>
<button class="btn mini danger" data-delete-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>\u524a\u9664</button>
<button class="btn mini" data-save-slot="${i}" type="button">セーブ</button>
<button class="btn mini" data-load-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>ロード</button>
<button class="btn mini danger" data-delete-slot="${i}" type="button" ${snapshot ? "" : "disabled"}>消す</button>
</div>`;
list.appendChild(row);
}
@ -195,34 +194,34 @@
await saveSlot(slot);
renderSlotList();
global.audio?.notify?.();
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u306b\u4fdd\u5b58\u3057\u307e\u3057\u305f\u3002`);
global.showToast?.(`スロット${slot}にセーブしました。`);
} catch (error) {
console.warn(error);
global.showToast?.("\u4fdd\u5b58\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u7a7a\u304d\u5bb9\u91cf\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
global.showToast?.("セーブに失敗しました。空き容量を確認してください。");
}
return;
}
if (loadBtn) {
const slot = Number(loadBtn.dataset.loadSlot);
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002`)) return;
if (!global.confirm?.(`スロット${slot}をロードします。現在の状態は上書きされます。`)) return;
try {
await loadSlot(slot);
closeDialog();
global.audio?.notify?.();
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002`);
global.showToast?.(`スロット${slot}をロードしました。`);
} catch (error) {
console.warn(error);
global.showToast?.("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002");
global.showToast?.("ロードに失敗しました。");
}
return;
}
if (deleteBtn) {
const slot = Number(deleteBtn.dataset.deleteSlot);
if (!global.confirm?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f`)) return;
if (!global.confirm?.(`スロット${slot}を消しますか?`)) return;
deleteSlot(slot);
renderSlotList();
global.audio?.delete?.();
global.showToast?.(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u524a\u9664\u3057\u307e\u3057\u305f\u3002`);
global.showToast?.(`スロット${slot}を消しました。`);
return;
}
if (e.target.closest("#saveCloseBtn")) closeDialog();
@ -235,20 +234,20 @@
area.focus();
area.select();
}
global.showToast?.("\u73fe\u5728\u306e\u72b6\u614b\u3092\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u306b\u66f8\u304d\u51fa\u3057\u307e\u3057\u305f\u3002");
global.showToast?.("呪文をセーブしました。");
}
if (e.target.closest("#saveImportBtn")) {
const area = dialog.querySelector("#saveHashText");
const text = area?.value || "";
if (!text.trim()) { global.showToast?.("\u8aad\u307f\u8fbc\u3080\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
if (!global.confirm?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u3092\u8aad\u307f\u8fbc\u307f\u307e\u3059\u3002\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002")) return;
if (!text.trim()) { global.showToast?.("ロードする呪文を貼り付けてください。"); return; }
if (!global.confirm?.("呪文をロードします。現在の状態は上書きされます。")) return;
try {
restoreSnapshot(await decodeSnapshot(text), global.world, { source: "import" });
closeDialog();
global.showToast?.("\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u304b\u3089\u8aad\u307f\u8fbc\u307f\u307e\u3057\u305f\u3002");
global.showToast?.("呪文からロードしました。");
} catch (error) {
console.warn(error);
global.showToast?.("\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30c6\u30ad\u30b9\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
global.showToast?.("ロードに失敗しました。呪文を確認してください。");
}
}
});

View file

@ -482,6 +482,21 @@ class Effect {
ctx.arc(Math.cos(a) * r, Math.sin(a) * r, randSeed(this.seed + i + 12, 2.2, 5.2), 0, Math.PI * 2);
ctx.fill();
}
} else if (this.type === "wind") {
const drift = 1 - alpha;
ctx.strokeStyle = this.color || "rgba(160,210,245,0.30)";
ctx.lineWidth = Math.max(1.2, this.size * 0.12);
ctx.lineCap = "round";
for (let i = 0; i < 3; i++) {
const y = (i - 1) * this.size * 0.28 + Math.sin(this.seed + i) * 2;
const x0 = -this.size * (0.55 + i * 0.08) + drift * this.size * 0.55;
const x1 = this.size * (0.72 + i * 0.12) + drift * this.size * 0.85;
ctx.globalAlpha = alpha * (0.22 + i * 0.08);
ctx.beginPath();
ctx.moveTo(x0, y);
ctx.quadraticCurveTo(0, y - this.size * 0.22, x1, y);
ctx.stroke();
}
} else if (this.type === "zunchi_miasma") {
const drift = 1 - alpha;
ctx.globalCompositeOperation = "source-over";
@ -619,7 +634,7 @@ class SpatialGrid {
const obstacleMap = dynamic ? this.dynamicObstacleCells : this.staticObstacleCells;
const foodMap = dynamic ? this.dynamicFoodCells : this.staticFoodCells;
const hazardMap = dynamic ? this.dynamicHazardCells : this.staticHazardCells;
if (typeof itemHasTrait === "function" ? itemHasTrait(type, "obstacle") : (type === "stone" || type === "ball" || type === "nest_box" || type === "bed" || type === "fence_v" || type === "fence_h")) this.add(obstacleMap, it);
if (typeof itemHasTrait === "function" ? itemHasTrait(type, "obstacle") : (type === "stone" || type === "ball" || type === "nest_box" || type === "bed" || type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v")) this.add(obstacleMap, it);
if (typeof itemHasTrait === "function" ? itemHasTrait(type, "food_interest") : (type === "sweet" || type === "love_mochi" || type === "fight_mochi" || type === "grass" || type === "water" || type === "water_bowl" || type === "ant_corpse" || (typeof isParamEffectItemType === "function" && isParamEffectItemType(type)))) this.add(foodMap, it);
if (typeof itemHasTrait === "function" && itemHasTrait(type, "hazard")) this.add(hazardMap, it);
}

View file

@ -1,11 +1,11 @@
"use strict";
(function (global) {
const SNAPSHOT_VERSION = 4;
const SNAPSHOT_VERSION = 5;
const NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"];
const DEFAULT_SKIP = new Set(["world", "target", "panicTarget", "targetRef", "targetEntity", "source"]);
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
const GROUND_IDS = Object.freeze(["soil", "concrete", "blanket", "foot_massage"]);
const GROUND_IDS = Object.freeze(["soil", "concrete", "blanket", "foot_massage", "ice"]);
const WEATHER_IDS = Object.freeze(["sunny", "cloudy", "light_rain"]);
const MOOD_IDS = Object.freeze(["relaxed", "calm", "prickly", "stinky", "weaklings", "zunda_party", "ball_party", "sun_party", "ant_overrun"]);
const STATE_IDS = Object.freeze(["idle", "sleep", "panic", "sunbath", "fight", "seek_food", "seek_bed", "wander"]);
@ -18,7 +18,7 @@
"grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi",
"sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h",
"splat", "food", "bed", "genkotsu"
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet"
]);
function enumIndex(list, value, fallback = 0) {
@ -50,6 +50,13 @@
const n = Number(value);
return Number.isFinite(n) ? n / scale : fallback;
}
function mortonKeyXY(x = 0, y = 0) {
const ix = Math.max(0, Math.min(65535, Math.round(Number(x) || 0)));
const iy = Math.max(0, Math.min(65535, Math.round(Number(y) || 0)));
let out = 0;
for (let i = 0; i < 16; i++) out += (((ix >> i) & 1) * (2 ** (2 * i))) + (((iy >> i) & 1) * (2 ** (2 * i + 1)));
return out;
}
function isPlainObject(value) {
if (!value || typeof value !== "object") return false;
const proto = Object.getPrototypeOf(value);
@ -189,6 +196,8 @@
q(worldRef?.lastBirthAt, 10, -9990),
q(worldRef?.maxGeneration, 1, 1),
q(worldRef?.deadCount, 1, 0),
worldRef?.worldSeed || global.TarinaiSeedFactory?.ensureWorldSeed?.(worldRef) || "",
q(worldRef?.birthSerial, 1, 0),
];
}
function applyCompactWorld(worldRef, arr = []) {
@ -197,6 +206,8 @@
const totalTime = u(arr[3], 10, 0);
worldRef.fieldType = enumValue(FIELD_IDS, arr[0], "garden");
worldRef.weather = enumValue(WEATHER_IDS, arr[4], "sunny");
worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory?.createWorldSeed?.() || `w${Date.now().toString(36)}`;
worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0));
worldRef.day = Math.max(1, Math.floor(totalTime / dayLength) + 1);
worldRef.time = totalTime - (worldRef.day - 1) * dayLength;
worldRef.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
@ -243,7 +254,6 @@
}
function compactTarinai(t, idx, familyIndex, tarinaiIndex, itemIndex) {
const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
const childIdx = (t.children || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
const timers = [
q(t.reproductionTimer, 10), q(t.fightCooldown, 10), q(t.loveMochiTimer, 10), q(t.fightMochiTimer, 10),
q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100)
@ -258,34 +268,35 @@
targetIdx = itemIndex.get(targetId) ?? itemIndex.get(t.target?.id) ?? -1;
}
return [
flagPack(t), t.name || "", spriteIndex(t.type || "smile"), q(t.x, 1), q(t.y, 1), q(t.adultScale ?? t.scale, 1000),
q(t.age, 10), q(t.lifeSpan, 10), q(t.generation, 1, 1), q(t.hunger, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1),
needsToArray(t.needs || t.needRaw || {}), personalityToArray(t.birthPersonality), personalityToArray(t.currentPersonality), geneticsToArray(t.genetics),
parentIdx, childIdx, compactRelationships(t, tarinaiIndex), timers, modes, pinIdx, nestIdx, [enumIndex(STATE_IDS, state), targetIdx], spriteIndex(t.goodMode || "smile")
flagPack(t), t.birthSeed || t.familyKey || t.id || `s${idx}`, q(t.x, 1), q(t.y, 1),
q(t.age, 10), q(t.generation, 1, 1), q(t.hunger, 1), q(t.energy, 1), q(t.circadianSleepPressure, 1),
needsToArray(t.needs || t.needRaw || {}), personalityToArray(t.currentPersonality),
parentIdx, compactRelationships(t, tarinaiIndex), timers, modes, pinIdx, nestIdx, [enumIndex(STATE_IDS, state), targetIdx]
];
}
function expandTarinai(row, index, worldRef) {
const TarinaiClass = global.Tarinai || (typeof Tarinai !== "undefined" ? Tarinai : null);
if (!TarinaiClass) throw new Error("Tarinai is not available");
const needs = arrayToNeeds(row[12]);
const age = u(row[6], 10);
const needs = arrayToNeeds(row[9]);
const age = u(row[4], 10);
const birthSeed = String(row[1] || `s${index}`);
const profile = global.TarinaiSeedFactory?.birthProfile?.(birthSeed) || {};
const opts = {
familyKey: `sf${index}`,
familyKey: birthSeed,
birthSeed,
birthSerial: global.TarinaiSeedFactory?.serialFromBirthSeed?.(birthSeed) || 0,
id: `st${index}`,
liveToken: 1,
name: row[1] || undefined,
type: spriteValue(row[2]),
birthPersonality: arrayToPersonality(row[13]),
currentPersonality: arrayToPersonality(row[14]),
genetics: arrayToGenetics(row[15]),
x: u(row[3], 1), y: u(row[4], 1), vx: 0, vy: 0,
adultScale: u(row[5], 1000, 0.28), scale: u(row[5], 1000, 0.28),
age, lifeSpan: u(row[7], 10, 1600), birthTime: (worldRef?.time || 0) - age, generation: Math.max(1, u(row[8], 1, 1)),
hunger: u(row[9], 1), loneliness: Number(needs.social) || 0, energy: u(row[10], 1), circadianSleepPressure: u(row[11], 1),
needs, state: enumValue(STATE_IDS, row[23]?.[0], "idle"), goodMode: spriteValue(row[24]),
currentPersonality: arrayToPersonality(row[10]),
x: u(row[2], 1), y: u(row[3], 1), vx: 0, vy: 0,
age, birthTime: (worldRef?.time || 0) - age, generation: Math.max(1, u(row[5], 1, 1)),
hunger: u(row[6], 1), loneliness: Number(needs.social) || 0, energy: u(row[7], 1), circadianSleepPressure: u(row[8], 1),
needs, state: enumValue(STATE_IDS, row[17]?.[0], "idle"),
isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)),
};
const t = new TarinaiClass(worldRef, opts);
t.familyKey = `sf${index}`;
t.familyKey = birthSeed;
t.birthSeed = birthSeed;
t.id = `st${index}`;
t.liveToken = 1;
t.needRaw = { ...needs };
@ -293,19 +304,18 @@
t.previousNeeds = { ...needs };
t.needRawBase = { ...needs };
flagApply(t, Number(row[0]) || 0);
t.__savedParentIdx = Array.isArray(row[16]) ? row[16] : [];
t.__savedChildIdx = Array.isArray(row[17]) ? row[17] : [];
t.__savedRelationships = Array.isArray(row[18]) ? row[18] : [];
const timers = Array.isArray(row[19]) ? row[19] : [];
t.__savedParentIdx = Array.isArray(row[11]) ? row[11] : [];
t.__savedRelationships = Array.isArray(row[12]) ? row[12] : [];
const timers = Array.isArray(row[13]) ? row[13] : [];
t.reproductionTimer = u(timers[0], 10); t.fightCooldown = u(timers[1], 10); t.loveMochiTimer = u(timers[2], 10); t.fightMochiTimer = u(timers[3], 10);
t.zunchiDiseaseSeverity = u(timers[4], 10); t.zunchiStain = u(timers[5], 10);
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10) };
t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1);
const modes = Array.isArray(row[20]) ? row[20] : [];
const modes = Array.isArray(row[14]) ? row[14] : [];
t.powerItemMode = enumValue(POWER_MODE_IDS, modes[0], "");
t.sizeItemMode = enumValue(SIZE_MODE_IDS, modes[1], "");
t.lifeItemMode = enumValue(LIFE_MODE_IDS, modes[2], "");
t.__savedPinIdx = row[21]; t.__savedNestIdx = row[22]; t.__savedStateInfo = row[23];
t.__savedPinIdx = row[15]; t.__savedNestIdx = row[16]; t.__savedStateInfo = row[17];
if (typeof t.refreshEffectiveSize === "function") t.refreshEffectiveSize();
return t;
}
@ -326,6 +336,7 @@
if (type === "ant_corpse") return [q(item.decayTimer, 10)];
if (type === "firecracker") return [q(item.fuseTimer, 10)];
if (typeof global.isPinType === "function" && global.isPinType(type)) return [enumIndex(PIN_STATE_IDS, item.pinState || "loose"), tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) return [q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0];
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
return [];
}
@ -376,21 +387,33 @@
item.fuseTimer = u(extra[0], 10); item.fuseMax = Math.max(item.fuseMax || 5, item.fuseTimer || 0);
} else if (typeof global.isPinType === "function" && global.isPinType(type)) {
item.pinState = enumValue(PIN_STATE_IDS, extra[0], "loose"); item.pinTargetId = tarinaiList[extra[1]]?.id || ""; item.pinAttachAngle = u(extra[2], 1000); item.pinAttachDistance = u(extra[3], 10); item.pinOffsetY = u(extra[4], 10);
} else if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) {
const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback) : u(extra[0], 1000, fallback);
if (type === "gate_fence") item.gateOpen = !!extra[1];
} else if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) {
item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining;
}
}
function createSnapshot(worldRef = global.world) {
if (!worldRef) throw new Error("world is not ready");
const tarinaiLive = (worldRef.tarinai || []).filter(t => t && !t.dead);
const itemsLive = (worldRef.items || []).filter(it => it && !it.dead && it.type !== "trace" && enumIndex(ITEM_TYPE_IDS, it.type || "", -1) >= 0);
const tarinaiLive = (worldRef.tarinai || [])
.filter(t => t && !t.dead)
.slice()
.sort((a, b) => mortonKeyXY(a.x, a.y) - mortonKeyXY(b.x, b.y) || String(a.id || "").localeCompare(String(b.id || "")));
const tarinaiIndex = new Map();
const familyIndex = new Map();
tarinaiLive.forEach((t, i) => { if (t.id) tarinaiIndex.set(t.id, i); if (t.familyKey) familyIndex.set(t.familyKey, i); });
const itemRecords = [];
for (const it of (worldRef.items || [])) {
if (!it || it.dead || it.type === "trace" || enumIndex(ITEM_TYPE_IDS, it.type || "", -1) < 0) continue;
const row = compactItem(it, itemRecords.length, tarinaiIndex);
if (row) itemRecords.push({ item: it, row });
}
itemRecords.sort((a, b) => (a.row[0] || 0) - (b.row[0] || 0) || mortonKeyXY(a.row[1], a.row[2]) - mortonKeyXY(b.row[1], b.row[2]) || String(a.item?.id || "").localeCompare(String(b.item?.id || "")));
const itemIndex = new Map();
itemsLive.forEach((it, i) => { if (it.id) itemIndex.set(it.id, i); });
const itemRows = [];
itemsLive.forEach((it, i) => { const row = compactItem(it, i, tarinaiIndex); if (row) itemRows.push(row); });
itemRecords.forEach((rec, i) => { if (rec.item?.id) itemIndex.set(rec.item.id, i); });
const itemRows = itemRecords.map(rec => rec.row);
return {
v: SNAPSHOT_VERSION,
a: "tj1",
@ -426,8 +449,18 @@
}
for (const t of worldRef.tarinai) {
t.parents = (t.__savedParentIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
t.children = (t.__savedChildIdx || []).map(i => worldRef.tarinai[i]?.familyKey).filter(Boolean);
delete t.__savedParentIdx; delete t.__savedChildIdx;
t.children = [];
}
for (const child of worldRef.tarinai) {
for (const parentIdx of child.__savedParentIdx || []) {
const parent = worldRef.tarinai[parentIdx];
if (parent?.familyKey && child.familyKey && !parent.children.includes(child.familyKey)) parent.children.push(child.familyKey);
}
}
const liveByFamilyKey = new Map((worldRef.tarinai || []).map(t => [t.familyKey, t]).filter(([id]) => id));
for (const t of worldRef.tarinai) {
t.parentNames = (t.parents || []).map(id => liveByFamilyKey.get(id)?.name || "").filter(Boolean);
delete t.__savedParentIdx;
}
const itemRows = Array.isArray(snapshot.i) ? snapshot.i : [];
for (let idx = 0; idx < itemRows.length; idx++) {
@ -472,6 +505,7 @@
}
worldRef.liveIdNext = Math.max(1, worldRef.tarinai.length + 1);
worldRef.liveIdSerial = Math.max(1, worldRef.tarinai.length + 1);
worldRef.birthSerial = Math.max(Number(worldRef.birthSerial) || 0, ...worldRef.tarinai.map(t => Number(t.birthSerial) || 0));
worldRef.family = {};
worldRef.familyVersion = (worldRef.familyVersion || 0) + 1;
for (const t of worldRef.tarinai) worldRef.recordFamily?.(t);

View file

@ -15,21 +15,26 @@ function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
}
class Tarinai { constructor(world, opts = {}) {
if (world && !opts.birthSeed && !opts.familyKey && !opts.id && globalThis.TarinaiSeedFactory?.prepareAddOptions) {
opts = globalThis.TarinaiSeedFactory.prepareAddOptions(world, opts) || opts;
}
this.world = world;
this.familyKey = opts.familyKey || opts.id || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`);
this.birthSeed = opts.birthSeed || opts.familyKey || opts.id || (globalThis.TarinaiSeedFactory?.childSeed?.(world?.worldSeed || "", Number(world?.birthSerial || 0) + 1, []) || (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`));
this.birthSerial = Number.isFinite(opts.birthSerial) ? Number(opts.birthSerial) : (globalThis.TarinaiSeedFactory?.serialFromBirthSeed?.(this.birthSeed) || 0);
const birthProfile = globalThis.TarinaiSeedFactory?.birthProfile?.(this.birthSeed, opts) || {};
this.familyKey = opts.familyKey || this.birthSeed;
this.id = opts.id || "";
this.liveToken = opts.liveToken || 0;
this.name = opts.name || makeName(world);
this.name = opts.name || birthProfile.name || makeName(world);
this.personality = "";
this.type = opts.type || baseSpriteId();
this.type = opts.type || birthProfile.type || baseSpriteId();
const typeMeta = SPRITES.find(s => s.id === this.type);
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = baseSpriteId();
this.birthPersonality = normalizePersonality(opts.birthPersonality);
if (!opts.birthPersonality) this.birthPersonality = randomBirthPersonality(this.familyKey);
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = birthProfile.type || baseSpriteId();
this.birthPersonality = normalizePersonality(opts.birthPersonality || birthProfile.birthPersonality);
this.currentPersonality = normalizePersonality(opts.currentPersonality || this.birthPersonality);
this.personalityDaily = opts.personalityDaily && typeof opts.personalityDaily === "object" ? JSON.parse(JSON.stringify(opts.personalityDaily)) : null;
ensurePersonality(this);
this.genetics = normalizeGenetics(opts.genetics, this.familyKey);
this.genetics = normalizeGenetics(opts.genetics || birthProfile.genetics, this.birthSeed);
this.trait = makeTrait(this.type);
this.trait.speed *= this.genetics.speedMul || 1;
this.birthTime = opts.birthTime ?? this.world.time;
@ -46,12 +51,12 @@ class Tarinai { constructor(world, opts = {}) {
this.vy = opts.vy ?? rand(-12, 12);
this.impulseVx = opts.impulseVx ?? 0;
this.impulseVy = opts.impulseVy ?? 0;
this.adultScale = Number.isFinite(opts.adultScale) ? opts.adultScale : (Number.isFinite(opts.baseAdultScale) ? opts.baseAdultScale : (Number.isFinite(opts.scale) && growthAtConstruct >= 1 ? opts.scale : adultScaleFromGenetics(this.familyKey, this.genetics)));
this.adultScale = Number.isFinite(opts.adultScale) ? opts.adultScale : (Number.isFinite(opts.baseAdultScale) ? opts.baseAdultScale : (Number.isFinite(opts.scale) && growthAtConstruct >= 1 ? opts.scale : (Number.isFinite(birthProfile.adultScale) ? birthProfile.adultScale : adultScaleFromGenetics(this.birthSeed, this.genetics))));
this.scale = Number.isFinite(opts.scale) ? opts.scale : (generationOpt > 1 ? scaleForGrowth(this.adultScale, growthAtConstruct) : this.adultScale);
this.radius = 56 * this.scale;
this.age = opts.age ?? rand(0, 20);
this.lifeSpan = opts.lifeSpan ?? rand(1400, 2200) * (this.genetics.lifeSpanMul || 1);
if (this.lifeSpan < 900) this.lifeSpan = rand(1200, 1800) * (this.genetics.lifeSpanMul || 1);
this.lifeSpan = opts.lifeSpan ?? (Number.isFinite(birthProfile.lifeSpan) ? birthProfile.lifeSpan : rand(1400, 2200) * (this.genetics.lifeSpanMul || 1));
if (this.lifeSpan < 900) this.lifeSpan = Number.isFinite(birthProfile.lifeSpan) ? birthProfile.lifeSpan : rand(1200, 1800) * (this.genetics.lifeSpanMul || 1);
this.generation = generationOpt;
this.hasPaired = opts.hasPaired ?? false;
this.hunger = opts.hunger ?? rand(16, 45);
@ -113,7 +118,7 @@ class Tarinai { constructor(world, opts = {}) {
this.wasSleepingLastTick = false;
this.blink = rand(0, 10);
this.lastSocial = 0;
this.goodMode = opts.goodMode ?? stableChoice(this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";
this.goodMode = opts.goodMode ?? birthProfile.goodMode ?? stableChoice(this.birthSeed || this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";
this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1);
this.visualFacing = opts.visualFacing ?? this.facing;
this.facingLockUntil = opts.facingLockUntil ?? 0;

View file

@ -421,8 +421,8 @@ function createBirthRitualActionSpec() {
id: "birth_ritual",
need: "social",
subNeed: "mate",
label: "繁殖の前ぶれをしている",
phrase: "繁殖の前ぶれをしている",
label: "へこへこしている",
phrase: "へこへこしている",
weight: 0,
state: "birth_ritual",
timerBacked: true,

View file

@ -38,7 +38,7 @@ function actionTextForReason(action = null, tarinai = null) {
const id = action?.id || behavior?.actionId || "";
const target = tarinai?.target || currentBehaviorTarget?.(tarinai) || null;
const name = target?.name || "\u76f8\u624b";
if (id === "birth_ritual") return target?.name ? `${name}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` : "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b";
if (id === "birth_ritual") return target?.name ? `${name}とへこへこしている` : "へこへこしている";
if (id === "fight_rival") {
const cause = String(behavior?.causeText || "");
if (cause.includes("\u3051\u3093\u304b\u3092\u58f2\u3089\u308c")) return "\u53cd\u6483\u3057\u3066\u3044\u308b";

View file

@ -440,8 +440,8 @@ function synchronizeActionTextFromState(tarinai) {
const catalogText = typeof window !== "undefined" && window.TEXT_CATALOG?.reasonLabel ? window.TEXT_CATALOG.reasonLabel(tarinai) : "";
if (state === "birth_ritual" || (tarinai.birthRitualTimer || 0) > 0.04) {
const partner = tarinai.world?.tarinai?.find?.(o => o && !o.dead && o.id === tarinai.birthPartnerId) || tarinai.target || null;
const label = partner?.name ? `${partner.name}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` : "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b";
setBehaviorText(tarinai, { need: "social", actionId: "birth_ritual", actionLabel: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b", reason: label, target: partner });
const label = partner?.name ? `${partner.name}\u3068へこへこしている` : "へこへこしている";
setBehaviorText(tarinai, { need: "social", actionId: "birth_ritual", actionLabel: "へこへこしている", reason: label, target: partner });
return;
}
if (state === "eat" || (tarinai.eatTimer || 0) > 0.04 || (tarinai.grassEatTimer || 0) > 0.04) {

View file

@ -36,7 +36,7 @@ function roleMatches(item, role) {
if (item.roles?.[role]) return true;
if (role === "food" || role === "drink" || role === "medicine") return effectiveFoodRoleMatches(item, role);
if (role === "sleepPlace") return item.type === "bed" || item.type === "nest_box";
if (role === "danger") return ["firecracker", "genkotsu", "pushpin", "splat"].includes(item.type);
if (role === "danger") return ["firecracker", "genkotsu", "pushpin", "oshibyo", "splat"].includes(item.type);
if (role === "grassMaterial") return item.type === "grass";
return false;
}

View file

@ -958,6 +958,7 @@ function protectSleepSession(tarinai, world, needs) {
const wasFighting = this.fightTimer > 0.04;
this.fightTimer = Math.max(0, this.fightTimer - dt);
this.fightCooldown = Math.max(0, this.fightCooldown - dt);
this.groundStumbleCooldown = Math.max(0, (this.groundStumbleCooldown || 0) - dt);
this.defeatedTimer = Math.max(0, this.defeatedTimer - dt);
if (wasFighting && this.fightTimer <= 0.04 && this.defeatedById) {
const winner = this.world.liveTarinaiById?.(this.defeatedById) || null;
@ -976,6 +977,23 @@ function protectSleepSession(tarinai, world, needs) {
this.grassEatTimer = Math.max(0, this.grassEatTimer - dt);
this.hurtTimer = Math.max(0, this.hurtTimer - dt);
this.fallTimer = Math.max(0, this.fallTimer - dt);
if ((this.world?.groundType || "soil") === "foot_massage"
&& (this.groundStumbleCooldown || 0) <= 0.04
&& (this.fallTimer || 0) <= 0.04
&& !this.dead
&& !["sleep", "fight", "panic", "birth_ritual", "frozen"].includes(String(this.state || ""))
&& Math.random() < dt * 0.055) {
const dir = Math.random() < 0.5 ? -1 : 1;
this.groundStumbleCooldown = rand(8.5, 16.0);
this.fallTimer = Math.max(this.fallTimer || 0, rand(0.86, 1.18));
this.fallMax = Math.max(this.fallMax || 1.05, this.fallTimer);
this.fallDir = dir;
this.vx += dir * rand(26, 52);
this.vy += rand(-18, 18);
this.world?.spawnFallEffect?.(this.x, this.y + this.radius * 0.45);
this.setActionState?.("idle", { target: null, reason: "足つぼの突起につまづいた。", sleeping: false, clearTarget: false });
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble?.("うっ", 1.6, "rgba(86,70,96,0.78)");
}
if (this.stress > 92 && this.state !== "sleep" && this.state !== "fight" && this.state !== "eat" && Math.random() < dt * 0.050) {
this.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
}
@ -1252,7 +1270,7 @@ function protectSleepSession(tarinai, world, needs) {
if (this.birthRitualTimer > 0.04) {
const partner = this.world.liveTarinaiById?.(this.birthPartnerId) || null;
this.setActionState?.("birth_ritual", { target: partner, reason: "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b" });
this.setActionState?.("birth_ritual", { target: partner, reason: "へこへこしている" });
synchronizeActionTextFromState(this);
return;
}
@ -1275,6 +1293,19 @@ function protectSleepSession(tarinai, world, needs) {
const d = dist(this, t);
if (d < bestD) { best = t; bestD = d; }
}
const maxFightContinueDistance = Math.max(150, (this.radius || 20) + (best?.radius || 20) + 116);
if (!best || bestD > maxFightContinueDistance) {
const rival = best || (this.fightTargetId ? this.world.liveTarinaiById?.(this.fightTargetId) : null);
this.fightTimer = 0;
this.fightTargetIds = [];
this.fightTargetId = null;
this.counterAttackFromId = null;
this.fightCooldown = Math.max(this.fightCooldown || 0, CONFIG.fightCooldown + rand(4, 8));
if (rival) this.world?.markFightPairCooldown?.(this, rival, CONFIG.fightCooldown + 6);
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(this, e => e && e.id === "fight_rival");
this.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
return;
}
const reasonText = best ? `${best.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u5165\u3089\u306a\u3044\u306e\u3067\u3001\u55a7\u5629\u3057\u3066\u3044\u308b\u3002` : "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u304c\u3044\u308b\u306e\u3067\u3001\u55a7\u5629\u3057\u3066\u3044\u308b\u3002";
this.setActionState?.("fight", { target: best, reason: reasonText });
if (typeof setBehaviorText === "function") setBehaviorText(this, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText, target: best, phase: "perform", source: "behavior" });

154
js/tarinai_seed_factory.js Normal file
View file

@ -0,0 +1,154 @@
"use strict";
(function (global) {
function hashString(value = "") {
const str = String(value || "");
let h = 2166136261;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0).toString(36);
}
function createWorldSeed() {
const cryptoObj = global.crypto;
if (cryptoObj?.getRandomValues) {
const buf = new Uint32Array(2);
cryptoObj.getRandomValues(buf);
return `w${buf[0].toString(36)}${buf[1].toString(36)}`;
}
return `w${Date.now().toString(36)}${Math.floor(Math.random() * 0xffffffff).toString(36)}`;
}
function ensureWorldSeed(worldRef = null) {
if (!worldRef) return createWorldSeed();
if (!worldRef.worldSeed) worldRef.worldSeed = createWorldSeed();
if (!Number.isFinite(worldRef.birthSerial)) worldRef.birthSerial = 0;
return worldRef.worldSeed;
}
function childSeed(worldSeed = "", serial = 0, parentSeeds = []) {
const safeSerial = Math.max(0, Math.floor(Number(serial) || 0));
const parents = (parentSeeds || []).filter(Boolean).join("+");
// 区切りを入れておくと、将来 birthSeed 自体を保存しない形式へ移る時に
// birthSerial を安全に取り出せる。
return `b${safeSerial.toString(36)}_${hashString(`${worldSeed}|${safeSerial}|${parents}`)}`;
}
function serialFromBirthSeed(seed = "") {
const match = /^b([0-9a-z]+)_/i.exec(String(seed || ""));
if (!match) return 0;
const n = parseInt(match[1], 36);
return Number.isFinite(n) && n > 0 ? n : 0;
}
function prepareAddOptions(worldRef, opts = {}) {
const next = { ...(opts || {}) };
ensureWorldSeed(worldRef);
if (!next.birthSeed) {
worldRef.birthSerial = Math.max(0, Number(worldRef.birthSerial) || 0) + 1;
next.birthSerial = worldRef.birthSerial;
const parentSeeds = Array.isArray(next.seedParents)
? next.seedParents.map(p => typeof p === "string" ? p : (p?.birthSeed || p?.familyKey || p?.id || ""))
: [];
next.birthSeed = childSeed(worldRef.worldSeed, next.birthSerial, parentSeeds);
} else {
if (!Number.isFinite(next.birthSerial)) {
const parsedSerial = serialFromBirthSeed(next.birthSeed);
if (parsedSerial > 0) next.birthSerial = parsedSerial;
}
if (Number.isFinite(next.birthSerial)) {
worldRef.birthSerial = Math.max(Number(worldRef.birthSerial) || 0, Number(next.birthSerial) || 0);
}
}
if (!next.familyKey) next.familyKey = next.birthSeed;
return next;
}
function seedUnit(seed = "", key = "") {
if (typeof stableUnit === "function") return stableUnit(seed, key);
return parseInt(hashString(`${seed}:${key}`).slice(0, 6), 36) / (36 ** 6);
}
function seedPick(seed = "", key = "", list = []) {
if (!list?.length) return null;
const i = Math.min(list.length - 1, Math.floor(seedUnit(seed, key) * list.length));
return list[i];
}
function seededName(seed = "") {
const len = 2 + Math.floor(seedUnit(seed, "name.len") * 6); // 2..7
const hasStop = len >= 2 && seedUnit(seed, "name.stop") < 0.30;
const middleLen = Math.max(0, len - 1 - (hasStop ? 1 : 0));
let name = "は";
for (let i = 0; i < middleLen; i++) name += seedUnit(seed, `name.u.${i}`) < 0.64 ? "う" : "ぅ";
if (hasStop) name += "っ";
return name;
}
function seededType(seed = "") {
const list = typeof spawnableSprites === "function" ? spawnableSprites().map(s => s.id).filter(Boolean) : ["smile"];
return seedPick(seed, "sprite.base", list) || "smile";
}
function seededGoodMode(seed = "") {
const list = typeof displayNormalSprites === "function" ? displayNormalSprites() : ["smile", "jito", "normal_smirk"];
return seedPick(seed, "sprite.good", list) || "smile";
}
function seededPersonality(seed = "") {
if (typeof randomBirthPersonality === "function") return randomBirthPersonality(seed);
return { aggression: 0, openness: 0, sociability: 0, neuroticism: 0 };
}
function seededGenetics(seed = "") {
if (typeof normalizeGenetics === "function") return normalizeGenetics(null, seed);
return { lifeSpanMul: 1, attackMul: 1, speedMul: 1, sizeMul: 1 };
}
function seededAdultScale(seed = "", genetics = null) {
if (typeof adultScaleFromGenetics === "function") return adultScaleFromGenetics(seed, genetics);
return 0.28;
}
function seededLifeSpan(seed = "", genetics = null) {
const g = genetics || seededGenetics(seed);
const base = 1400 + seedUnit(seed, "life.span") * 800;
let span = base * (g.lifeSpanMul || 1);
if (span < 900) span = (1200 + seedUnit(seed, "life.span.low") * 600) * (g.lifeSpanMul || 1);
return span;
}
function birthProfile(seed = "", opts = {}) {
const zunchiSlave = Boolean(opts.isZunchiSlave || opts.zunchiSlaveLocked);
const genetics = seededGenetics(seed);
const personality = zunchiSlave && typeof zunchiSlavePersonalityValue === "function" ? zunchiSlavePersonalityValue() : seededPersonality(seed);
return {
birthSeed: seed,
name: zunchiSlave ? "ずんちどれい" : seededName(seed),
type: zunchiSlave ? "zunchi_slave" : seededType(seed),
goodMode: zunchiSlave ? "zunchi_slave" : seededGoodMode(seed),
birthPersonality: personality,
genetics,
adultScale: seededAdultScale(seed, genetics),
lifeSpan: seededLifeSpan(seed, genetics),
};
}
function personalityArray(seed = "") {
const p = birthProfile(seed).birthPersonality || {};
return ["aggression", "openness", "sociability", "neuroticism"].map(k => Math.round(((Number(p[k]) || 0) + 1) * 100));
}
global.TarinaiSeedFactory = Object.freeze({
createWorldSeed,
ensureWorldSeed,
childSeed,
serialFromBirthSeed,
prepareAddOptions,
birthProfile,
personalityArray,
hashString,
});
})(typeof window !== "undefined" ? window : globalThis);

View file

@ -71,12 +71,24 @@ function updateFightBehavior(t, world, dt) {
if (!t || !world) return false;
if ((t.fightTimer || 0) > 0.04) {
const rival = currentBehaviorTarget(t) || (t.fightTargetId ? world.liveTarinaiById?.(t.fightTargetId) : null);
t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` : "\u55a7\u5629\u3057\u3066\u3044\u308b", sleeping: false });
const maxFightContinueDistance = Math.max(150, (t.radius || 20) + (rival?.radius || 20) + 116);
if (!isLiveTarinaiEntity(rival) || dist(t, rival) > maxFightContinueDistance) {
t.fightTimer = 0;
t.fightTargetIds = [];
t.fightTargetId = null;
t.counterAttackFromId = null;
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + rand(4, 8));
if (isLiveTarinaiEntity(rival)) world.markFightPairCooldown?.(t, rival, CONFIG.fightCooldown + 6);
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
t.setActionState?.("idle", { target: null, reason: "相手を見失った。", sleeping: false, clearTarget: true });
return true;
}
t.setActionState?.("fight", { target: rival || t.target, reason: rival?.name ? `${rival.name}と喧嘩している` : "喧嘩している", sleeping: false });
return true;
}
const candidate = conflictTargetFor(t, world, (t?.fightMochiTimer || 0) > 0.04 || isCurrentBehaviorForced(t) ? 24 : 42);
const current = currentBehaviorTarget(t);
const rival = isLiveTarinaiEntity(candidate?.target) ? candidate.target : (isLiveTarinaiEntity(current) ? current : nearestForcedFightTarget(t, world, isCurrentBehaviorForced(t) ? 760 : 360));
const rival = isLiveTarinaiEntity(candidate?.target) ? candidate.target : (isLiveTarinaiEntity(current) ? current : nearestForcedFightTarget(t, world, isCurrentBehaviorForced(t) ? 260 : 220));
if (!isLiveTarinaiEntity(rival)) return false;
t.conflictTargetId = rival.id;
if (dist(t, rival) > Math.max(42, (t.radius || 20) + (rival.radius || 20) + 12)) return moveToOrUse(t, rival, "seek_enemy", "\u6c17\u306b\u5165\u3089\u306a\u3044\u76f8\u624b\u306b\u5411\u304b\u3063\u3066\u3044\u308b");

View file

@ -18,7 +18,7 @@
closeCount += 1;
const fightMochiContact = this.world.canFightPair?.(this, o) && (this.hasFightMochiEffect() || o.hasFightMochiEffect());
if (fightMochiContact && d <= Math.max(42, this.radius + o.radius + 16)) {
if (fightMochiContact && (this.world.fightPairCooldownRemaining?.(this, o) || 0) <= 0.04 && this.fightCooldown <= 0.04 && o.fightCooldown <= 0.04 && d <= Math.max(42, this.radius + o.radius + 16)) {
this.conflictTargetId = o.id;
o.conflictTargetId = this.id;
this.conflictUrge = Math.max(this.conflictUrge || 0, 92);
@ -26,8 +26,8 @@
if (typeof applyNeedShock === "function") applyNeedShock(this, { social: dt * 36, safety: dt * 5 }, o);
if (typeof applyNeedShock === "function") applyNeedShock(o, { social: dt * 30, safety: dt * 5 }, this);
if (typeof queueForcedTarinaiBehavior === "function") {
queueForcedTarinaiBehavior(this, "fight_rival", { target: o, source: "fight_mochi", priority: 180, ttl: 8, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
queueForcedTarinaiBehavior(o, "fight_rival", { target: this, source: "fight_mochi", priority: 180, ttl: 8, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
queueForcedTarinaiBehavior(this, "fight_rival", { target: o, source: "fight_mochi", priority: 180, ttl: 3.2, retryDelay: 0.55, searchRange: 140, replaceSameSource: true, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
queueForcedTarinaiBehavior(o, "fight_rival", { target: this, source: "fight_mochi", priority: 180, ttl: 3.2, retryDelay: 0.55, searchRange: 140, replaceSameSource: true, causeText: "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c" });
}
}
@ -375,10 +375,12 @@
this.vx += ax * dt;
this.vy += ay * dt;
this.vx *= Math.pow(0.84, dt * 8);
this.vy *= Math.pow(0.84, dt * 8);
const iceGround = (this.world?.groundType || "soil") === "ice";
const moveFriction = iceGround ? 0.965 : 0.84;
this.vx *= Math.pow(moveFriction, dt * 8);
this.vy *= Math.pow(moveFriction, dt * 8);
const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1);
const maxV = (this.state === "panic" ? 116 : (this.state === "zunchi_sick" ? 54 : (this.state === "fight_sick" ? 68 : 76))) * Math.max(1, itemSpeedMul || 1) * (this.geneticStatRatio ? this.geneticStatRatio("speed") : 1) * (iceGround ? 1.35 : 1);
const v = Math.hypot(this.vx, this.vy);
if (v > maxV) {
this.vx = this.vx / v * maxV;

View file

@ -26,7 +26,7 @@ const TEXT_CATALOG = {
{
title: "草、地面、天気",
image: "assets/ui/tool_grass.webp",
text: "草は成長し、食料やかんたんベッドの材料になります。地面は土、コンクリート、ふわふわ毛布、足つぼを切り替えられ、草の発生やストレス増加に影響します。雨や時間帯も状態に関わります。",
text: "草は成長し、食料やかんたんベッドの材料になります。地面は土、コンクリート、ふわふわ毛布、足つぼ、氷を切り替えられ、草の発生やストレス増加に影響します。雨や時間帯も状態に関わります。",
},
{
title: "性格と行動傾向",
@ -36,7 +36,7 @@ const TEXT_CATALOG = {
{
title: "関係、家族、繁殖",
image: "assets/sprites/tarinai_27_birth_ritual.webp",
text: "親子、共同親、友好、敵対、恐怖が記録されます。条件がそろうと繁殖の前ぶれを行い、子が生まれます。子は親の性格傾向を受け継ぎ、世代情報は家系図に残ります。",
text: "親子、共同親、友好、敵対、恐怖が記録されます。条件がそろうとへこへこを行い、子が生まれます。子は親の性格傾向を受け継ぎ、世代情報は家系図に残ります。",
},
{
title: "病気と回復",
@ -56,7 +56,7 @@ const TEXT_CATALOG = {
{
title: "コロニーの見方",
image: "assets/sprites/tarinai_11_weak.webp",
text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。たりないデータでは選択中の個体の健康、性格、関係、行動理由、記録を追えます。",
text: "コロニー欄では個体数、死亡数、全体状態、地面、欲求平均、物を確認できます。たりないデータでは選択中の個体の健康、性格、関係、行動理由、記録を追えます。",
},
],};
@ -126,7 +126,16 @@ TEXT_CATALOG.targetLabelOverrides = Object.freeze({
});
TEXT_CATALOG.targetLabels = TEXT_CATALOG.targetLabelOverrides;
TEXT_CATALOG.stateLabel = function stateLabelFromCatalog(s) {
TEXT_CATALOG.idleGroundText = function idleGroundText(t = null) {
const ground = String(t?.world?.groundType || (typeof world !== "undefined" ? world?.groundType : "") || "soil");
if (ground === "foot_massage") return "足つぼに耐えている。";
if (ground === "blanket") return "毛布にうっとりしている。";
if (ground === "ice") return "氷の上でじっとしている。";
return "ぼーっとしている。";
};
TEXT_CATALOG.stateLabel = function stateLabelFromCatalog(s, t = null) {
if (String(s || "") === "idle") return TEXT_CATALOG.idleGroundText(t);
return TEXT_CATALOG.stateLabels?.[s] || s || "";
};
@ -161,7 +170,7 @@ TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
if (t.state === "intimidate") return target !== "\u306a\u3057" ? `${target}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b` : "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
if (t.state === "ant_intimidate") return "\u30a2\u30ea\u3092\u5a01\u5687\u3057\u3066\u3044\u308b";
if (t.state === "ant_attack") return "\u30a2\u30ea\u3092\u653b\u6483\u3057\u3066\u3044\u308b";
if (t.state === "birth_ritual") return t.target?.name ? `${t.target.name}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` : "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b";
if (t.state === "birth_ritual") return t.target?.name ? `${t.target.name}とへこへこしている` : "へこへこしている";
if (t.state === "sunbath") return "\u6674\u308c\u305f\u65e5\u306a\u305f\u3067\u30b9\u30c8\u30ec\u30b9\u3092\u4e0b\u3052\u3066\u3044\u308b";
if (t.state === "frozen") return "\u30d5\u30a3\u30fc\u30eb\u30c9\u5916\u3067\u51b7\u51cd\u4fdd\u5b58\u3055\u308c\u3066\u3044\u308b";
if (t.state === "seek_food") return target !== "\u306a\u3057" ? `${target}\u3092\u63a2\u3057\u3066\u3044\u308b` : "\u304a\u8179\u304c\u3059\u3044\u3066\u3044\u308b";
@ -180,8 +189,11 @@ TEXT_CATALOG.reasonLabel = function reasonLabelFromCatalog(t) {
if ((t.fearTimer || 0) > 0.08) return "\u6016\u304c\u3063\u3066\u3044\u308b";
if ((t.grassEatTimer || 0) > 0.04) return "\u8349\u3092\u98df\u3079\u3066\u3044\u308b";
const thought = String(t.thought || "").trim();
const cleanedThought = TEXT_CATALOG.cleanBehaviorText(thought, t.state || "");
if (["待機", "待っている", "待っている。", "ぼーっとしている", "ぼーっとしている。", "周囲を見ている"].includes(cleanedThought)) return TEXT_CATALOG.idleGroundText(t);
if (thought) return thought;
return "\u5468\u56f2\u3092\u898b\u3066\u3044\u308b";
if (t.state === "idle" || !t.state) return TEXT_CATALOG.idleGroundText(t);
return "周囲を見ている";
};
function stateLabel(s) { return TEXT_CATALOG.stateLabel(s); }

View file

@ -81,6 +81,32 @@ function bindUI() {
syncToolSizeBadges();
setActivePanelTab(ui.selectedCard?.classList.contains("hidden") ? "tools" : "selected");
function activeRotatableToolType() {
const type = typeof toolItemType === "function" ? toolItemType(world?.tool || "") : "";
return type && typeof isRotatableItemType === "function" && isRotatableItemType(type) ? type : "";
}
window.addEventListener("keydown", (e) => {
const tag = String(e.target?.tagName || "").toLowerCase();
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable) return;
const type = activeRotatableToolType();
if (!type || !world?.rotateToolAngle) return;
const key = String(e.key || "").toLowerCase();
const step = (e.shiftKey ? 5 : 45) * Math.PI / 180;
if (key === "q" || key === "[") {
e.preventDefault();
world.rotateToolAngle(type, -step);
render();
} else if (key === "e" || key === "]") {
e.preventDefault();
world.rotateToolAngle(type, step);
render();
} else if (key === "r") {
e.preventDefault();
world.setToolAngle?.(type, typeof defaultItemAngle === "function" ? defaultItemAngle(type) : 0);
render();
}
});
for (const toggle of document.querySelectorAll(".panel-card-toggle")) {
toggle.addEventListener("click", () => {
const card = toggle.closest(".collapsible-card");

View file

@ -37,6 +37,7 @@
btnEl.dataset.tip = desc;
btnEl.removeAttribute("title");
btnEl.setAttribute("aria-label", desc ? `地面を切り替える。現在は${label}${desc}` : `地面を切り替える。現在は${label}`);
global.TarinaiTooltips?.refresh?.(btnEl);
}
}

View file

@ -100,6 +100,14 @@ function screenToWorld(e) {
}
function resizeFieldFromWheel(e) {
const itemType = typeof toolItemType === "function" ? toolItemType(world?.tool || "") : "";
if ((e.shiftKey || e.altKey) && itemType && typeof isRotatableItemType === "function" && isRotatableItemType(itemType) && world?.rotateToolAngle) {
e.preventDefault();
const step = (e.altKey ? 1 : 5) * Math.PI / 180;
world.rotateToolAngle(itemType, (e.deltaY > 0 ? 1 : -1) * step);
render();
return;
}
if (!world.setFieldZoom) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();

View file

@ -175,7 +175,7 @@ function selectedInventoryHtml(t) {
}
function selectedStateText(t) {
return stateLabel(t?.state);
return window.TEXT_CATALOG?.stateLabel?.(t?.state, t) || stateLabel(t?.state);
}
function renderSelected() {
@ -211,7 +211,7 @@ function renderSelected() {
const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"];
const behaviorState = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior;
const snapshot = [
t.id, t.type, t.name, generationDisplay, t.state, t.thought, targetLabel(t.target),
t.id, world?.groundType || "soil", t.type, t.name, generationDisplay, t.state, t.thought, targetLabel(t.target),
behaviorState?.need || "", behaviorState?.actionId || "", behaviorState?.label || "", behaviorState?.reason || "", behaviorState?.source || "", behaviorState?.text || "", needKeys.map(key => `${key}:${t.needs?.[key] ?? 0}`).join(","),
fmt(t.age), fmt(t.lifeSpan), fmt(t.hunger), fmt(t.loneliness),
fmt(t.energy), fmt(t.stress), fmt(t.mood),
@ -313,6 +313,14 @@ function stateLabel(s) {
return window.TEXT_CATALOG?.stateLabel?.(s) || s || "";
}
function groundAwareIdleText(t) {
return window.TEXT_CATALOG?.idleGroundText?.(t) || "ぼーっとしている。";
}
function isIdleDisplayText(value = "") {
return ["待機", "待っている", "待っている。", "ぼーっとしている", "ぼーっとしている。", "周囲を見ている"].includes(String(value || "").trim());
}
function reasonLabel(t) {
if (!t) return "\u306a\u3057";
const behavior = typeof globalThis.behaviorText === "function"
@ -320,7 +328,7 @@ function reasonLabel(t) {
: String(typeof getTarinaiBehaviorText === "function" ? getTarinaiBehaviorText(t) : (t.behavior?.text || t.behavior?.reason || "")).trim();
if (behavior) {
const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(behavior, t.state || "") || behavior;
if (normalized === "待機" || normalized === "待っている" || normalized === "待っている。") return "ぼーっとしている";
if (isIdleDisplayText(normalized)) return groundAwareIdleText(t);
return normalized;
}
const state = String(t.state || "");
@ -331,10 +339,11 @@ function reasonLabel(t) {
if (behaviorState?.reason || behaviorState?.text) {
const raw = behaviorState.reason || behaviorState.text;
const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(raw, behaviorState?.actionId || t.state || "") || raw;
if (normalized === "待機" || normalized === "待っている" || normalized === "待っている。") return "ぼーっとしている";
if (isIdleDisplayText(normalized)) return groundAwareIdleText(t);
return normalized;
}
if (live === "待機" || live === "待っている" || live === "待っている。") return "ぼーっとしている";
if (isIdleDisplayText(live)) return groundAwareIdleText(t);
if ((!live || live === "なし") && (String(t.state || "") === "idle" || !t.state)) return groundAwareIdleText(t);
return live || "\u306a\u3057";
}

View file

@ -33,7 +33,8 @@ function renderToolPalette() {
const label = document.createElement("span");
label.className = "tool-label";
label.textContent = def.label || toolId;
btn.title = label.textContent;
btn.setAttribute("aria-label", label.textContent);
btn.removeAttribute("title");
btn.appendChild(label);
attachToolSpritePreview(btn, def, toolId);
if (toolId === world?.tool) btn.classList.add("selected");

View file

@ -1,6 +1,8 @@
"use strict";
(function (global) {
let activeTooltipElement = null;
function ensureTooltipPopover() {
let pop = document.getElementById("toolTipPopover");
if (!pop) {
@ -19,9 +21,11 @@
}
function showTooltip(el) {
if (!el || !document.documentElement.contains(el)) return;
const tip = resolveTip(el);
if (!tip) return;
const pop = ensureTooltipPopover();
activeTooltipElement = el;
pop.textContent = tip;
pop.classList.remove("hidden");
const rect = el.getBoundingClientRect();
@ -35,19 +39,38 @@
}
function hideTooltip() {
activeTooltipElement = null;
ensureTooltipPopover().classList.add("hidden");
}
function refreshTooltip(el = null) {
const target = el || activeTooltipElement;
if (!target) return;
// Do not let unrelated UI refreshes steal an already-visible tooltip.
// Ground UI updates run during stats refresh; without this guard, hovering a
// tool can suddenly turn into the current ground description.
if (el && activeTooltipElement && el !== activeTooltipElement) return;
if (!document.documentElement.contains(target)) { hideTooltip(); return; }
const pop = ensureTooltipPopover();
if (pop.classList.contains("hidden")) return;
showTooltip(target);
}
function bindTooltip(el, tip = "") {
if (!el) return;
if (typeof tip === "function") el.__tarinaiTipProvider = tip;
else if (tip) el.dataset.tip = String(tip);
// Native browser title tooltips can linger and overlap the custom popover.
el.removeAttribute?.("title");
if (el.dataset.tipBound === "1") return;
el.dataset.tipBound = "1";
el.addEventListener("mouseenter", () => showTooltip(el));
el.addEventListener("focus", () => showTooltip(el));
el.addEventListener("mouseleave", hideTooltip);
el.addEventListener("blur", hideTooltip);
el.addEventListener("pointerdown", () => {
if (activeTooltipElement === el) hideTooltip();
});
}
function applyToolPaletteTooltips() {
@ -64,6 +87,8 @@
bind: bindTooltip,
show: showTooltip,
hide: hideTooltip,
refresh: refreshTooltip,
active: () => activeTooltipElement,
applyToolPalette: applyToolPaletteTooltips,
});
global.bindTooltip = bindTooltip;

View file

@ -1,8 +1,8 @@
"use strict";
(function () {
const APP_VERSION = "16.07.20";
const APP_BUILD = "noise-cleanup";
const APP_VERSION = "16.07.32";
const APP_BUILD = "fan-magnet-gate-ice-rotation-v3";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -12,6 +12,7 @@ class World { constructor() {
this.tool = "observe";
this.toolSize = "medium";
this.toolSizes = {};
this.toolAngles = {};
this.tarinai = [];
this.frozenTarinai = [];
this.items = [];
@ -42,6 +43,8 @@ class World { constructor() {
this.pointer = { x: 0, y: 0, inside: false, motion: 0, movedAt: 0 };
this.lastPhase = "";
this.weather = "sunny";
this.worldSeed = window.TarinaiSeedFactory?.createWorldSeed?.() || `w${Date.now().toString(36)}`;
this.birthSerial = 0;
this.fieldType = "garden";
this.groundType = "soil";
this.fieldZoom = 1;

View file

@ -467,6 +467,11 @@
winner.totalFightWins = (winner.totalFightWins || 0) + 1;
loser.totalFightLosses = (loser.totalFightLosses || 0) + 1;
this.emit?.("fight:ended", { winner, loser });
this.markFightPairCooldown?.(winner, loser, CONFIG.fightCooldown + 8);
for (const t of [winner, loser]) {
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + rand(4, 8));
if (typeof clearForcedBehaviorQueue === "function") clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
}
winner.adjustPersonality?.("aggression", 0.018, "after winning fights.");
loser.adjustPersonality?.("aggression", -0.018, "after losing fights.");
this.maybeApplyZunchiSlaveByFightRecord?.(winner, loser);

View file

@ -3,6 +3,64 @@
(function (global) {
const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_environment.js");
function normalizedAngleLocal(angle = 0, fallback = 0) {
if (typeof normalizedItemAngle === "function") return normalizedItemAngle(angle, fallback);
const twoPi = Math.PI * 2;
const n = Number(angle);
const base = Number.isFinite(n) ? n : fallback;
return ((base % twoPi) + twoPi) % twoPi;
}
function angleForItemLocal(it) {
if (typeof itemAngleFor === "function") return itemAngleFor(it);
const type = String(it?.type || "");
const fallback = (type === "fence_v" || type === "bounce_fence_v") ? Math.PI / 2 : 0;
return normalizedAngleLocal(it?.angle, fallback);
}
function orientedRectAabb(cx, cy, halfW, halfH, angle) {
const c = Math.cos(angle);
const s = Math.sin(angle);
const ex = Math.abs(c) * halfW + Math.abs(s) * halfH;
const ey = Math.abs(s) * halfW + Math.abs(c) * halfH;
return { left: cx - ex, right: cx + ex, top: cy - ey, bottom: cy + ey, cos: c, sin: s };
}
function rectLocalPoint(r, x, y) {
const dx = x - (r.cx ?? 0);
const dy = y - (r.cy ?? 0);
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
const s = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0);
return { x: dx * c + dy * s, y: -dx * s + dy * c };
}
function rectWorldNormal(r, nx, ny) {
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
const s = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0);
return { x: nx * c - ny * s, y: nx * s + ny * c };
}
function segmentIntersectsOrientedRectLocal(x1, y1, x2, y2, r, padding = 0) {
const a = rectLocalPoint(r, x1, y1);
const b = rectLocalPoint(r, x2, y2);
const minX = -(r.halfW || 0) - padding;
const maxX = (r.halfW || 0) + padding;
const minY = -(r.halfH || 0) - padding;
const maxY = (r.halfH || 0) + padding;
let t0 = 0, t1 = 1;
const clip = (p, q) => {
if (Math.abs(p) < 1e-9) return q >= 0;
const t = q / p;
if (p < 0) { if (t > t1) return false; if (t > t0) t0 = t; }
else { if (t < t0) return false; if (t < t1) t1 = t; }
return true;
};
const dx = b.x - a.x;
const dy = b.y - a.y;
return clip(-dx, a.x - minX) && clip(dx, maxX - a.x) && clip(-dy, a.y - minY) && clip(dy, maxY - a.y);
}
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
nearest(entity, types, maxDist = Infinity) {
let best = null, bestD = maxDist;
@ -41,14 +99,69 @@
},
fenceRect(it) {
const vertical = it?.type === "fence_v";
const horizontal = it?.type === "fence_h";
if (!vertical && !horizontal) return null;
const type = it?.type || "";
if (!this.isFenceType(type)) return null;
const bounce = type === "bounce_fence" || type === "bounce_fence_v";
const gate = type === "gate_fence";
const len = Math.max(112, (it.r || 42) * 3.55);
const thick = Math.max(10, (it.r || 42) * 0.31);
const halfW = vertical ? thick / 2 : len / 2;
const halfH = vertical ? len / 2 : thick / 2;
return { left: it.x - halfW, right: it.x + halfW, top: it.y - halfH, bottom: it.y + halfH, vertical, horizontal };
const angle = angleForItemLocal(it);
const halfW = len / 2;
const halfH = thick / 2;
const aabb = orientedRectAabb(it.x || 0, it.y || 0, halfW, halfH, angle);
return {
left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom,
cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)),
bounce, gate, gateOpen: Boolean(it.gateOpen),
restitution: bounce ? 1.08 : undefined, minBounceSpeed: bounce ? 260 : undefined
};
},
deviceRect(it) {
const type = String(it?.type || "");
if (type !== "fan" && type !== "magnet") return null;
const r = it.r || (type === "fan" ? 32 : 30);
const angle = angleForItemLocal(it);
const cx = (it.x || 0) - Math.cos(angle) * r * (type === "fan" ? 0.18 : 0.08);
const cy = (it.y || 0) - Math.sin(angle) * r * (type === "fan" ? 0.18 : 0.08);
const halfW = Math.max(22, r * (type === "fan" ? 0.92 : 0.88));
const halfH = Math.max(18, r * (type === "fan" ? 0.72 : 0.78));
const aabb = orientedRectAabb(cx, cy, halfW, halfH, angle);
return {
left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom,
cx, cy, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
device: true, type, item: it, restitution: 0.68,
};
},
fanInfluenceShape(it) {
if (!it || it.type !== "fan") return null;
const angle = angleForItemLocal(it);
const r = it.r || 32;
return {
type: "fan",
x: it.x || 0,
y: it.y || 0,
angle,
range: Math.max(170, r * 7.2),
nearWidth: Math.max(42, r * 1.0),
farWidth: Math.max(42, r * 4.1),
};
},
magnetInfluenceShape(it) {
if (!it || it.type !== "magnet") return null;
const angle = angleForItemLocal(it);
const r = it.r || 30;
return {
type: "magnet",
x: (it.x || 0) + Math.cos(angle) * r * 1.10,
y: (it.y || 0) + Math.sin(angle) * r * 1.10,
angle,
range: Math.max(150, r * 7.2),
};
},
nestBoxCapacity() {
@ -126,9 +239,14 @@
solidObstacleRects(it) {
if (!it || it.dead) return [];
if (this.isFenceType(it.type)) {
if (it.type === "gate_fence" && it.gateOpen) return [];
const rect = this.fenceRect(it);
return rect ? [{ ...rect, type: it.type, item: it }] : [];
}
if (it.type === "fan" || it.type === "magnet") {
const rect = this.deviceRect?.(it);
return rect ? [{ ...rect, type: it.type, item: it }] : [];
}
if (it.type === "nest_box") return this.nestBoxSolidRects(it);
return [];
},
@ -168,27 +286,93 @@
pushTarinaiOutOfRect(t, r, rr) {
if (!t || !r) return false;
const cx = clamp(t.x, r.left, r.right);
const cy = clamp(t.y, r.top, r.bottom);
let dx = t.x - cx;
let dy = t.y - cy;
let d = Math.hypot(dx, dy);
if (d >= rr) return false;
if (d < 0.001) {
const left = Math.abs(t.x - r.left);
const right = Math.abs(r.right - t.x);
const top = Math.abs(t.y - r.top);
const bottom = Math.abs(r.bottom - t.y);
const m = Math.min(left, right, top, bottom);
if (m === left) { dx = -1; dy = 0; d = 1; }
else if (m === right) { dx = 1; dy = 0; d = 1; }
else if (m === top) { dx = 0; dy = -1; d = 1; }
else { dx = 0; dy = 1; d = 1; }
let cx, cy, dx, dy, d, nx, ny;
let insidePenetration = 0;
if (r.oriented) {
const local = rectLocalPoint(r, t.x, t.y);
const clx = clamp(local.x, -(r.halfW || 0), r.halfW || 0);
const cly = clamp(local.y, -(r.halfH || 0), r.halfH || 0);
dx = local.x - clx;
dy = local.y - cly;
d = Math.hypot(dx, dy);
if (d >= rr) return false;
if (d < 0.001) {
const left = Math.abs(local.x + (r.halfW || 0));
const right = Math.abs((r.halfW || 0) - local.x);
const top = Math.abs(local.y + (r.halfH || 0));
const bottom = Math.abs((r.halfH || 0) - local.y);
const m = Math.min(left, right, top, bottom);
insidePenetration = m;
if (m === left) { dx = -1; dy = 0; d = 1; }
else if (m === right) { dx = 1; dy = 0; d = 1; }
else if (m === top) { dx = 0; dy = -1; d = 1; }
else { dx = 0; dy = 1; d = 1; }
}
const n = rectWorldNormal(r, dx / d, dy / d);
nx = n.x; ny = n.y;
cx = t.x - dx; cy = t.y - dy;
} else {
cx = clamp(t.x, r.left, r.right);
cy = clamp(t.y, r.top, r.bottom);
dx = t.x - cx;
dy = t.y - cy;
d = Math.hypot(dx, dy);
if (d >= rr) return false;
if (d < 0.001) {
const left = Math.abs(t.x - r.left);
const right = Math.abs(r.right - t.x);
const top = Math.abs(t.y - r.top);
const bottom = Math.abs(r.bottom - t.y);
const m = Math.min(left, right, top, bottom);
if (m === left) { dx = -1; dy = 0; d = 1; }
else if (m === right) { dx = 1; dy = 0; d = 1; }
else if (m === top) { dx = 0; dy = -1; d = 1; }
else { dx = 0; dy = 1; d = 1; }
}
nx = dx / d;
ny = dy / d;
}
const push = Math.min(rr - d + 0.8, Math.max(10, rr * 0.92));
t.x += dx / d * push;
t.y += dy / d * push;
if (Math.abs(dx) > Math.abs(dy)) t.vx *= -0.18;
const push = r.oriented && insidePenetration > 0
? Math.min(insidePenetration + rr + 0.8, Math.max(18, rr * 2.8))
: Math.min(rr - d + 0.8, Math.max(10, rr * 0.92));
t.x += nx * push;
t.y += ny * push;
if (r.bounce) {
const restitution = Number.isFinite(r.restitution) ? r.restitution : 1.08;
const vx = t.vx || 0;
const vy = t.vy || 0;
const toward = vx * nx + vy * ny;
if (toward < 0) {
t.vx = vx - (1 + restitution) * toward * nx;
t.vy = vy - (1 + restitution) * toward * ny;
} else {
const kick = Math.max(46, Math.min(120, rr * 3.2));
t.vx = vx + nx * kick;
t.vy = vy + ny * kick;
}
const minBounceSpeed = Math.max(0, Number(r.minBounceSpeed || 0) || 0);
if (minBounceSpeed > 0) {
const currentOut = (t.vx || 0) * nx + (t.vy || 0) * ny + (t.impulseVx || 0) * nx + (t.impulseVy || 0) * ny;
const now = this.time || 0;
if (currentOut < minBounceSpeed && ((t.lastBounceFenceImpulseAt || -999) + 0.16 <= now || toward < 0)) {
const add = minBounceSpeed - currentOut;
t.impulseVx = clamp((t.impulseVx || 0) + nx * add, -520, 520);
t.impulseVy = clamp((t.impulseVy || 0) + ny * add, -520, 520);
t.lastBounceFenceImpulseAt = now;
}
}
t.vx = clamp(t.vx || 0, -520, 520);
t.vy = clamp(t.vy || 0, -520, 520);
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.18);
if (t.bubble && (this.time || 0) >= (t.lastBounceFenceBubbleAt || -999) + 0.55) {
t.lastBounceFenceBubbleAt = this.time || 0;
t.bubble("!", 0.45, "rgba(80,130,210,0.72)");
}
if ((this.time || 0) >= (t.lastBounceFenceEffectAt || -999) + 0.08) {
t.lastBounceFenceEffectAt = this.time || 0;
this.effects?.push(new Effect("ring", t.x, t.y, { size: Math.max(18, rr * 1.15), life: 0.18, color: "rgba(82,153,230,0.46)" }));
}
} else if (Math.abs(dx) > Math.abs(dy)) t.vx *= -0.18;
else t.vy *= -0.18;
return true;
},
@ -197,16 +381,21 @@
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return;
const rr = Math.max(8, t.radius * 0.74);
let pushed = false;
let bounced = false;
for (const rect of this.nearbySolidObstacleRects(t.x, t.y, rr + 150)) {
if (rect?.type === "nest_box" && this.shouldIgnoreNestBoxCollisionFor(t, rect.item)) continue;
if (this.pushTarinaiOutOfRect(t, rect, rr)) pushed = true;
if (this.pushTarinaiOutOfRect(t, rect, rr)) {
pushed = true;
if (rect?.bounce) bounced = true;
}
}
if (pushed) {
const pad = CONFIG.worldPadding + Math.max(2, t.radius * 0.18);
t.x = clamp(t.x, pad, this.w - pad);
t.y = clamp(t.y, pad, this.h - pad);
t.vx = clamp(t.vx || 0, -130, 130);
t.vy = clamp(t.vy || 0, -130, 130);
const maxV = bounced ? 520 : 130;
t.vx = clamp(t.vx || 0, -maxV, maxV);
t.vy = clamp(t.vy || 0, -maxV, maxV);
}
},
@ -216,6 +405,7 @@
segmentIntersectsRect(x1, y1, x2, y2, r) {
if (!r) return false;
if (r.oriented) return segmentIntersectsOrientedRectLocal(x1, y1, x2, y2, r, 0);
if ((x1 >= r.left && x1 <= r.right && y1 >= r.top && y1 <= r.bottom) || (x2 >= r.left && x2 <= r.right && y2 >= r.top && y2 <= r.bottom)) return true;
const intersects = (ax, ay, bx, by, cx, cy, dx, dy) => {
const ccw = (px, py, qx, qy, rx, ry) => (ry - py) * (qx - px) > (qy - py) * (rx - px);
@ -232,8 +422,13 @@
const cy = (y1 + y2) / 2;
const maxD = Math.hypot(x2 - x1, y2 - y1) / 2 + 120;
for (const rect of this.nearbySolidObstacleRects(cx, cy, maxD + padding + 170)) {
const r = { left: rect.left - padding, right: rect.right + padding, top: rect.top - padding, bottom: rect.bottom + padding };
if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true;
if (rect?.oriented) {
const r = { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding };
if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true;
} else {
const r = { left: rect.left - padding, right: rect.right + padding, top: rect.top - padding, bottom: rect.bottom + padding };
if (this.segmentIntersectsRect(x1, y1, x2, y2, r)) return true;
}
}
return false;
},

View file

@ -40,6 +40,8 @@
this.liveIdFree = [];
this.liveIdSerial = 0;
this.liveTarinai = new Map();
this.worldSeed = global.TarinaiSeedFactory?.createWorldSeed?.() || `w${Date.now().toString(36)}`;
this.birthSerial = 0;
this.lastBirthAt = -999;
this.maxGeneration = 1;
this.family = {};
@ -62,10 +64,8 @@
this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
seedPopulation = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
const grassCount = field.grass ?? 18;
const types = spawnableSprites().map(s => s.id);
for (let i = 0; i < seedPopulation; i++) {
this.addTarinai({
type: types[i % types.length],
x: rand(80, this.w - 80),
y: rand(80, this.h - 80),
generation: 1,
@ -410,6 +410,7 @@
},
addTarinai(opts = {}) {
opts = global.TarinaiSeedFactory?.prepareAddOptions?.(this, opts) || opts;
const t = new Tarinai(this, opts);
this.assignTarinaiLiveId(t);
this.tarinai.push(t);
@ -501,7 +502,7 @@
b.birthRitualRole = 1;
a.birthRitualLeader = true;
b.birthRitualLeader = false;
const ritualReason = "\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b";
const ritualReason = "へこへこしている";
const applyRitualAction = (actor, partner) => {
if (!actor) return false;
if (typeof actor.setActionState === "function") {
@ -526,10 +527,10 @@
const bBehavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(b) : b.behavior;
const aLoveCause = (aBehavior?.source === "love_mochi" || (a.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${b.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
const bLoveCause = (bBehavior?.source === "love_mochi" || (b.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${a.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`;
const aText = typeof buildReasonText === "function" ? buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` }, a, this, { causeText: aLoveCause }) : `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`;
const bText = typeof buildReasonText === "function" ? buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause }) : `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`;
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u7e41\u6b96\u306e\u524d\u3076\u308c\u3092\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a });
const aText = typeof buildReasonText === "function" ? buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068へこへこしている` }, a, this, { causeText: aLoveCause }) : `${b.name || "\u76f8\u624b"}\u3068へこへこしている`;
const bText = typeof buildReasonText === "function" ? buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068へこへこしている` }, b, this, { causeText: bLoveCause }) : `${a.name || "\u76f8\u624b"}\u3068へこへこしている`;
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068へこへこしている`, reasonText: aText, causeText: aLoveCause, target: b });
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068へこへこしている`, reasonText: bText, causeText: bLoveCause, target: a });
}
a.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
b.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
@ -616,32 +617,18 @@
spawnChild(a, b, index = 0, total = 1) {
const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave);
const inherited = Math.random() < 0.56 ? a.type : b.type;
const mutation = zunchiSlaveChild ? "zunchi_slave" : (Math.random() < 0.18 ? baseSpriteId() : inherited);
const childSeed = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}-${index}`;
const childGenetics = inheritedGenetics(a, b, childSeed);
const childAdultScale = adultScaleFromGenetics(childSeed, childGenetics);
const childBirthPersonality = zunchiSlaveChild ? zunchiSlavePersonalityValue() : personalityFromParents(a, b);
const child = this.addTarinai({
familyKey: childSeed,
type: mutation,
birthPersonality: childBirthPersonality,
currentPersonality: childBirthPersonality,
genetics: childGenetics,
adultScale: childAdultScale,
name: zunchiSlaveChild ? "\u305a\u3093\u3061\u3069\u308c\u3044" : makeName(),
seedParents: [a, b],
isZunchiSlave: zunchiSlaveChild,
age: 0,
x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18,
y: (a.y + b.y) / 2 + rand(-14, 14) + (total > 1 ? rand(-10, 10) : 0),
scale: scaleForGrowth(childAdultScale, 0),
hunger: rand(20, 40),
loneliness: rand(30, 56),
energy: rand(55, 95),
stress: rand(0, 14),
need: "",
generation: Math.max(a.generation, b.generation) + 1,
lifeSpan: rand(1500, 2200) * (childGenetics.lifeSpanMul || 1),
parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)],
parentNames: [a.name, b.name],
birthTime: this.time,
@ -708,6 +695,9 @@
if (alreadyFighting) return true;
if ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04) return false;
if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
const maxForcedFightDistance = Math.max(142, (a.radius || 20) + (b.radius || 20) + 96);
if (dist(a, b) > maxForcedFightDistance) return false;
if ((a.fightCooldown || 0) > 0.04 || (b.fightCooldown || 0) > 0.04) return false;
for (const t of [a, b]) {
t.birthRitualTimer = 0;
t.birthRitualMax = 0;
@ -715,7 +705,6 @@
t.birthRitualLeader = false;
t.birthRitualRole = 0;
t.postBirthPeaceTimer = 0;
t.fightCooldown = 0;
t.intimidateTimer = 0;
t.intimidateTargetId = null;
t.intimidatedTimer = 0;
@ -850,7 +839,9 @@
const allowForcedStart = Boolean(opts?.forced);
if (!allowForcedStart && ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04 || (a.intimidateTimer || 0) > 0.04 || (b.intimidateTimer || 0) > 0.04)) return false;
if (!allowForcedStart && this.fightPairCooldownRemaining?.(a, b) > 0.04) return false;
this.markFightPairCooldown?.(a, b, 3.4);
const maxStartFightDistance = Math.max(150, (a.radius || 20) + (b.radius || 20) + 104);
if (dist(a, b) > maxStartFightDistance) return false;
this.markFightPairCooldown?.(a, b, CONFIG.fightCooldown + 6);
const initiator = opts?.initiator === b ? b : a;
const receiver = initiator === a ? b : a;
const causeFor = (self, other) => {

View file

@ -149,7 +149,7 @@
function isEditableItem(item) {
return item && !item.dead && item.type === "signboard";
return item && !item.dead && (item.type === "signboard" || item.type === "gate_fence");
}
function findEditableItemAt(worldRef, x, y) {
@ -167,6 +167,16 @@
function openEditableItemEditor(item, worldRef) {
if (!isEditableItem(item)) return false;
if (item.type === "signboard") return openSignboardEditor(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 ? "ゲート柵を開けた。" : "ゲート柵を閉めた。", "observe");
if (typeof showToast === "function") showToast(item.gateOpen ? "ゲート柵: 開" : "ゲート柵: 閉");
global.render?.();
return true;
}
return false;
}
@ -175,7 +185,70 @@
return Boolean(def?.placeable && typeof toolItemType === "function" && toolItemType(tool));
}
function findDuplicatorAt(worldRef, x, y) {
let best = null;
let bestD = Infinity;
const runtime = global.TarinaiDuplicatorRuntime;
for (let i = (worldRef?.items || []).length - 1; i >= 0; i -= 1) {
const it = worldRef.items[i];
if (!it || it.dead || it.type !== "duplicator") continue;
const r = it.r || 34;
const centerD = distXY(x, y, it.x, it.y);
const load = runtime?.loadPoint?.(it) || { x: it.x + r * 0.62, y: it.y + r * 0.24 };
const slotD = distXY(x, y, load.x, load.y);
const hit = Math.max(44, r * 1.45);
if (centerD <= hit || slotD <= Math.max(30, r * 0.86)) {
const d = Math.min(centerD, slotD);
if (d < bestD) { best = it; bestD = d; }
}
}
return best;
}
function directSetDuplicatorAt(worldRef, x, y, itemType = "") {
const type = String(itemType || "");
if (!type || type === "duplicator") return false;
const runtime = global.TarinaiDuplicatorRuntime;
const loadType = runtime?.loadTypeForItem?.({ type, amount: 1, foodServingsRemaining: 1, dead: false }) || "";
if (!loadType) return false;
const duplicator = findDuplicatorAt(worldRef, x, y);
if (!duplicator) return false;
const ok = runtime?.setStoredType?.(duplicator, loadType, worldRef, { direct: true, force: false });
if (!ok) return false;
duplicator.duplicatorLoadSuppressedUntil = Math.max(duplicator.duplicatorLoadSuppressedUntil || 0, (worldRef?.time || 0) + 0.45);
if (typeof showToast === "function") showToast(`複製機: ${duplicator.storedFoodLabel || toolLabel(loadType)}をセット`);
worldRef.drawListDirty = true;
global.render?.();
return true;
}
function clearOverlappingGrassBedsForPlacement(worldRef, item) {
if (!worldRef || !item || item.type === "grass_bed" || item.type === "trace" || item.type === "splat") return 0;
const radius = Math.max(16, item.r || item.radius || 12);
let removed = 0;
for (const bed of worldRef.nearbyItems?.(item.x, item.y, radius + 72) || worldRef.items || []) {
if (!bed || bed.dead || bed.type !== "grass_bed") continue;
const hit = radius * 0.95 + Math.max(14, (bed.r || 18) * 0.95);
if (distXY(item.x, item.y, bed.x, bed.y) > hit) continue;
const ok = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, bed, {
reason: "grass-bed-ignored-by-placement",
userReason: "置きものが置かれた",
wake: true,
});
if (ok) removed += 1;
}
return removed;
}
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
findDuplicatorAt(x, y) {
return findDuplicatorAt(this, x, y);
},
directSetDuplicatorAt(x, y, itemType = "") {
return directSetDuplicatorAt(this, x, y, itemType);
},
fencePlacementBlocked(item) {
if (!item || !this.isFenceType(item.type)) return false;
const rect = this.fenceRect(item);
@ -195,7 +268,7 @@
importantPlacementOverlapBlocked(item) {
if (!item || item.dead) return true;
const important = new Set(["grass", "fence_v", "fence_h", "nest_box", "water"]);
const important = new Set(["grass", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet", "nest_box", "water"]);
if (!important.has(item.type)) return false;
const placingGrass = item.type === "grass";
const itemRects = this.solidObstacleRects(item);
@ -314,7 +387,7 @@
const original = this.placementProbeItem(item, start.x, start.y);
if (allowOriginal && !this.placementBlocked(original)) return start;
const solid = this.isFenceType(item.type) || item.type === "nest_box";
const solid = this.isFenceType(item.type) || item.type === "nest_box" || item.type === "fan" || item.type === "magnet";
const searchRadius = maxRadius || (solid ? Math.max(150, (item.r || 42) * 4.2) : Math.max(90, (item.r || 16) * 3.2));
const count = attempts || (solid ? 72 : 36);
const golden = Math.PI * (3 - Math.sqrt(5));
@ -373,11 +446,16 @@
showToast(this.isFenceType(item.type) ? "\u305d\u3053\u306b\u306f\u67f5\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3002" : "\u305d\u3053\u306b\u306f\u914d\u7f6e\u3067\u304d\u307e\u305b\u3093\u3002");
return null;
}
if (dropped) {
item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : (isPinType(item.type) ? 0.48 : 0.58));
if (dropped && (item.type === "genkotsu" || item.type === "stone" || isPinType(item.type))) {
item.dropMax = item.type === "genkotsu" ? 0.22 : (item.type === "stone" ? 0.72 : 0.48);
item.dropTimer = item.dropMax;
item.dropImpactDone = false;
} else {
item.dropMax = 0;
item.dropTimer = 0;
item.dropImpactDone = true;
}
clearOverlappingGrassBedsForPlacement(this, item);
const placed = this.addItem?.(item, `place:${item.type}`);
if (!placed) {
if (item.type === "grass") showToast(`草はこのフィールドでは${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}本までです。`);
@ -385,7 +463,9 @@
}
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}`);
if (typeof global.render === "function") global.render();
return item;
},
@ -436,7 +516,7 @@
if (tool === "new") {
const spot = this.edgeSpawnPoint(x, y, 42, true);
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), generation: 1, entryTimer: 2.0 });
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, generation: 1, entryTimer: 2.0 });
t.wanderAngle = spot.angle;
this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
return;
@ -445,9 +525,11 @@
if (this.useToolAt?.(x, y)) return;
const itemType = toolItemType(tool);
if (itemType && this.directSetDuplicatorAt?.(x, y, itemType)) return;
if (itemType) {
let px = x, py = y;
const rawItem = this.applyToolSize(new Item(itemType, px, py));
if (typeof isRotatableItemType === "function" && isRotatableItemType(itemType)) rawItem.angle = this.toolAngleFor ? this.toolAngleFor(itemType) : (typeof defaultItemAngle === "function" ? defaultItemAngle(itemType) : 0);
// Placement uses the same footprint that the preview validates.
const placed = this.placeItem(rawItem, true);
if (!placed) return;

View file

@ -1,13 +1,13 @@
"use strict";
(function (global) {
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker"]);
const REALTIME_ITEM_TYPES = new Set(["ball", "genkotsu", "firecracker", "fan", "magnet"]);
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
const SCHEDULED_ITEM_TYPES = [
"ball", "genkotsu", "firecracker", "pushpin", "oshibyo",
"plushie", "grass_bed", "zunchi", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h", "water_bowl",
"stone", "bed", "nest_box", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "fan", "magnet", "water_bowl",
];
function forScheduledItems(worldRef, visitor) {
@ -135,7 +135,7 @@
if ((worldRef.groundGrassMultiplier?.() ?? 1) <= 0) return false;
worldRef.grassGrowthTimer = (worldRef.grassGrowthTimer || 0) + dt;
const field = worldRef.fieldType || "garden";
const growthSpeedMul = 2.0;
const growthSpeedMul = 6.0;
const interval = (field === "park" ? 1.8 : (field === "cage" ? 3.2 : 2.4)) / growthSpeedMul;
if (worldRef.grassGrowthTimer < interval) return false;
worldRef.grassGrowthTimer = 0;
@ -349,7 +349,7 @@
function spawnAmbientTarinai(worldRef, dt) {
if (worldRef.tarinai.length > 0 && worldRef.tarinai.length <= 10 && worldRef.time - (worldRef.lastBirthAt || -999) > 12 && Math.random() < dt * 0.022) {
const spot = worldRef.edgeSpawnPoint(null, null, 42, true);
const t = worldRef.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, type: baseSpriteId(), entryTimer: 2.0 });
const t = worldRef.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, entryTimer: 2.0 });
t.wanderAngle = spot.angle;
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35);
worldRef.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
@ -358,7 +358,7 @@
function spawnAmbientGrass(worldRef, dt) {
if ((worldRef.groundGrassMultiplier?.() ?? 1) <= 0) return;
if ((worldRef.canAddGrass?.(1) ?? true) && Math.random() < dt * 0.045) {
if ((worldRef.canAddGrass?.(1) ?? true) && Math.random() < dt * 0.070) {
const spot = worldRef.findGrassPlantingSpot(rand(60, worldRef.w - 60), rand(60, worldRef.h - 60), {
allowOriginal: true,
minRadius: 28,

View file

@ -147,6 +147,9 @@
this.drawListDirty = true;
window.TarinaiRender?.invalidateRenderCaches?.("ground-type");
window.TarinaiGroundUI?.update?.(this);
if (window.uiCache) window.uiCache.selectedSnapshot = "";
if (typeof window.renderSelected === "function") window.renderSelected();
if (typeof renderSelected === "function") renderSelected();
if (typeof document !== "undefined") {
const groundTextEl = document.getElementById("statGroundType");
if (groundTextEl) groundTextEl.textContent = this.groundLabel?.() || "土";
@ -173,7 +176,32 @@
},
isFenceType(type = "") {
return type === "fence_v" || type === "fence_h";
return type === "fence_v" || type === "fence_h" || type === "bounce_fence" || type === "bounce_fence_v" || type === "gate_fence";
},
toolAngleFor(type = "") {
const key = String(type || "");
const fallback = typeof defaultItemAngle === "function" ? defaultItemAngle(key) : 0;
const value = this.toolAngles?.[key];
return typeof normalizedItemAngle === "function" ? normalizedItemAngle(value, fallback) : (Number.isFinite(Number(value)) ? Number(value) : fallback);
},
setToolAngle(type = "", angle = 0, { silent = false } = {}) {
const key = String(type || "");
if (!key || !(typeof isRotatableItemType === "function" && isRotatableItemType(key))) return false;
if (!this.toolAngles) this.toolAngles = {};
const fallback = typeof defaultItemAngle === "function" ? defaultItemAngle(key) : 0;
const next = typeof normalizedItemAngle === "function" ? normalizedItemAngle(angle, fallback) : angle;
this.toolAngles[key] = next;
this.drawListDirty = true;
if (!silent && typeof showToast === "function") showToast(`向き: ${Math.round(next * 180 / Math.PI)}°`);
return true;
},
rotateToolAngle(type = "", delta = 0, options = {}) {
const key = String(type || "");
const current = this.toolAngleFor ? this.toolAngleFor(key) : (typeof defaultItemAngle === "function" ? defaultItemAngle(key) : 0);
return this.setToolAngle?.(key, current + Number(delta || 0), options) || false;
},
baseZoomMin() {
return CONFIG.fieldZoomMin || 0.42;

View file

@ -378,7 +378,7 @@ def check_current_save_format() -> None:
save_storage = read_text(ROOT / "js/save_storage.js")
restore_coordinator = read_text(ROOT / "js/restore_coordinator.js")
required_snapshot = [
"const SNAPSHOT_VERSION = 4",
"const SNAPSHOT_VERSION = 5",
"a: \"tj1\"",
"function compactWorld",
"function compactTarinai",

View file

@ -1,11 +1,11 @@
"use strict";
const APP_VERSION = "16.07.20";
const APP_VERSION = "16.07.32";
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const v = `v=${APP_VERSION}`;
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
const coreScriptNames = [
"version", "event_bus", "registry_base", "disease_registry", "sound_pack", "item_registry", "data", "ground_types", "input_mode_manager", "math", "physics_helpers", "assets", "audio", "perf_profiler", "render", "sim_core", "structures", "items", "item_type_initializers", "item_lifecycle_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "world_update_phases", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "snapshot_system", "restore_coordinator", "save_codec", "save_storage", "save_system", "patch_helpers", "freeze_store", "freeze_snapshot_adapter", "freeze_panel", "freeze_system", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
"version", "event_bus", "registry_base", "disease_registry", "sound_pack", "item_registry", "data", "ground_types", "input_mode_manager", "math", "physics_helpers", "assets", "audio", "perf_profiler", "render", "sim_core", "tarinai_seed_factory", "structures", "items", "item_type_initializers", "item_lifecycle_runtime", "structure_lifecycle", "item_render_runtime", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_social_move_life", "tarinai_render", "world", "world_view", "family_graph", "world_family_social", "world_combat_effects", "world_environment", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "world_update_phases", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "snapshot_system", "restore_coordinator", "save_codec", "save_storage", "save_system", "patch_helpers", "freeze_store", "freeze_snapshot_adapter", "freeze_panel", "freeze_system", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
];
const CORE_ASSETS = [
"./",