not baaad
This commit is contained in:
parent
16074232aa
commit
fbdac9ebf1
44 changed files with 2152 additions and 454 deletions
|
|
@ -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",
|
||||
|
|
|
|||
47
css/base.css
47
css/base.css
|
|
@ -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"); }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
192
index.html
192
index.html
|
|
@ -7,11 +7,11 @@
|
|||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=16.07.17" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
|
||||
<link rel="stylesheet" href="css/base.css?v=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">同じ道具ボタンをもう一度押すと、対応道具の大きさが小・中・大で切り替わります。</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="地面をきりかえる"><strong id="statGroundType">土</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="コロニーの推移グラフ" 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">家系図をリセットしますか?</h2>
|
||||
<p class="hint">現在の親子関係と世代情報を消去します。個体その物量や庭の状態は消えません。</p>
|
||||
<p class="hint">現在の親子関係と世代情報を消去します。個体そのものや庭の状態は消えません。</p>
|
||||
<div class="field-dialog-actions archive-reset-actions">
|
||||
<button id="archiveResetCancelBtn" class="btn" type="button">キャンセル</button>
|
||||
<button id="archiveResetConfirmBtn" class="btn danger" type="button">リセットする</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>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
121
js/items.js
121
js/items.js
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
158
js/render.js
158
js/render.js
|
|
@ -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);
|
||||
|
|
|
|||
303
js/save_codec.js
303
js/save_codec.js
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?.("ロードに失敗しました。呪文を確認してください。");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -421,8 +421,8 @@ function createBirthRitualActionSpec() {
|
|||
id: "birth_ritual",
|
||||
need: "social",
|
||||
subNeed: "mate",
|
||||
label: "繁殖の前ぶれをしている",
|
||||
phrase: "繁殖の前ぶれをしている",
|
||||
label: "へこへこしている",
|
||||
phrase: "へこへこしている",
|
||||
weight: 0,
|
||||
state: "birth_ritual",
|
||||
timerBacked: true,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
154
js/tarinai_seed_factory.js
Normal 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);
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
btnEl.dataset.tip = desc;
|
||||
btnEl.removeAttribute("title");
|
||||
btnEl.setAttribute("aria-label", desc ? `地面を切り替える。現在は${label}。${desc}` : `地面を切り替える。現在は${label}`);
|
||||
global.TarinaiTooltips?.refresh?.(btnEl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
"./",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue