This commit is contained in:
33333-33333 2026-08-08 15:31:31 +09:00
commit 28591233f3
85 changed files with 946 additions and 267 deletions

View file

@ -1,4 +1,4 @@
# たりない観察 v39.18.3
# たりない観察 v39.18.6
## 実績システム
@ -12,7 +12,7 @@
実績状態とサーバー資格情報は、通常保存、予備保存、同一タブ保存、IndexedDBへ世代付きで保存します。起動時は同じ世代の状態を追加方向に統合し、明示的な実績リセット時だけ世代を進めます。
v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデータ、実績スペルv9・v10、旧バイナリセーブを読み込めます。旧データは現行形式へ正規化され、新規保存には廃止済みフィールドを書きません。
v39.18.6は、v39.18.0以前の実績保存、旧ワールド実績メタデータ、実績スペルv9・v10、旧バイナリセーブを読み込めます。旧データは現行形式へ正規化され、新規保存には廃止済みフィールドを書きません。
## サーバー同期
@ -25,7 +25,7 @@ v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデー
- 古い端末からリセット前の状態を復活させない
- プレイヤーIDと所有秘密鍵を耐久保存から復旧
共有達成率は、サーバーがゲームシミュレーションを再実行した検証値ではなく、クライアント端末の報告値です。UIとAPIでは「端末報告ベース」と明示します。
共有達成率は、サーバーがゲームシミュレーションを再実行した検証値ではなく、クライアント端末の報告値です。UIでは共有値を「達成率」「達成人数」と表示します。
## 旧サーバーデータの自動移行
@ -42,7 +42,7 @@ v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデー
旧ブラウザーに22文字のプレイヤーIDが残っている場合、クライアントはUUIDへ上書きする前に専用領域へ退避し、初回セッションで一度だけ新しい所有秘密鍵へ引き継ぎます。引継ぎ済みの旧IDを別の秘密鍵で再取得することはできません。
旧版から直接v39.18.3へ更新したブラウザーは自動引継ぎできます。v39.18.1を既に起動して旧22文字IDがブラウザーから失われている場合、サーバーデータだけから端末との本人対応を再構成することはできません。解除履歴と共有集計自体は正常に移行されます。
旧版から直接v39.18.6へ更新したブラウザーは自動引継ぎできます。v39.18.1を既に起動して旧22文字IDがブラウザーから失われている場合、サーバーデータだけから端末との本人対応を再構成することはできません。解除履歴と共有集計自体は正常に移行されます。
## 実績APIの保存先

View file

@ -1,5 +1,5 @@
{
"version": "39.18.3",
"version": "39.18.18",
"css": [
"css/base.css",
"css/layout.css",

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -78,10 +78,19 @@ button {
min-width: 0;
}
.room-links {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 6px;
flex: 0 0 auto;
}
.room-link {
text-decoration: none;
white-space: nowrap;
background: rgba(255,255,255,0.86);
text-align: center;
}
h1 {
@ -386,6 +395,7 @@ h1 {
word-break: normal;
overflow-wrap: anywhere;
text-shadow: 0 1px 0 rgba(255,255,255,0.62);
white-space: pre-line;
}
.tool.tool-has-canvas-icon { --tool-icon: none; grid-template-columns: 31px minmax(0,1fr); min-height: 47px; padding-top: 4px; }

View file

@ -4,16 +4,16 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</title>
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.18.3" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.18.3" />
<link rel="manifest" href="manifest.webmanifest?v=39.18.3" />
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.18.18" />
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.18.18" />
<link rel="manifest" href="manifest.webmanifest?v=39.18.18" />
<meta name="theme-color" content="#244d3a" />
<link rel="shortcut icon" href="favicon.ico?v=39.18.3" />
<link rel="stylesheet" href="css/base.css?v=39.18.3" />
<link rel="stylesheet" href="css/layout.css?v=39.18.3" />
<link rel="stylesheet" href="css/panel.css?v=39.18.3" />
<link rel="stylesheet" href="css/components.css?v=39.18.3" />
<link rel="stylesheet" href="css/mobile.css?v=39.18.3" />
<link rel="shortcut icon" href="favicon.ico?v=39.18.18" />
<link rel="stylesheet" href="css/base.css?v=39.18.18" />
<link rel="stylesheet" href="css/layout.css?v=39.18.18" />
<link rel="stylesheet" href="css/panel.css?v=39.18.18" />
<link rel="stylesheet" href="css/components.css?v=39.18.18" />
<link rel="stylesheet" href="css/mobile.css?v=39.18.18" />
</head>
<body>
<div id="loadingScreen" class="loading-screen" aria-live="polite">
@ -29,7 +29,10 @@
<div id="app">
<header class="topbar">
<div class="top-left">
<a class="btn room-link" href="https://2012r2.nishi.boats/~333/">333&#x306E;&#x90E8;&#x5C4B;&#x306B;&#x79FB;&#x52D5;</a>
<div class="room-links" style="display:flex;flex-direction:column;align-items:stretch;gap:6px;">
<a class="btn room-link" style="display:block;text-align:center;" href="https://2012r2.nishi.boats/~333/">333&#x306E;&#x90E8;&#x5C4B;&#x306B;&#x79FB;&#x52D5;</a>
<a class="btn room-link" style="display:block;text-align:center;" href="https://seesaawiki.jp/informalzunda/d/%a5%b2%a1%bc%a5%e0%3a%20%a4%bf%a4%ea%a4%ca%a4%a4%b4%d1%bb%a1">&#x5730;&#x4E0B;&#x7262;&#x306B;&#x623B;&#x308B;</a>
</div>
<div class="brand">
<h1>&#x305F;&#x308A;&#x306A;&#x3044;&#x89B3;&#x5BDF;</h1>
</div>
@ -307,176 +310,221 @@
</div>
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
<script src="js/version.js?v=39.18.3" defer></script>
<script src="js/event_bus.js?v=39.18.3" defer></script>
<script src="js/domain_ids.js?v=39.18.3" defer></script>
<script src="js/registry_base.js?v=39.18.3" defer></script>
<script src="js/disease_registry.js?v=39.18.3" defer></script>
<script src="js/sound_pack.js?v=39.18.3" defer></script>
<script src="js/deterministic_helpers.js?v=39.18.3" defer></script>
<script src="js/item_tool_metadata.js?v=39.18.3" defer></script>
<script src="js/item_tool_definitions.js?v=39.18.3" defer></script>
<script src="js/item_visual_definitions.js?v=39.18.3" defer></script>
<script src="js/item_food_definitions.js?v=39.18.3" defer></script>
<script src="js/item_effect_definitions.js?v=39.18.3" defer></script>
<script src="js/item_registry.js?v=39.18.3" defer></script>
<script src="js/data.js?v=39.18.3" defer></script>
<script src="js/ground_types.js?v=39.18.3" defer></script>
<script src="js/input_mode_manager.js?v=39.18.3" defer></script>
<script src="js/math.js?v=39.18.3" defer></script>
<script src="js/geometry_helpers.js?v=39.18.3" defer></script>
<script src="js/collision_footprint_system.js?v=39.18.3" defer></script>
<script src="js/physics_helpers.js?v=39.18.3" defer></script>
<script src="js/placement_preview_system.js?v=39.18.3" defer></script>
<script src="js/pin_attachment_system.js?v=39.18.3" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.18.3" defer></script>
<script src="js/mechanical_system.js?v=39.18.3" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.18.3" defer></script>
<script src="js/constraint_system.js?v=39.18.3" defer></script>
<script src="js/physics_world_system.js?v=39.18.3" defer></script>
<script src="js/physics_projection_system.js?v=39.18.3" defer></script>
<script src="js/circuit_board_system.js?v=39.18.3" defer></script>
<script src="js/signal_system.js?v=39.18.3" defer></script>
<script src="js/assets.js?v=39.18.3" defer></script>
<script src="js/audio.js?v=39.18.3" defer></script>
<script src="js/perf_profiler.js?v=39.18.3" defer></script>
<script src="js/display_helpers.js?v=39.18.3" defer></script>
<script src="js/render.js?v=39.18.3" defer></script>
<script src="js/sim_core.js?v=39.18.3" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.18.3" defer></script>
<script src="js/structures.js?v=39.18.3" defer></script>
<script src="js/item_type_catalog.js?v=39.18.3" defer></script>
<script src="js/items.js?v=39.18.3" defer></script>
<script src="js/item_type_initializers.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_support.js?v=39.18.3" defer></script>
<script src="js/item_update_policy.js?v=39.18.3" defer></script>
<script src="js/fire_runtime_system.js?v=39.18.3" defer></script>
<script src="js/robot_cleaner_system.js?v=39.18.3" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.18.3" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.18.3" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.18.3" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.18.3" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.18.3" defer></script>
<script src="js/item_environment_hazard_system.js?v=39.18.3" defer></script>
<script src="js/item_dynamic_system.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.18.3" defer></script>
<script src="js/update_step_pipeline_runner.js?v=39.18.3" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.18.3" defer></script>
<script src="js/item_runtime.js?v=39.18.3" defer></script>
<script src="js/structure_lifecycle.js?v=39.18.3" defer></script>
<script src="js/memorial_bell_system.js?v=39.18.3" defer></script>
<script src="js/item_render_helpers.js?v=39.18.3" defer></script>
<script src="js/item_render_runtime.js?v=39.18.3" defer></script>
<script src="js/burn_motion_util.js?v=39.18.3" defer></script>
<script src="js/ants.js?v=39.18.3" defer></script>
<script src="js/health.js?v=39.18.3" defer></script>
<script src="js/tarinai.js?v=39.18.3" defer></script>
<script src="js/tarinai_action_spec.js?v=39.18.3" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.18.3" defer></script>
<script src="js/tarinai_identity_social.js?v=39.18.3" defer></script>
<script src="js/tarinai_action_state.js?v=39.18.3" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.18.3" defer></script>
<script src="js/tarinai_item_effects.js?v=39.18.3" defer></script>
<script src="js/tarinai_needs_core.js?v=39.18.3" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.18.3" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.18.3" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.18.3" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.18.3" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.18.3" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.18.3" defer></script>
<script src="js/seesaw_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.18.3" defer></script>
<script src="js/tarinai_needs_items.js?v=39.18.3" defer></script>
<script src="js/tarinai_food_prototype_mixin.js?v=39.18.3" defer></script>
<script src="js/tarinai_direct_feeding_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.18.3" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.18.3" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.18.3" defer></script>
<script src="js/tarinai_runtime.js?v=39.18.3" defer></script>
<script src="js/tarinai_render.js?v=39.18.3" defer></script>
<script src="js/world.js?v=39.18.3" defer></script>
<script src="js/world_view.js?v=39.18.3" defer></script>
<script src="js/family_graph.js?v=39.18.3" defer></script>
<script src="js/world_reset_presets.js?v=39.18.3" defer></script>
<script src="js/world_family_social.js?v=39.18.3" defer></script>
<script src="js/impact_core_system.js?v=39.18.3" defer></script>
<script src="js/impact_response_system.js?v=39.18.3" defer></script>
<script src="js/collision_response_system.js?v=39.18.3" defer></script>
<script src="js/world_combat_effects.js?v=39.18.3" defer></script>
<script src="js/world_environment.js?v=39.18.3" defer></script>
<script src="js/world_temperature_system.js?v=39.18.3" defer></script>
<script src="js/world_pathfinding_system.js?v=39.18.3" defer></script>
<script src="js/world_grass_placement_system.js?v=39.18.3" defer></script>
<script src="js/world_spatial_budget.js?v=39.18.3" defer></script>
<script src="js/world_ants_system.js?v=39.18.3" defer></script>
<script src="js/weather_system.js?v=39.18.3" defer></script>
<script src="js/item_update_scheduler.js?v=39.18.3" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.18.3" defer></script>
<script src="js/tarinai_update_policy.js?v=39.18.3" defer></script>
<script src="js/simulation_environment_system.js?v=39.18.3" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.18.3" defer></script>
<script src="js/simulation_effects_system.js?v=39.18.3" defer></script>
<script src="js/simulation_creature_system.js?v=39.18.3" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.18.3" defer></script>
<script src="js/simulation_ambient_system.js?v=39.18.3" defer></script>
<script src="js/simulation_systems.js?v=39.18.3" defer></script>
<script src="js/system_order.js?v=39.18.3" defer></script>
<script src="js/simulation.js?v=39.18.3" defer></script>
<script src="js/colony_situation_system.js?v=39.18.3" defer></script>
<script src="js/world_update.js?v=39.18.3" defer></script>
<script src="js/world_tool_actions.js?v=39.18.3" defer></script>
<script src="js/world_placement_log.js?v=39.18.3" defer></script>
<script src="js/world_event_effects.js?v=39.18.3" defer></script>
<script src="js/command_dispatcher.js?v=39.18.3" defer></script>
<script src="js/text_catalog.js?v=39.18.3" defer></script>
<script src="js/ui.js?v=39.18.3" defer></script>
<script src="js/game_dialogs.js?v=39.18.3" defer></script>
<script src="js/achievement_catalog.js?v=39.18.3" defer></script>
<script src="js/achievements.js?v=39.18.3" defer></script>
<script src="js/ui_helpers.js?v=39.18.3" defer></script>
<script src="js/ui_log.js?v=39.18.3" defer></script>
<script src="js/ui_selected.js?v=39.18.3" defer></script>
<script src="js/save_schema.js?v=39.18.3" defer></script>
<script src="js/snapshot_system.js?v=39.18.3" defer></script>
<script src="js/restore_coordinator.js?v=39.18.3" defer></script>
<script src="js/history_system.js?v=39.18.3" defer></script>
<script src="js/save_codec.js?v=39.18.3" defer></script>
<script src="js/save_storage.js?v=39.18.3" defer></script>
<script src="js/save_system.js?v=39.18.3" defer></script>
<script src="js/ui_tooltips.js?v=39.18.3" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.18.3" defer></script>
<script src="js/ui_ground.js?v=39.18.3" defer></script>
<script src="js/ui_tools.js?v=39.18.3" defer></script>
<script src="js/ui_input_shared.js?v=39.18.3" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.18.3" defer></script>
<script src="js/ui_input_touch.js?v=39.18.3" defer></script>
<script src="js/ui_input_mouse.js?v=39.18.3" defer></script>
<script src="js/ui_bind.js?v=39.18.3" defer></script>
<script src="js/ui_charts.js?v=39.18.3" defer></script>
<script src="js/ui_family_data.js?v=39.18.3" defer></script>
<script src="js/ui_family_async.js?v=39.18.3" defer></script>
<script src="js/ui_family_layout.js?v=39.18.3" defer></script>
<script src="js/ui_family_paths.js?v=39.18.3" defer></script>
<script src="js/ui_family_render.js?v=39.18.3" defer></script>
<script src="js/main.js?v=39.18.3" defer></script>
<script src="js/debug_tools.js?v=39.18.3" defer></script>
<script>
(() => {
const replacements = [
["端末報告ベース", "達成率"],
["報告プレイヤー", "達成人数"],
];
const replaceText = value => replacements.reduce((text, [from, to]) => text.replaceAll(from, to), String(value || ""));
const normalizeNode = node => {
if (!node) return;
if (node.nodeType === Node.TEXT_NODE) {
const next = replaceText(node.nodeValue);
if (next !== node.nodeValue) node.nodeValue = next;
return;
}
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.hasAttribute("aria-label")) {
const current = node.getAttribute("aria-label");
const next = replaceText(current);
if (next !== current) node.setAttribute("aria-label", next);
}
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
while (walker.nextNode()) {
const textNode = walker.currentNode;
const next = replaceText(textNode.nodeValue);
if (next !== textNode.nodeValue) textNode.nodeValue = next;
}
node.querySelectorAll?.("[aria-label]").forEach(element => {
const current = element.getAttribute("aria-label");
const next = replaceText(current);
if (next !== current) element.setAttribute("aria-label", next);
});
};
const list = document.getElementById("achievementList");
if (!list) return;
normalizeNode(list);
new MutationObserver(records => {
for (const record of records) {
if (record.type === "characterData") normalizeNode(record.target);
for (const node of record.addedNodes || []) normalizeNode(node);
}
}).observe(list, { childList: true, subtree: true, characterData: true });
})();
</script>
<script src="js/version.js?v=39.18.18" defer></script>
<script src="js/event_bus.js?v=39.18.18" defer></script>
<script src="js/domain_ids.js?v=39.18.18" defer></script>
<script src="js/registry_base.js?v=39.18.18" defer></script>
<script src="js/disease_registry.js?v=39.18.18" defer></script>
<script src="js/sound_pack.js?v=39.18.18" defer></script>
<script src="js/deterministic_helpers.js?v=39.18.18" defer></script>
<script src="js/item_tool_metadata.js?v=39.18.18" defer></script>
<script src="js/item_tool_definitions.js?v=39.18.18" defer></script>
<script src="js/item_visual_definitions.js?v=39.18.18" defer></script>
<script src="js/item_food_definitions.js?v=39.18.18" defer></script>
<script src="js/item_effect_definitions.js?v=39.18.18" defer></script>
<script src="js/item_registry.js?v=39.18.18" defer></script>
<script src="js/data.js?v=39.18.18" defer></script>
<script src="js/ground_types.js?v=39.18.18" defer></script>
<script src="js/input_mode_manager.js?v=39.18.18" defer></script>
<script src="js/math.js?v=39.18.18" defer></script>
<script src="js/geometry_helpers.js?v=39.18.18" defer></script>
<script src="js/collision_footprint_system.js?v=39.18.18" defer></script>
<script src="js/physics_helpers.js?v=39.18.18" defer></script>
<script src="js/placement_preview_system.js?v=39.18.18" defer></script>
<script src="js/pin_attachment_system.js?v=39.18.18" defer></script>
<script src="js/physics_shape_editor_system.js?v=39.18.18" defer></script>
<script src="js/mechanical_system.js?v=39.18.18" defer></script>
<script src="js/mechanical_shape_bridge.js?v=39.18.18" defer></script>
<script src="js/constraint_system.js?v=39.18.18" defer></script>
<script src="js/physics_world_system.js?v=39.18.18" defer></script>
<script src="js/physics_projection_system.js?v=39.18.18" defer></script>
<script src="js/circuit_board_system.js?v=39.18.18" defer></script>
<script src="js/signal_system.js?v=39.18.18" defer></script>
<script src="js/assets.js?v=39.18.18" defer></script>
<script src="js/audio.js?v=39.18.18" defer></script>
<script src="js/perf_profiler.js?v=39.18.18" defer></script>
<script src="js/display_helpers.js?v=39.18.18" defer></script>
<script src="js/render.js?v=39.18.18" defer></script>
<script src="js/sim_core.js?v=39.18.18" defer></script>
<script src="js/tarinai_seed_factory.js?v=39.18.18" defer></script>
<script src="js/structures.js?v=39.18.18" defer></script>
<script src="js/item_type_catalog.js?v=39.18.18" defer></script>
<script src="js/items.js?v=39.18.18" defer></script>
<script src="js/item_type_initializers.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_support.js?v=39.18.18" defer></script>
<script src="js/item_update_policy.js?v=39.18.18" defer></script>
<script src="js/fire_runtime_system.js?v=39.18.18" defer></script>
<script src="js/robot_cleaner_system.js?v=39.18.18" defer></script>
<script src="js/item_dynamic_tool_system.js?v=39.18.18" defer></script>
<script src="js/item_dynamic_ball_system.js?v=39.18.18" defer></script>
<script src="js/item_dynamic_duplicator_system.js?v=39.18.18" defer></script>
<script src="js/item_dynamic_pin_system.js?v=39.18.18" defer></script>
<script src="js/item_dynamic_zunchi_system.js?v=39.18.18" defer></script>
<script src="js/item_environment_hazard_system.js?v=39.18.18" defer></script>
<script src="js/item_dynamic_system.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_decay_system.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_growth_system.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_step_frame.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_step_decay.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_step_dynamic.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_step_growth.js?v=39.18.18" defer></script>
<script src="js/update_step_pipeline_runner.js?v=39.18.18" defer></script>
<script src="js/item_lifecycle_pipeline.js?v=39.18.18" defer></script>
<script src="js/item_runtime.js?v=39.18.18" defer></script>
<script src="js/structure_lifecycle.js?v=39.18.18" defer></script>
<script src="js/memorial_bell_system.js?v=39.18.18" defer></script>
<script src="js/item_render_helpers.js?v=39.18.18" defer></script>
<script src="js/item_render_runtime.js?v=39.18.18" defer></script>
<script src="js/burn_motion_util.js?v=39.18.18" defer></script>
<script src="js/ants.js?v=39.18.18" defer></script>
<script src="js/health.js?v=39.18.18" defer></script>
<script src="js/tarinai.js?v=39.18.18" defer></script>
<script src="js/mamekusaredake_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_action_spec.js?v=39.18.18" defer></script>
<script src="js/tarinai_behavior_state.js?v=39.18.18" defer></script>
<script src="js/tarinai_identity_social.js?v=39.18.18" defer></script>
<script src="js/tarinai_action_state.js?v=39.18.18" defer></script>
<script src="js/tarinai_disease_nest.js?v=39.18.18" defer></script>
<script src="js/tarinai_item_effects.js?v=39.18.18" defer></script>
<script src="js/tarinai_needs_core.js?v=39.18.18" defer></script>
<script src="js/tarinai_behavior_text.js?v=39.18.18" defer></script>
<script src="js/tarinai_forced_behavior.js?v=39.18.18" defer></script>
<script src="js/tarinai_nest_sleep_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_item_targeting.js?v=39.18.18" defer></script>
<script src="js/tarinai_consumable_behavior.js?v=39.18.18" defer></script>
<script src="js/tarinai_social_action_runtime.js?v=39.18.18" defer></script>
<script src="js/tarinai_building_behavior.js?v=39.18.18" defer></script>
<script src="js/seesaw_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_action_definitions.js?v=39.18.18" defer></script>
<script src="js/tarinai_needs_items.js?v=39.18.18" defer></script>
<script src="js/tarinai_food_prototype_mixin.js?v=39.18.18" defer></script>
<script src="js/tarinai_direct_feeding_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_need_planner_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_item_interaction_context.js?v=39.18.18" defer></script>
<script src="js/tarinai_food_interaction_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_contact_item_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_item_interaction_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_sunbath_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_cursor_care_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_local_environment_system.js?v=39.18.18" defer></script>
<script src="js/tarinai_social_move_life.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_step_frame.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_step_ai.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_step_environment.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_step_movement.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_step_health.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_pipeline.js?v=39.18.18" defer></script>
<script src="js/tarinai_runtime.js?v=39.18.18" defer></script>
<script src="js/tarinai_render.js?v=39.18.18" defer></script>
<script src="js/world.js?v=39.18.18" defer></script>
<script src="js/world_view.js?v=39.18.18" defer></script>
<script src="js/family_graph.js?v=39.18.18" defer></script>
<script src="js/world_reset_presets.js?v=39.18.18" defer></script>
<script src="js/world_family_social.js?v=39.18.18" defer></script>
<script src="js/impact_core_system.js?v=39.18.18" defer></script>
<script src="js/impact_response_system.js?v=39.18.18" defer></script>
<script src="js/collision_response_system.js?v=39.18.18" defer></script>
<script src="js/world_combat_effects.js?v=39.18.18" defer></script>
<script src="js/world_environment.js?v=39.18.18" defer></script>
<script src="js/world_temperature_system.js?v=39.18.18" defer></script>
<script src="js/world_pathfinding_system.js?v=39.18.18" defer></script>
<script src="js/world_grass_placement_system.js?v=39.18.18" defer></script>
<script src="js/world_spatial_budget.js?v=39.18.18" defer></script>
<script src="js/world_ants_system.js?v=39.18.18" defer></script>
<script src="js/weather_system.js?v=39.18.18" defer></script>
<script src="js/item_update_scheduler.js?v=39.18.18" defer></script>
<script src="js/simulation_runtime_helpers.js?v=39.18.18" defer></script>
<script src="js/tarinai_update_policy.js?v=39.18.18" defer></script>
<script src="js/simulation_environment_system.js?v=39.18.18" defer></script>
<script src="js/simulation_item_ant_system.js?v=39.18.18" defer></script>
<script src="js/simulation_effects_system.js?v=39.18.18" defer></script>
<script src="js/simulation_creature_system.js?v=39.18.18" defer></script>
<script src="js/simulation_maintenance_system.js?v=39.18.18" defer></script>
<script src="js/simulation_ambient_system.js?v=39.18.18" defer></script>
<script src="js/simulation_systems.js?v=39.18.18" defer></script>
<script src="js/system_order.js?v=39.18.18" defer></script>
<script src="js/simulation.js?v=39.18.18" defer></script>
<script src="js/colony_situation_system.js?v=39.18.18" defer></script>
<script src="js/world_update.js?v=39.18.18" defer></script>
<script src="js/world_tool_actions.js?v=39.18.18" defer></script>
<script src="js/world_placement_log.js?v=39.18.18" defer></script>
<script src="js/world_event_effects.js?v=39.18.18" defer></script>
<script src="js/command_dispatcher.js?v=39.18.18" defer></script>
<script src="js/text_catalog.js?v=39.18.18" defer></script>
<script src="js/ui.js?v=39.18.18" defer></script>
<script src="js/game_dialogs.js?v=39.18.18" defer></script>
<script src="js/achievement_catalog.js?v=39.18.18" defer></script>
<script src="js/achievements.js?v=39.18.18" defer></script>
<script src="js/ui_helpers.js?v=39.18.18" defer></script>
<script src="js/ui_log.js?v=39.18.18" defer></script>
<script src="js/ui_selected.js?v=39.18.18" defer></script>
<script src="js/save_schema.js?v=39.18.18" defer></script>
<script src="js/snapshot_system.js?v=39.18.18" defer></script>
<script src="js/restore_coordinator.js?v=39.18.18" defer></script>
<script src="js/history_system.js?v=39.18.18" defer></script>
<script src="js/save_codec.js?v=39.18.18" defer></script>
<script src="js/save_storage.js?v=39.18.18" defer></script>
<script src="js/save_system.js?v=39.18.18" defer></script>
<script src="js/ui_tooltips.js?v=39.18.18" defer></script>
<script src="js/ui_layout_dialogs.js?v=39.18.18" defer></script>
<script src="js/ui_ground.js?v=39.18.18" defer></script>
<script src="js/ui_tools.js?v=39.18.18" defer></script>
<script src="js/ui_input_shared.js?v=39.18.18" defer></script>
<script src="js/ui_pointer_action_system.js?v=39.18.18" defer></script>
<script src="js/ui_input_touch.js?v=39.18.18" defer></script>
<script src="js/ui_input_mouse.js?v=39.18.18" defer></script>
<script src="js/ui_bind.js?v=39.18.18" defer></script>
<script src="js/ui_charts.js?v=39.18.18" defer></script>
<script src="js/ui_family_data.js?v=39.18.18" defer></script>
<script src="js/ui_family_async.js?v=39.18.18" defer></script>
<script src="js/ui_family_layout.js?v=39.18.18" defer></script>
<script src="js/ui_family_paths.js?v=39.18.18" defer></script>
<script src="js/ui_family_render.js?v=39.18.18" defer></script>
<script src="js/main.js?v=39.18.18" defer></script>
<script src="js/debug_tools.js?v=39.18.18" defer></script>
</body>
</html>

View file

@ -82,6 +82,13 @@ window.TARINAI_ACHIEVEMENT_CATALOG = {
"category": "ecology",
"showLockedDescription": true
},
{
"id": "ochibi_natural_slave",
"title": "たりないより「たりない」",
"description": "おちびが鎖無しでずんちどれいになる。",
"category": "ecology",
"showLockedDescription": true
},
{
"id": "natural_tarinai_king",
"title": "たりない王",
@ -329,7 +336,7 @@ window.TARINAI_ACHIEVEMENT_CATALOG = {
"title": "過保護",
"description": "同じたりないに手から10回餌を与え、3回以上薬で治療する。",
"category": "ecology",
"showLockedDescription": false
"showLockedDescription": true
},
{
"id": "self_sufficient",

View file

@ -34,7 +34,7 @@
const PLAYER_SECRET_KEY = "tarinai_achievement_player_secret_v1";
const LEGACY_PLAYER_KEY = "tarinai_achievement_legacy_player_v4";
const API_URL = String(global.TARINAI_ACHIEVEMENT_API || "achievement_api.php");
const GAME_VERSION = String(global.TARINAI_VERSION || "39.18.3");
const GAME_VERSION = String(global.TARINAI_VERSION || "39.18.18");
const DEBUG_MODE = ["1", "true"].includes(new URLSearchParams(global.location?.search || "").get("debug"));
const PLAYER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const LEGACY_PLAYER_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/;
@ -1021,7 +1021,7 @@
}
function sharedAchievementText(id) {
return `端末報告ベース ${percentageText(id)}(報告プレイヤー ${achieverCountText(id)}`;
return `達成率 ${percentageText(id)}(達成人数 ${achieverCountText(id)}`;
}
function continuousPlayElapsedMs(now = Date.now()) {
@ -2671,6 +2671,7 @@
const world = detail.world || tarinai?.world || null;
if (kind === "slave") {
unlock("natural_zunchi_slave", detail);
if (tarinai?.isAcquiredTarinai) unlock("ochibi_natural_slave", detail);
if (recordNaturalGeneration(world, tarinai, "_achievementNaturalSlaveLineageDepth", 5)) {
unlock("natural_zunchi_slave_5_generations", detail);
}

View file

@ -16,6 +16,8 @@ const DECOR_ASSETS = [
{ id: "oshibyo", path: "assets/objects/oshibyo.webp" },
{ id: "oshibyo_stuck", path: "assets/objects/oshibyo_stuck.webp" },
{ id: "plushie_bear", path: "assets/objects/plushie_bear.png" },
{ id: "acquired_tarinai_overlay", path: "assets/sprites/tarinai_acquired_overlay.png" },
{ id: "mamekusaredake", path: "assets/objects/mamekusaredake.png" },
];
const ALPHA_BOUNDS = {
@ -45,6 +47,7 @@ const ALPHA_BOUNDS = {
stress_dizzy: { x: 16, y: 21, w: 480, h: 433, imageW: 512, imageH: 468 },
stress_sweat: { x: 16, y: 26, w: 480, h: 428, imageW: 512, imageH: 468 },
intimidate: { x: 98, y: 16, w: 316, h: 438, imageW: 512, imageH: 468 },
intimidate_alt: { x: 45, y: 48, w: 422, h: 379, imageW: 512, imageH: 468 },
zunchi_slave: { x: 17, y: 22, w: 478, h: 431, imageW: 512, imageH: 468 },
hungry_70: { x: 0, y: 0, w: 430, h: 366, imageW: 430, imageH: 366 },
zunchi_slave_alt: { x: 42, y: 22, w: 425, h: 426, imageW: 512, imageH: 468 },
@ -78,7 +81,7 @@ const SUNBATH_IMAGE_IDS = new Set(SPRITES
.map(asset => asset?.id || "")
.filter(id => String(id).startsWith("sunbath")));
const INITIAL_IMAGE_IDS = new Set(["smile", "sleep", "hungry_70", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "zunchi_slave", "zunchi_slave_alt", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
const EARLY_IMAGE_IDS = new Set(["smile", "angry", "teary", "jito", "drool", "cry", "sleep", "pokan", "normal_smirk", "normal_tongue", "normal_happy", "hot_1", "hot_2", "hot_3", "cold_1", "cold_2", "cold_3", "weak", "hurt", "hurt2", "fear", "flee", "flee_fear2", "fear_blue", "fear_cry", "sleep2", "stress_dizzy", "stress_sweat", "intimidate", "intimidate_alt", "birth_ritual", "zunchi_slave", "zunchi_slave_alt", "hungry_70", "zunchi", "zunchi_02", "genkotsu", "pushpin", "pushpin_stuck", "oshibyo", "oshibyo_stuck", ...SUNBATH_IMAGE_IDS]);
function trimCanvasCache(cache, limit = 128) {
while (cache.size > limit) {

View file

@ -26,6 +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: "intimidate_alt", label: "威嚇たりない 2", path: "assets/sprites/tarinai_40_intimidate_alt.webp", actionOnly: true },
{ id: "birth_ritual", label: "\u3078\u3053\u3078\u3053\u305f\u308a\u306a\u3044", 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 },

View file

@ -22,6 +22,12 @@
id: "disease_fight",
flag: "fightDisease",
mysteryEligible: true,
},
disease_mamekusaredake: {
id: "disease_mamekusaredake",
flag: "mamekusaredakeDisease",
baseInfectionChance: 0.025,
mysteryEligible: false,
}
};

View file

@ -180,10 +180,16 @@ const HEALTH = (() => {
const MAJOR_DAMAGE_REACTIONS = Object.freeze([
{ id: "voice_major_damage_01", text: "\u3042\u3042\u3042\u3042\uff01" },
{ id: "voice_major_damage_02", text: "\u3048\u3048\u3048\u3048\uff01" },
{ id: "voice_major_damage_03", text: "\u306f\u3046\u3046\u3046\uff01" },
{ id: "voice_major_damage_04", text: "\u306f\u3046\u306f\u3046\uff01" },
{ id: "voice_major_damage_01", text: "ああああ!" },
{ id: "voice_major_damage_02", text: "ええええ!" },
{ id: "voice_major_damage_03", text: "はううう!" },
{ id: "voice_major_damage_04", text: "はうはう!" },
]);
const OCHIBI_MAJOR_DAMAGE_REACTIONS = Object.freeze([
{ id: "voice_ochibi_damage_001", text: "いちゃい!" },
{ id: "voice_ochibi_damage_002", text: "ごふっ" },
{ id: "voice_ochibi_damage_003", text: "ああああああああ" },
]);
function spawnSilentDamageBubble(t, text = "!", color = "rgba(142,58,58,0.86)") {
@ -242,7 +248,7 @@ const HEALTH = (() => {
t.lastMajorDamageAt = now;
if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false;
t.nextMajorDamageVoiceAt = now + 1.0;
const list = MAJOR_DAMAGE_REACTIONS;
const list = t.isAcquiredTarinai ? OCHIBI_MAJOR_DAMAGE_REACTIONS : MAJOR_DAMAGE_REACTIONS;
const reaction = list[Math.floor(Math.random() * list.length)] || list[0];
audio.play?.(reaction.id, { category: "voice", minGap: 0.85 });
spawnSilentDamageBubble(t, reaction.text, "rgba(146,58,58,0.88)");

View file

@ -12,6 +12,8 @@
case "leakage_device":
case "poison_gas_can":
case "poison_gas":
case "mamekusaredake":
case "mame_spore":
return global.TarinaiItemEnvironmentHazardSystem.update(item, dt, worldRef) !== false;
case "firecracker":
case "flame_firecracker":

View file

@ -259,6 +259,7 @@
t.hurtTimer=Math.max(t.hurtTimer||0,.22);
t.electricShockUntil=Math.max(Number(t.electricShockUntil||0),now+.48);
t.setActionState?.("panic",{target:null,reason:"\u611f\u96fb\u3057\u3066\u3044\u308b\u3002",wake:true,sleeping:false});
if(t.isAcquiredTarinai&&now>=Number(t.ochibiElectricVoiceAt||0)){t.ochibiElectricVoiceAt=now+1.8;if(t.bubble?.("びりびりちゃ!",1.4,"rgba(70,104,168,0.82)",{force:true})){const a=globalThis.TarinaiAudio;if(a?.playSample)a.playSample("voice_ochibi_electric_006",.9,"voice")||a.play?.("voice_ochibi_electric_006",{category:"voice",minGap:.9});}}
world.queueEffect?.("electric_shock",t.x,t.y-Math.max(2,(t.radius||18)*.15),{size:Math.max(18,(t.radius||18)*1.35),life:.32,color:"rgba(122,224,255,0.98)",importance:3,forceVisual:true});
hit++;
}
@ -365,6 +366,7 @@
if(item.type==="leakage_device")return updateLeakage(item,dt,world);
if(item.type==="poison_gas_can")return updatePoisonGasCan(item,dt,world);
if(item.type==="poison_gas")return updateGas(item,dt,world);
if(item.type==="mamekusaredake"||item.type==="mame_spore") return global.TarinaiMamekusaredakeSystem?.updateItem?.(item,dt,world) || false;
return false;
}

View file

@ -1294,6 +1294,8 @@ draw(ctx, t, lighting = null) {
ctx.fill();
}
ctx.restore();
} else if (this.type === "mamekusaredake" || this.type === "mame_spore") {
globalThis.TarinaiMamekusaredakeSystem?.drawItem?.(ctx, this, t);
} else if (this.type === "fan") {
const r = this.r || 26;
const bodyAngle = Number.isFinite(Number(this.fanBodyAngle)) ? normalizedItemAngle(Number(this.fanBodyAngle), itemAngleFor(this)) : itemAngleFor(this);

View file

@ -13,6 +13,7 @@ const TOOL_DEFINITIONS = Object.freeze({
poke: { id: "poke", label: "\u3064\u3064\u304f", placeable: false, scalable: false, iconText: "\u{1F448}", tooltip: "\u305f\u308a\u306a\u3044\u3092\u3064\u3064\u304f\u3002\u30dc\u30fc\u30eb\u3082\u3064\u3064\u3044\u3066\u8ee2\u304c\u305b\u308b\u3002" },
pinch: { id: "pinch", label: "\u3064\u307e\u3080", placeable: false, scalable: false, iconText: "\u{1F90F}", tooltip: "\u305f\u308a\u306a\u3044\u3084\u4e00\u90e8\u306e\u7f6e\u304d\u3082\u306e\u3092\u79fb\u52d5\u3055\u305b\u308b\u3002" },
new: { id: "new", label: "\u8ffd\u52a0", placeable: false, scalable: false, icon: "assets/ui/tool_new.webp", tooltip: "\u753b\u9762\u5916\u304b\u3089\u65b0\u3057\u3044\u305f\u308a\u306a\u3044\u3092\u547c\u3076\u3002" },
acquired_tarinai: { id: "acquired_tarinai", label: "おちび", placeable: false, scalable: false, icon: "assets/ui/tool_acquired_tarinai.webp", tooltip: "近所の鬼威惨から貰ったおちび。後天的なたりないらしい。" },
water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3084\u706b\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" },
rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" },
@ -24,6 +25,8 @@ const TOOL_DEFINITIONS = Object.freeze({
bear_trap: { id: "bear_trap", itemType: "bear_trap", label: "\u30c8\u30e9\u30d0\u30b5\u30df", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u7f60", tooltip: "\u52d5\u304f\u3082\u306e\u3092\u6355\u3089\u3048\u3066\u3057\u3070\u3089\u304f\u96e2\u3055\u306a\u3044\u3002" },
leakage_device: { id: "leakage_device", itemType: "leakage_device", label: "\u6f0f\u96fb\u5668", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u6f0f", tooltip: "\u901a\u96fb\u4e2d\u3001\u5e83\u7bc4\u56f2\u306b\u96fb\u6483\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
poison_gas_can: { id: "poison_gas_can", itemType: "poison_gas_can", label: "\u6bd2\u30ac\u30b9\u7f36", placeable: true, scalable: true, radius: 28, amount: 999, iconText: "\u6bd2", tooltip: "\u6bd2\u30ac\u30b9\u3092\u653e\u51fa\u3059\u308b\u3002" },
mamekusaredake: { id: "mamekusaredake", itemType: "mamekusaredake", label: "マメクサレダケ", placeable: true, scalable: false, radius: 34, amount: 999, icon: "assets/ui/tool_mamekusaredake.webp", tooltip: "胞子で感染する寄生キノコ。つまむと除去できる。" },
mame_spore: { id: "mame_spore", itemType: "mame_spore", label: "マメクサレダケ胞子", placeable: false, scalable: false, radius: 28, amount: 100, iconText: "胞", tooltip: "マメクサレダケの胞子。" },
robot_cleaner: { id: "robot_cleaner", itemType: "robot_cleaner", label: "\u30ed\u30dc\u6383\u9664\u6a5f", placeable: true, scalable: false, radius: 22, amount: 999, iconText: "\u25c9", tooltip: "\u30b4\u30df\u3084\u6c5a\u308c\u3092\u7247\u4ed8\u3051\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" },
zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" },
sweet: { id: "sweet", itemType: "sweet", label: "\u305a\u3093\u3060\u9905", placeable: true, scalable: true, radius: 13, amount: 62, icon: "assets/ui/tool_sweet.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u5927\u597d\u7269\u3002\u4e00\u90e8\u306e\u75c5\u6c17\u3092\u6cbb\u305b\u308b\u3002" },

View file

@ -5,12 +5,12 @@
const ITEM_TRAITS = Object.freeze({
obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "circuit_board"]),
food_interest: new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]),
hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug", "mamekusaredake"]),
pin: new Set(["pushpin", "oshibyo"]),
kinematic: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi"]),
freeStackingMovable: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi", "water", "zunda_juice", "mercury", "genkotsu", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "ant_corpse"]),
sleepFurniture: new Set(["bed", "nest_box", "pipe", "grass_bed"]),
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "oil", "bear_trap", "leakage_device", "poison_gas_can", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]),
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "oil", "bear_trap", "leakage_device", "poison_gas_can", "mamekusaredake", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch", "seesaw", "circuit_board"]),
});
@ -30,10 +30,10 @@
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "clear_tool", "water_hose", "bell"] },
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] },
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] },
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["acquired_tarinai", "zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "seesaw", "signboard"] },
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_CATEGORY_TOOL_IDS },
{ id: "electric", label: "\u96fb\u6c17", themeClass: "tool-category-electric", order: 46, toolIds: ELECTRIC_TOOL_IDS },
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "oil", "bear_trap", "poison_gas_can", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] },
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "oil", "bear_trap", "poison_gas_can", "mamekusaredake", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] },
]);
const TOOL_SIZE_ORDER = Object.freeze(["small", "medium", "large"]);

View file

@ -41,7 +41,7 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "water" || type === "zunda_juice") item.roles.drink = true;
if (["sweet", "water", "zunda_juice", "slave_chain", "golden_crown"].includes(type) || isParamEffectItemType(type)) item.roles.medicine = true;
if (type === "bed" || type === "nest_box" || type === "pipe") item.roles.sleepPlace = true;
if (["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "splat", "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas"].includes(type)) item.roles.danger = true;
if (["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "splat", "oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore"].includes(type)) item.roles.danger = true;
if (type === "grass") item.roles.grassMaterial = true;
if (type === "grass") {
item.grassStage = Math.floor(rand(0, 2.999));
@ -310,6 +310,8 @@ function initializeItemTypeState(item, type, x, y) {
if (type === "leakage_device") { item.r = 34; item.amount = 999; item.nextLeakagePulseAt = 0; item.deletable = true; }
if (type === "poison_gas_can") { item.r = 28; item.amount = 999; item.fuseMax = 5; item.fuseTimer = 5; item._gasReleased = false; item.deletable = true; }
if (type === "poison_gas") { item.r = 96; item.amount = 100; item.gasConcentration = 1; item.gasAge = 0; item.gasLife = 22; item.gasExpansion = 72; item.vx = 0; item.vy = 0; item.deletable = false; }
if (type === "mamekusaredake") { item.r = 34; item.amount = 999; item.mamePlanted = true; item.nextSporeAt = 0; item.deletable = true; item.roles.danger = true; }
if (type === "mame_spore") { item.r = 28; item.amount = 100; item.sporeAge = 0; item.sporeLife = 9; item.sporeConcentration = 1; item.vx = 0; item.vy = 0; item.deletable = false; item.roles.danger = true; }
if (globalThis.TarinaiPhysicsBodySystem.isPhysicsType(type)) {
globalThis.TarinaiPhysicsBodySystem.invalidateItem(item, "item-initialized");

View file

@ -5,10 +5,10 @@
// helpers and the item scheduler both read this single policy instead of
// duplicating item-type timing rules.
(function (global) {
const REALTIME_ITEM_TYPES = new Set(["oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "robot_cleaner"]);
const REALTIME_ITEM_TYPES = new Set(["oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "robot_cleaner"]);
const PIN_ITEM_TYPES = new Set(["pushpin", "oshibyo"]);
const SCHEDULED_ITEM_TYPES = Object.freeze([
"oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo",
"oil", "bear_trap", "leakage_device", "poison_gas_can", "poison_gas", "mamekusaredake", "mame_spore", "ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo",
"plushie", "grass_bed", "zunchi", "robot_cleaner", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
"stone", "bed", "nest_box", "pipe", "toilet", "rain_shelter", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch",
@ -52,7 +52,7 @@
function idleRealtimeInterval(item) {
if (!item) return Infinity;
if (item.type === "oil" || item.type === "bear_trap" || item.type === "leakage_device" || item.type === "poison_gas_can" || item.type === "poison_gas") return 0.18;
if (item.type === "oil" || item.type === "bear_trap" || item.type === "leakage_device" || item.type === "poison_gas_can" || item.type === "poison_gas" || item.type === "mamekusaredake" || item.type === "mame_spore") return 0.18;
if (item.type === "ball" || item.type === "balloon") return 0.24;
if (item.type === "fan") return 0.60;
if (item.type === "genkotsu") return 0.45;

187
js/mamekusaredake_system.js Normal file
View file

@ -0,0 +1,187 @@
"use strict";
(function(global){
function mushroomImage(){ return global.getRenderableImage?.("mamekusaredake", "") || global.images?.get?.("mamekusaredake") || null; }
function growthOf(t){ return Math.max(0,Math.min(1,Number(t?.mamekusaredakeGrowth||0)||0)); }
const hostAlphaCache = typeof WeakMap !== "undefined" ? new WeakMap() : null;
function unit(seed="",salt=""){
if(typeof global.stableUnit==="function") return global.stableUnit(String(seed||"mame"),String(salt||""));
const s=`${seed}|${salt}`; let h=2166136261;
for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}
return ((h>>>0)%1000003)/1000003;
}
function alphaPointsForImage(img){
if(!img)return null;
if(hostAlphaCache?.has(img)) return hostAlphaCache.get(img);
let out=null;
try{
const c=document.createElement("canvas"); c.width=64; c.height=64;
const cx=c.getContext("2d",{willReadFrequently:true}); cx.clearRect(0,0,64,64); cx.drawImage(img,0,0,64,64);
const data=cx.getImageData(0,0,64,64).data, pts=[];
for(let y=3;y<61;y++) for(let x=3;x<61;x++) if(data[(y*64+x)*4+3]>=96) pts.push([x,y]);
out=pts.length?pts:null;
}catch(_){out=null;}
hostAlphaCache?.set(img,out); return out;
}
function hostPoint(t,img,dw,dh,index=0){
const seed=t?.birthSeed||t?.familyKey||t?.id||"mame-host";
const pts=alphaPointsForImage(img);
if(pts?.length){
const k=Math.min(pts.length-1,Math.floor(unit(seed,`mame-pos-${index}`)*pts.length));
const p=pts[k]||pts[0];
return {x:(p[0]/63-.5)*dw,y:(p[1]/63-.5)*dh};
}
// Fallback stays inside the usual non-transparent body ellipse.
const a=unit(seed,`mame-a-${index}`)*Math.PI*2, rr=Math.sqrt(unit(seed,`mame-r-${index}`))*.70;
return {x:Math.cos(a)*dw*.34*rr,y:Math.sin(a)*dh*.28*rr+dh*.03};
}
function hostMushroomCount(g){ return g<=.001?0:Math.max(1,Math.min(5,Math.ceil(g*5))); }
function infect(t, source=null){
if(!t||t.dead||t.mamekusaredakeDisease) return false;
t.mamekusaredakeDisease=true; t.mamekusaredakeGrowth=0; t.mamekusaredakeDeathPlanted=false;
t.mamekusaredakeSpeechAt=(t.world?.time||0)+3+Math.random()*5;
t.world?.log?.(`${t.name}がマメクサレダケ病に罹患した。`,"accident",{participants:[t]});
return true;
}
function visualSizeForHost(t,g=growthOf(t)){
const rendered=Number(t?._mameRenderedSize||0)||0;
if(rendered>0) return Math.max(3,rendered);
const radius=Math.max(16,Number(t?.radius||20)||20);
return Math.max(3,radius*1.16*Math.max(.05,g));
}
function spawnDetached(world,t,growth=growthOf(t)){
if(!world||!t||typeof Item==="undefined") return null;
const side=t.facingDir?.()||1;
const it=new Item("mamekusaredake", t.x+side*(t.radius||20)*0.46, t.y+(t.radius||20)*0.30);
it.mameDetached=true; it.mamePlanted=false; it.mameLife=1.25; it.vx=side*(35+Math.random()*25); it.vy=-35-Math.random()*20;
it.mameDetachedSize=visualSizeForHost(t,growth); it.r=Math.max(2,it.mameDetachedSize/2.15);
world.addItem?.(it,"mame-detached",{terrain:false}); return it;
}
function recover(t, reason="キノコが取れた"){
if(!t?.mamekusaredakeDisease) return false;
const growth=growthOf(t);
spawnDetached(t.world,t,growth);
t.mamekusaredakeDisease=false; t.mamekusaredakeGrowth=0; t.mamekusaredakeSpeechAt=0; t.mamekusaredakeDeathPlanted=false;
t.world?.spawnEffect?.("ring",t.x,t.y,{size:(t.radius||20)*(.55+.75*growth),life:.55,color:"rgba(190,153,80,.66)"});
t.world?.log?.(`${t.name}のマメクサレダケが外れて治った。`,"event",{participants:[t]}); return true;
}
function plantAt(world,x,y,r=34){
if(!world||typeof Item==="undefined")return null;
const it=new Item("mamekusaredake",x,y); it.r=r; it.mamePlanted=true; it.mameDetached=false; it.nextSporeAt=(world.time||0)+1.2+Math.random();
world.addItem?.(it,"mame-plant",{terrain:false}); return it;
}
function plantFromHost(t){
if(!t?.world||t.mamekusaredakeDeathPlanted) return null;
t.mamekusaredakeDeathPlanted=true;
const g=growthOf(t);
// 死亡サイズの50%以下なら地面には残らず消滅する。
if(g<=.5) return null;
// 表示上の本数とは無関係に、死亡時に植わるのは必ず1本だけ。
return plantAt(t.world,t.x,t.y,Math.max(22,(t.radius||20)*(.62+.32*g)));
}
function spawnSpore(world,m){
if(!world||typeof Item==="undefined")return null;
const a=Math.random()*Math.PI*2,d=Math.random()*(m.r||34)*.35;
const s=new Item("mame_spore",m.x+Math.cos(a)*d,m.y+Math.sin(a)*d*.55); s.r=22+Math.random()*18; s.sporeAge=0; s.sporeLife=7+Math.random()*5; s.sporeConcentration=.45+Math.random()*.25; s.vx=Math.cos(a)*(6+Math.random()*10); s.vy=-4+Math.sin(a)*5;
world.addItem?.(s,"mame-spore",{terrain:false}); return s;
}
function updateTarinai(t,dt){
if(!t||t.dead)return false;
const now=t.world?.time||0;
const sick=Boolean(t.zunchiDisease||t.sleepDisease||t.explosionDisease||t.fightDisease||t.mamekusaredakeDisease);
if(t.mamekusaredakeDisease){
if(t.state==="sunbath"||(t.sunbathTimer||0)>.04){
t.sunbathTimer=0; t.sunbathLeisure=false; t.sunbathCooldown=Math.max(t.sunbathCooldown||0,20);
t.visibleSpriteId=""; t.setActionState?.("idle",{target:null,reason:"マメクサレダケ病のため日光浴できない",sleeping:false,clearTarget:true});
}
t.mamekusaredakeGrowth=Math.min(1.08,(Number(t.mamekusaredakeGrowth||0)||0)+dt/90);
const g=growthOf(t);
if(g>=.5){
const maxEnergy=Math.max(1,typeof global.tarinaiMaxEnergy==="function"?global.tarinaiMaxEnergy(t):(Number(t.maxEnergy)||100));
const severity=Math.max(0,Math.min(1,(g-.5)/.5));
const loss=dt*maxEnergy*(.0018+.0037*severity);
t.energy=Math.max(0,(Number(t.energy)||0)-loss);
if(loss>.01) t.showHpBar?.();
if(t.energy<=.5){ t.die?.("マメクサレダケ病",{fromDamage:true}); return true; }
}
}
if(t.isAcquiredTarinai && sick && now>=Number(t.mamekusaredakeSpeechAt||0) && (t.nextBubbleAt||0)<=now){
const choices=[{text:"くるち、くるち",id:"voice_ochibi_disease_001"},{text:"おみじゅ...",id:"voice_ochibi_hot_007"},{text:"ぶるぶる",id:"voice_temp_cold"}];
const c=choices[Math.floor(Math.random()*choices.length)]; if(c&&t.bubble?.(c.text,4.5,"rgba(96,72,48,.82)")){ const a=global.TarinaiAudio; a?.playSample?.(c.id,1.1,"voice")||a?.play?.(c.id,{category:"voice",minGap:1.1}); }
t.mamekusaredakeSpeechAt=now+7+Math.random()*10;
}
if(t.mamekusaredakeDisease && t.mamekusaredakeGrowth>=1){ t.die?.("マメクサレダケ病"); }
return true;
}
function updateItem(item,dt,world){
if(item.type==="mamekusaredake"){
if(item.mameDetached){ item.mameLife=(item.mameLife||1)-dt; item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vy=(item.vy||0)+55*dt; item.vx*=Math.pow(.6,dt); if(item.mameLife<=0)item.amount=0; return true; }
item.mamePlanted=true; const now=world?.time||0; if(now>=Number(item.nextSporeAt||0)){ item.nextSporeAt=now+1.4+Math.random()*1.8; if(Math.random()<.75)spawnSpore(world,item); } return true;
}
if(item.type==="mame_spore"){
item.sporeAge=(item.sporeAge||0)+dt; const life=Math.max(1,item.sporeLife||9); item.sporeConcentration=Math.max(0,1-item.sporeAge/life); item.x+=(item.vx||0)*dt; item.y+=(item.vy||0)*dt; item.vx*=Math.pow(.72,dt); item.vy*=Math.pow(.72,dt);
for(const t of world?.tarinai||[]){ if(!t||t.dead||t.mamekusaredakeDisease)continue; if(distXY(item.x,item.y,t.x,t.y)<=item.r+(t.radius||18)*.45 && Math.random()<dt*.036*item.sporeConcentration) infect(t,item); }
if(item.sporeConcentration<=.01)item.amount=0; return true;
} return false;
}
function drawMushroom(ctx,x,y,size,alpha=1,rot=0){ const img=mushroomImage(); if(!img)return false; ctx.save();ctx.globalAlpha*=alpha;ctx.translate(x,y);ctx.rotate(rot);ctx.drawImage(img,-size*.44,-size*.55,size*.88,size);ctx.restore();return true; }
function drawOnTarinai(ctx,t,dw,dh,faceRight=false,hostImage=null,renderInfo=null){
if(!t?.mamekusaredakeDisease)return false;
const g=growthOf(t); if(g<=.001)return false;
const count=hostMushroomCount(g), seed=t.birthSeed||t.familyKey||t.id||"mame-host";
const baseSize=Math.max(4,dh*.34*g);
t._mameRenderedSize=baseSize;
t._mameHostLocalTargets=[];
t._mameHostWorldTargets=[];
const originX=Number(renderInfo?.originX);
const originY=Number(renderInfo?.originY);
const renderAngle=Number(renderInfo?.angle||0)||0;
const hasWorldTransform=Number.isFinite(originX)&&Number.isFinite(originY);
const cosA=Math.cos(renderAngle), sinA=Math.sin(renderAngle);
let drew=false;
for(let i=0;i<count;i++){
const p=hostPoint(t,hostImage,dw,dh,i);
const size=baseSize*(.72+unit(seed,`mame-size-${i}`)*.34);
const rot=(-40+unit(seed,`mame-rot-${i}`)*80)*Math.PI/180;
t._mameHostLocalTargets.push({x:p.x,y:p.y,size});
if(hasWorldTransform){
// draw() mirrors local X when faceRight, then rotates the whole sprite.
// Use the identical transform for hit testing so the first click lands
// on the mushroom that is actually visible on screen.
const lx=faceRight?-p.x:p.x, ly=p.y;
t._mameHostWorldTargets.push({
x:originX+cosA*lx-sinA*ly,
y:originY+sinA*lx+cosA*ly,
size
});
}
drew=drawMushroom(ctx,p.x,p.y,size,Math.min(1,.32+g*.82),rot)||drew;
}
return drew;
}
function drawItem(ctx,item,time=0){
if(item.type==="mamekusaredake"){
const alpha=item.mameDetached?Math.max(0,Math.min(1,(item.mameLife||0)/.45)):1;
const size=item.mameDetached&&Number(item.mameDetachedSize)>0?Number(item.mameDetachedSize):(item.r||34)*2.15;
if(!item.mameDetached){
ctx.save(); ctx.globalAlpha*=.34; ctx.fillStyle="rgba(38,28,18,.72)"; ctx.beginPath(); ctx.ellipse(size*.055,size*.41,size*.30,size*.085,0,0,Math.PI*2); ctx.fill(); ctx.restore();
}
return drawMushroom(ctx,0,0,size,alpha,item.mameDetached?(time*2.2):0);
}
if(item.type==="mame_spore"){ const r=item.r||28,c=Math.max(0,Math.min(1,item.sporeConcentration??1));ctx.save();ctx.globalAlpha*=.10+.22*c;ctx.fillStyle="rgba(181,139,65,.58)";for(let i=0;i<3;i++){const a=(item.seed||0)+i*2.2,rr=r*(.30+i*.06);ctx.beginPath();ctx.arc(Math.cos(a)*r*.18,Math.sin(a)*r*.12,rr,0,Math.PI*2);ctx.fill();}ctx.restore();return true;}
return false;
}
function mushroomHit(t,x,y){
if(!t?.mamekusaredakeDisease)return false;
const worldTargets=Array.isArray(t._mameHostWorldTargets)?t._mameHostWorldTargets:[];
for(const p of worldTargets){
if(distXY(x,y,p.x,p.y)<=Math.max(8,(p.size||10)*.52)) return true;
}
// Fallback for the short interval before the first render after infection.
// It intentionally stays conservative so pinching the body itself does not
// accidentally cure the disease.
const g=Math.max(.05,growthOf(t));
const face=typeof t.facingDir==="function"?(t.facingDir()||1):(t.facing||1);
return distXY(x,y,t.x+face*(t.radius||20)*.30,t.y+(t.radius||20)*.18)<=Math.max(7,(t.radius||20)*.34*g);
}
global.TarinaiMamekusaredakeSystem=Object.freeze({infect,recover,plantAt,plantFromHost,spawnSpore,updateTarinai,updateItem,drawOnTarinai,drawItem,mushroomHit});
})(typeof window!=="undefined"?window:globalThis);

View file

@ -449,6 +449,13 @@
target.fearTimer = Math.max(num(target.fearTimer), 0.8);
target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.62);
target.setActionState?.("panic", { target: null, reason: "\u611f\u96fb\u3057\u3066\u3044\u308b", wake: true, sleeping: false });
if (target.isAcquiredTarinai && now >= Number(target.ochibiElectricVoiceAt || 0)) {
target.ochibiElectricVoiceAt = now + 1.8;
if (target.bubble?.("びりびりちゃ!", 1.4, "rgba(70,104,168,0.82)", { force: true })) {
const a = global.TarinaiAudio;
if (a?.playSample) a.playSample("voice_ochibi_electric_006", 0.9, "voice") || a.play?.("voice_ochibi_electric_006", { category: "voice", minGap: 0.9 });
}
}
}
emitEffect(worldRef, "electric_shock", target.x, target.y - Math.max(3, num(target.radius ?? target.r, 12) * 0.12), {
size: Math.max(12, num(target.radius ?? target.r, 12) * 1.15),

View file

@ -660,32 +660,10 @@ class Effect {
}
} else if (this.type === "bubble") {
ctx.font = "bold 12px Yomogi, ui-rounded, sans-serif";
const maxTextW = 118;
const lineH = 14;
const ellipsize = (text, maxW) => globalThis.canvasEllipsizeText(ctx, text, maxW);
const wrap = (text, maxW) => {
const src = String(text || "").replace(/\s+/g, " ").trim() || " ";
const units = src.includes(" ") ? src.split(/(\s+)/).filter(Boolean) : Array.from(src);
const out = [];
let line = "";
for (const unit of units) {
const candidate = line ? `${line}${unit}` : unit;
if (ctx.measureText(candidate).width <= maxW) line = candidate;
else {
if (line) out.push(line.trim());
line = unit.trim();
if (ctx.measureText(line).width > maxW) { out.push(ellipsize(line, maxW)); line = ""; }
}
if (out.length >= 3) break;
}
if (line && out.length < 3) out.push(line.trim());
if (out.length > 2) out[1] = ellipsize([out[1], ...out.slice(2)].join(""), maxW);
return out.slice(0, 2).map(line => ellipsize(line, maxW));
};
const lines = wrap(this.text, maxTextW);
const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0);
const w = Math.max(30, Math.min(142, textW + 18));
const h = Math.max(20, 10 + lines.length * lineH);
const line = String(this.text || "").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim() || " ";
const textW = Math.ceil(ctx.measureText(line).width);
const w = Math.max(30, textW + 20);
const h = 28;
ctx.globalAlpha = Math.min(1, alpha * 1.25);
ctx.fillStyle = "rgba(255, 252, 240, 0.92)";
ctx.strokeStyle = "rgba(78, 64, 48, 0.22)";
@ -704,9 +682,7 @@ class Effect {
ctx.fillStyle = this.color || "rgba(42,36,29,0.78)";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], 0, -h + 8 + lineH * i + lineH / 2);
}
ctx.fillText(line, 0, -h / 2);
}
ctx.restore();
}

View file

@ -301,6 +301,8 @@
if (t.birthRitualLeader) flags |= 1 << 7;
if (t.hasPaired) flags |= 1 << 8;
if (t.isTarinaiChampion) flags |= 1 << 9;
if (t.isAcquiredTarinai) flags |= 1 << 10;
if (t.mamekusaredakeDisease) flags |= 1 << 11;
return flags;
}
function flagApply(t, flags = 0) {
@ -314,6 +316,8 @@
t.birthRitualLeader = !!(flags & (1 << 7));
t.hasPaired = !!(flags & (1 << 8));
t.isTarinaiChampion = !!(flags & (1 << 9));
t.isAcquiredTarinai = !!(flags & (1 << 10));
t.mamekusaredakeDisease = !!(flags & (1 << 11));
if (t.isZunchiSlave) t.zunchiSlaveLocked = true;
}
function compactWorld(worldRef) {
@ -507,8 +511,7 @@
const parentIdx = (t.parents || []).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), q(t.itemEffectTimers?.sedative, 10)
];
q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100), q(t.itemEffectTimers?.sedative, 10), q(t.mamekusaredakeGrowth, 100)];
const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")];
const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1;
const nestIdx = itemIndex.get(t.insideNestBoxId) ?? -1;
@ -548,6 +551,8 @@
needs, state: "idle",
isZunchiSlave: Boolean((Number(row[0]) || 0) & (1 << 5)),
isTarinaiChampion: Boolean((Number(row[0]) || 0) & (1 << 9)),
isAcquiredTarinai: Boolean((Number(row[0]) || 0) & (1 << 10)),
mamekusaredakeDisease: Boolean((Number(row[0]) || 0) & (1 << 11)),
totalFightWins: Array.isArray(row[19]) ? u(row[19][0], 1) : 0,
totalFightLosses: Array.isArray(row[19]) ? u(row[19][1], 1) : 0,
tarinaiChampionSince: Array.isArray(row[19]) ? u(row[19][2], 10) : 0,
@ -567,6 +572,7 @@
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.mamekusaredakeGrowth = u(timers[10], 100, 0);
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10), sedative: u(timers[9], 10) };
t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1);
const modes = Array.isArray(row[14]) ? row[14] : [];

View file

@ -30,6 +30,40 @@
voice_major_damage_04: 1.0,
voice_temp_cold: 1.0,
voice_temp_hot: 1.0,
voice_acquired_stress_001: 1.0,
voice_acquired_stress_002: 1.0,
voice_acquired_stress_003: 1.0,
voice_acquired_stress_004: 1.0,
voice_acquired_stress_005: 1.0,
voice_acquired_stress_006: 1.0,
voice_acquired_stress_009: 1.0,
voice_ochibi_calm_001: 1.0,
voice_ochibi_calm_002: 1.0,
voice_ochibi_calm_003: 1.0,
voice_ochibi_calm_004: 1.0,
voice_ochibi_calm_005: 1.0,
voice_ochibi_calm_006: 1.0,
voice_ochibi_calm_007: 1.0,
voice_ochibi_calm_008: 1.0,
voice_ochibi_calm_009: 1.0,
voice_ochibi_dirty_004: 1.0,
voice_ochibi_damage_001: 1.0,
voice_ochibi_damage_002: 1.0,
voice_ochibi_damage_003: 1.0,
voice_ochibi_disease_001: 1.0,
voice_intimidate_alt: 1.0,
voice_ochibi_sleep_002: 1.0,
voice_ochibi_sleep_003: 1.0,
voice_ochibi_fight_hit_004: 1.0,
voice_ochibi_panic_005: 1.0,
voice_ochibi_electric_006: 1.0,
voice_ochibi_hot_007: 1.0,
voice_ochibi_stress_008: 1.0,
voice_ochibi_dirty_003: 1.0,
voice_ochibi_dirty_002: 1.0,
voice_ochibi_dirty_001: 1.0,
voice_acquired_stress_008: 1.0,
voice_acquired_stress_007: 1.0,
},
idMinGap: {
sfx_ui_click: 0.030,
@ -48,6 +82,40 @@
voice_major_damage_04: 0.45,
voice_temp_cold: 1.20,
voice_temp_hot: 1.20,
voice_acquired_stress_001: 1.50,
voice_acquired_stress_002: 1.50,
voice_acquired_stress_003: 1.50,
voice_acquired_stress_004: 1.50,
voice_acquired_stress_005: 1.50,
voice_acquired_stress_006: 1.50,
voice_acquired_stress_009: 1.50,
voice_ochibi_calm_001: 1.50,
voice_ochibi_calm_002: 1.50,
voice_ochibi_calm_003: 1.50,
voice_ochibi_calm_004: 1.50,
voice_ochibi_calm_005: 1.50,
voice_ochibi_calm_006: 1.50,
voice_ochibi_calm_007: 1.50,
voice_ochibi_calm_008: 1.50,
voice_ochibi_calm_009: 1.50,
voice_ochibi_dirty_004: 1.50,
voice_ochibi_damage_001: 1.25,
voice_ochibi_damage_002: 1.25,
voice_ochibi_damage_003: 1.25,
voice_ochibi_disease_001: 1.25,
voice_intimidate_alt: 1.35,
voice_ochibi_sleep_002: 1.35,
voice_ochibi_sleep_003: 1.35,
voice_ochibi_fight_hit_004: 1.35,
voice_ochibi_panic_005: 1.35,
voice_ochibi_electric_006: 1.35,
voice_ochibi_hot_007: 1.35,
voice_ochibi_stress_008: 1.35,
voice_ochibi_dirty_003: 1.50,
voice_ochibi_dirty_002: 1.50,
voice_ochibi_dirty_001: 1.50,
voice_acquired_stress_008: 1.50,
voice_acquired_stress_007: 1.50,
},
samplePaths: {
voice_hau: "assets/sounds/voice_001_hau.wav",
@ -67,7 +135,41 @@
voice_major_damage_04: "assets/sounds/major_damage_04.wav",
voice_temp_cold: "assets/sounds/voice_008_temperature_buruburu.wav",
voice_temp_hot: "assets/sounds/voice_009_temperature_achui.wav",
sfx_achievement_unlock: "assets/sounds/achievement_unlock.mp3"
sfx_achievement_unlock: "assets/sounds/achievement_unlock.mp3",
voice_acquired_stress_001: "assets/sounds/acquired_stress_001.wav",
voice_acquired_stress_002: "assets/sounds/acquired_stress_002.wav",
voice_acquired_stress_003: "assets/sounds/acquired_stress_003.wav",
voice_acquired_stress_004: "assets/sounds/acquired_stress_004.wav",
voice_acquired_stress_005: "assets/sounds/acquired_stress_005.wav",
voice_acquired_stress_006: "assets/sounds/acquired_stress_006.wav",
voice_acquired_stress_009: "assets/sounds/acquired_stress_009.wav",
voice_ochibi_calm_001: "assets/sounds/ochibi_calm_001.wav",
voice_ochibi_calm_002: "assets/sounds/ochibi_calm_002.wav",
voice_ochibi_calm_003: "assets/sounds/ochibi_calm_003.wav",
voice_ochibi_calm_004: "assets/sounds/ochibi_calm_004.wav",
voice_ochibi_calm_005: "assets/sounds/ochibi_calm_005.wav",
voice_ochibi_calm_006: "assets/sounds/ochibi_calm_006.wav",
voice_ochibi_calm_007: "assets/sounds/ochibi_calm_007.wav",
voice_ochibi_calm_008: "assets/sounds/ochibi_calm_008.wav",
voice_ochibi_calm_009: "assets/sounds/ochibi_calm_009.wav",
voice_ochibi_dirty_001: "assets/sounds/ochibi_dirty_001.wav",
voice_ochibi_dirty_002: "assets/sounds/ochibi_dirty_002.wav",
voice_ochibi_dirty_003: "assets/sounds/ochibi_dirty_003.wav",
voice_ochibi_dirty_004: "assets/sounds/ochibi_dirty_004.wav",
voice_ochibi_damage_001: "assets/sounds/ochibi_damage_001.wav",
voice_ochibi_damage_002: "assets/sounds/ochibi_damage_002.wav",
voice_ochibi_damage_003: "assets/sounds/ochibi_damage_003.wav",
voice_ochibi_disease_001: "assets/sounds/ochibi_disease_001.wav",
voice_intimidate_alt: "assets/sounds/intimidate_alt_001.wav",
voice_ochibi_sleep_002: "assets/sounds/ochibi_sleep_002.wav",
voice_ochibi_sleep_003: "assets/sounds/ochibi_sleep_003.wav",
voice_ochibi_fight_hit_004: "assets/sounds/ochibi_fight_hit_004.wav",
voice_ochibi_panic_005: "assets/sounds/ochibi_panic_005.wav",
voice_ochibi_electric_006: "assets/sounds/ochibi_electric_006.wav",
voice_ochibi_hot_007: "assets/sounds/ochibi_hot_007.wav",
voice_ochibi_stress_008: "assets/sounds/ochibi_stress_008.wav",
voice_acquired_stress_008: "assets/sounds/acquired_stress_008.wav",
voice_acquired_stress_007: "assets/sounds/acquired_stress_007.wav",
},
categories: {
voice: "\u305f\u308a\u306a\u3044\u306e\u58f0",
@ -78,6 +180,11 @@
voice_hau: "voice", voice_flee: "voice", voice_poop: "voice", voice_stress: "voice", voice_sunbath: "voice",
voice_major_damage_01: "voice", voice_major_damage_02: "voice", voice_major_damage_03: "voice", voice_major_damage_04: "voice",
voice_temp_cold: "voice", voice_temp_hot: "voice",
voice_acquired_stress_001: "voice", voice_acquired_stress_002: "voice", voice_acquired_stress_003: "voice", voice_acquired_stress_004: "voice", voice_acquired_stress_005: "voice", voice_acquired_stress_006: "voice", voice_acquired_stress_007: "voice", voice_acquired_stress_008: "voice", voice_acquired_stress_009: "voice",
voice_ochibi_calm_001: "voice", voice_ochibi_calm_002: "voice", voice_ochibi_calm_003: "voice", voice_ochibi_calm_004: "voice", voice_ochibi_calm_005: "voice", voice_ochibi_calm_006: "voice", voice_ochibi_calm_007: "voice", voice_ochibi_calm_008: "voice", voice_ochibi_calm_009: "voice",
voice_ochibi_dirty_001: "voice", voice_ochibi_dirty_002: "voice", voice_ochibi_dirty_003: "voice", voice_ochibi_dirty_004: "voice",
voice_ochibi_damage_001: "voice", voice_ochibi_damage_002: "voice", voice_ochibi_damage_003: "voice", voice_ochibi_disease_001: "voice",
voice_intimidate_alt: "voice", voice_ochibi_sleep_002: "voice", voice_ochibi_sleep_003: "voice", voice_ochibi_fight_hit_004: "voice", voice_ochibi_panic_005: "voice", voice_ochibi_electric_006: "voice", voice_ochibi_hot_007: "voice", voice_ochibi_stress_008: "voice",
sfx_eat: "voice", sfx_sleep: "voice", sfx_wake: "voice", sfx_birth: "voice", sfx_death: "voice",
sfx_damage_soft: "voice", sfx_damage_heavy: "voice", sfx_fight_hit: "voice", sfx_fight_finish: "voice",
sfx_disease: "voice", sfx_heal: "voice", sfx_ant_drag: "voice", sfx_ant_die: "voice", sfx_queen_ant: "voice",

View file

@ -16,17 +16,28 @@ function tarinaiRoundRectPath(ctx, x, y, w, h, r) {
}
function tarinaiChampionName(name = "") {
const map = { "\u306F": "\u8987", "\u3046": "\u5B87", "\u3045": "\u30A5", "\u3063": "\uFF01" };
return String(name || "").replace(/[\u306F\u3046\u3045\u3063]/g, ch => map[ch] || ch);
const raw = String(name || "");
if (/おちび(?:の王)?$/.test(raw)) return raw.endsWith("の王") ? raw : `${raw}の王`;
const map = { "は": "覇", "う": "宇", "ぅ": "ゥ", "っ": "" };
return raw.replace(/[はうぅっ]/g, ch => map[ch] || ch);
}
if (typeof window !== "undefined") window.tarinaiChampionName = tarinaiChampionName;
if (typeof globalThis !== "undefined") globalThis.tarinaiChampionName = tarinaiChampionName;
const ACQUIRED_TARINAI_NAME_PREFIXES = ["かしこい", "すごい", "たくましい", "げんきな", "わんぱくな", "ぴかぴかあにゃるの", "しゅーるすとれみんぐ・", "つよい", "もちもちほっぺの", "でかずんぽの", "ちびずんぽの", "すばらしい", "かわいい", "ずんちがくさい", "やさしい", "ゆうかんな", "いいにおいの"];
function acquiredTarinaiBaseName(seed = "", name = "") {
const raw = String(name || "").trim();
if (raw && raw !== "ずんちどれい") return raw.replace(/の王$/, "");
const pick = typeof stableChoice === "function" ? stableChoice(seed || raw || "acquired", "acquired-name-prefix", ACQUIRED_TARINAI_NAME_PREFIXES) : (ACQUIRED_TARINAI_NAME_PREFIXES[0] || "");
return `${pick || ACQUIRED_TARINAI_NAME_PREFIXES[0] || ""}おちび`;
}
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.isAcquiredTarinai = Boolean(opts.isAcquiredTarinai);
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) || {};
@ -34,6 +45,7 @@ class Tarinai { constructor(world, opts = {}) {
this.id = opts.id || "";
this.liveToken = opts.liveToken || 0;
this.name = opts.name || birthProfile.name || makeName(world);
this.acquiredBaseName = opts.acquiredBaseName || null;
this.type = opts.type || birthProfile.type || baseSpriteId();
const typeMeta = SPRITES.find(s => s.id === this.type);
if (typeMeta?.actionOnly || typeMeta?.displayOnly) this.type = birthProfile.type || baseSpriteId();
@ -120,6 +132,29 @@ class Tarinai { constructor(world, opts = {}) {
this.sleeping = false;
this.awakeLockTimer = opts.awakeLockTimer ?? 0;
this.goodMode = opts.goodMode ?? birthProfile.goodMode ?? stableChoice(this.birthSeed || this.familyKey, "good-mode", displayNormalSprites()) ?? "smile";
if (this.isAcquiredTarinai) {
const explicitAcquiredName = this.acquiredBaseName || (Object.prototype.hasOwnProperty.call(opts, "name") ? this.name : "");
this.acquiredBaseName = acquiredTarinaiBaseName(this.birthSeed || this.familyKey, explicitAcquiredName);
this.name = this.acquiredBaseName;
}
if (this.isAcquiredTarinai) {
const acquiredNeeds = { food: 22, sleep: 34, health: 78, safety: 100, social: 90, fulfill: 82 };
const hasExplicitNeedState = Boolean((opts.needs && typeof opts.needs === "object") || (opts.needRaw && typeof opts.needRaw === "object") || (opts.needDisplay && typeof opts.needDisplay === "object") || (opts.needRawBase && typeof opts.needRawBase === "object") || (opts.previousNeeds && typeof opts.previousNeeds === "object"));
if (!hasExplicitNeedState) {
this.needs = { ...acquiredNeeds };
this.needDisplay = { ...acquiredNeeds };
this.needRaw = { ...acquiredNeeds };
this.needRawBase = { ...acquiredNeeds };
this.previousNeeds = { ...acquiredNeeds };
}
if (!Object.prototype.hasOwnProperty.call(opts, "loneliness")) this.loneliness = Math.max(Number(this.loneliness) || 0, 90);
if (!Object.prototype.hasOwnProperty.call(opts, "hunger")) this.hunger = Math.max(Number(this.hunger) || 0, 22);
if (!Object.prototype.hasOwnProperty.call(opts, "energy")) this.energy = Math.min(Number(this.energy) || 0, 45);
if (!Object.prototype.hasOwnProperty.call(opts, "goodMode") && Array.isArray(SPRITES) && SPRITES.some(s => s.id === "jito")) this.goodMode = "jito";
this.stress = typeof calculateStressFromNeeds === "function" ? calculateStressFromNeeds(this.needRaw || this.needs) : this.stress;
this.acquiredStressSpeechAt = Number.isFinite(opts.acquiredStressSpeechAt) ? opts.acquiredStressSpeechAt : ((this.world?.time || 0) + rand(2.8, 6.5));
this.acquiredStressSpeechCooldownUntil = Number.isFinite(opts.acquiredStressSpeechCooldownUntil) ? opts.acquiredStressSpeechCooldownUntil : 0;
}
this.facing = opts.facing ?? (stableUnit(this.familyKey, "facing") < 0.5 ? -1 : 1);
this.visualFacing = opts.visualFacing ?? this.facing;
this.facingLockUntil = opts.facingLockUntil ?? 0;
@ -164,6 +199,9 @@ class Tarinai { constructor(world, opts = {}) {
this.explosionDisease = Boolean(opts.explosionDisease);
this.explosionDiseaseTimer = opts.explosionDiseaseTimer ?? (this.explosionDisease ? 60 : 0);
this.fightDisease = Boolean(opts.fightDisease);
this.mamekusaredakeDisease = Boolean(opts.mamekusaredakeDisease);
this.mamekusaredakeGrowth = Number.isFinite(opts.mamekusaredakeGrowth) ? opts.mamekusaredakeGrowth : 0;
this.mamekusaredakeSpeechAt = Number.isFinite(opts.mamekusaredakeSpeechAt) ? opts.mamekusaredakeSpeechAt : 0;
this.fightDiseaseCooldown = opts.fightDiseaseCooldown ?? 0;
this.lastBleedAt = opts.lastBleedAt ?? -999;
this.favorite = Boolean(opts.favorite);

View file

@ -4,12 +4,34 @@
const Tarinai = global.Tarinai;
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_action_state.js");
function prepareTarinaiIntimidatePresentation(tarinai) {
if (!tarinai || tarinai.dead) return "intimidate";
if (!tarinai.intimidateSpriteVariant) {
tarinai.intimidateSpriteVariant = tarinai.isAcquiredTarinai ? "intimidate_alt" : (Math.random() < 0.5 ? "intimidate_alt" : "intimidate");
}
if (tarinai.intimidateSpriteVariant === "intimidate_alt" && !tarinai._intimidateAltAnnounced) {
tarinai._intimidateAltAnnounced = true;
tarinai.bubble?.("ぷきゅー!", 1.0, "rgba(92,62,34,0.82)", { force: true });
const audio = global.TarinaiAudio;
if (audio?.playSample) audio.playSample("voice_intimidate_alt", 0.9, "voice") || audio.play?.("voice_intimidate_alt", { category: "voice", minGap: 0.9 });
else audio?.play?.("voice_intimidate_alt", { category: "voice", minGap: 0.9 });
}
return tarinai.intimidateSpriteVariant;
}
global.prepareTarinaiIntimidatePresentation = prepareTarinaiIntimidatePresentation;
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
setActionState(state, opts = {}) {
if (this.dead) return false;
const wasSleeping = this.state === "sleep" || this.sleeping;
const previousState = this.state;
const nextState = state || "idle";
this.state = nextState;
if (nextState === "intimidate" || nextState === "ant_intimidate") prepareTarinaiIntimidatePresentation(this);
else if ((previousState === "intimidate" || previousState === "ant_intimidate") && nextState !== previousState) {
this.intimidateSpriteVariant = "";
this._intimidateAltAnnounced = false;
}
if ("target" in opts) this.target = opts.target;
if (opts.reason != null) this.thought = String(opts.reason || "");
if (opts.clearTarget) this.target = null;
@ -204,7 +226,17 @@
this.fallTimer = Math.max(this.fallTimer || 0, opts.fallTimer);
this.fallMax = Math.max(this.fallMax || 0, this.fallTimer);
}
if (opts.bubble) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)");
if (opts.bubble && !this.isAcquiredTarinai) this.bubble?.(opts.bubble, opts.bubbleCooldown ?? 0.9, opts.bubbleColor || "rgba(84,66,86,0.80)");
if (this.isAcquiredTarinai) {
const now = Number(this.world?.time || 0) || 0;
if (now >= Number(this.ochibiPanicVoiceAt || 0)) {
this.ochibiPanicVoiceAt = now + 2.4;
if (this.bubble?.("きょわいのだ...", 1.8, "rgba(84,66,86,0.80)", { force: true })) {
const a = global.TarinaiAudio;
if (a?.playSample) a.playSample("voice_ochibi_panic_005", 1.0, "voice") || a.play?.("voice_ochibi_panic_005", { category: "voice", minGap: 1.0 });
}
}
}
this.lastPanicCause = opts.cause || opts.reason || this.thought || "panic";
this.lastPanicAt = this.world?.time || 0;
if (this.state === "panic" && !Number.isFinite(this.panicStartedAt)) this.panicStartedAt = this.lastPanicAt;

View file

@ -22,11 +22,9 @@ const d = contact.distance;
tarinai.tarinaiChampionSince = 0;
tarinai.becomeZunchiSlave?.({ locked: true, forcedItem: true });
tarinai.world?.syncTarinaiCountEntry?.(tarinai);
tarinai.bubble("\u9396", 2.4, "rgba(74,64,56,0.82)");
tarinai.world?.log?.(`${tarinai.name}\u306F${itemLabel}\u306B\u89E6\u308C\u3066\u305A\u3093\u3061\u3069\u308C\u3044\u306B\u306A\u3063\u305F\u3002`, "event", { participants: [tarinai], sound: false });
} else {
tarinai.becomeTarinaiChampion?.({ force: true, forcePersonality: true, forcedItem: true });
tarinai.bubble("\u738B", 2.4, "rgba(166,112,22,0.84)");
tarinai.world?.log?.(`${tarinai.name}\u306F${itemLabel}\u306B\u89E6\u308C\u3066\u305F\u308A\u306A\u3044\u738B\u306B\u306A\u3063\u305F\u3002`, "event", { participants: [tarinai], sound: false });
}
global.TarinaiAchievements?.recordConsumableUse?.({ world: tarinai.world, tarinai, item: it, type: it.type, source: "contact" });

View file

@ -3,6 +3,34 @@
// Food, medicine, grass, ant-corpse, and zunchi eating branch for Tarinai item interaction.
(function (global) {
const OCHIBI_DIRTY_SPEECHES = [
{ id: "voice_ochibi_dirty_001", text: "くちゃいのだ" },
{ id: "voice_ochibi_dirty_002", text: "ずんちがめだまについたのだ!" },
{ id: "voice_ochibi_dirty_003", text: "おえええ!" },
{ id: "voice_ochibi_dirty_004", text: "ずんちがおくちについたのだ!" }
];
function maybePlayOchibiDirtySpeech(tarinai, delta = 0) {
if (!tarinai || !tarinai.isAcquiredTarinai || tarinai.dead) return false;
if (!(delta > 0.01)) return false;
const now = Number(tarinai.world?.time || 0) || 0;
const nextAllowed = Number(tarinai.ochibiDirtySpeechCooldownUntil || 0) || 0;
const lastLevel = Number(tarinai.ochibiDirtySpeechLastLevel || 0) || 0;
const currentLevel = Number(tarinai.zunchiStain || 0) || 0;
if (now < nextAllowed && currentLevel < lastLevel + 5) return false;
if ((tarinai.nextBubbleAt || 0) > now) return false;
const idx = Math.floor(Math.random() * OCHIBI_DIRTY_SPEECHES.length) % OCHIBI_DIRTY_SPEECHES.length;
const line = OCHIBI_DIRTY_SPEECHES[idx];
if (!line) return false;
const bubbled = tarinai.bubble?.(line.text, 4.8, "rgba(82,66,42,0.84)");
if (!bubbled) return false;
const audio = global.TarinaiAudio;
if (audio?.playSample) audio.playSample(line.id, 1.5, "voice") || audio.play?.(line.id, { category: "voice", minGap: 1.5 });
else if (audio?.play) audio.play(line.id, { category: "voice", minGap: 1.5 });
tarinai.ochibiDirtySpeechCooldownUntil = now + 3.2;
tarinai.ochibiDirtySpeechLastLevel = currentLevel;
return true;
}
function updateCandidate(tarinai, ctx, it, dt = 0) {
if (!tarinai || tarinai.dead || !ctx || !it || it.dead) return false;
if (it.type === "duplicator" && global.TarinaiSignalSystem?.powerOverride?.(it) === false) return false;
@ -157,7 +185,7 @@ if (it.type === "zunchi") {
applyNeedShock(tarinai, { health: touch * dt * 7, safety: touch * dt * 4 });
if (touch > 0.08) tarinai.adjustPersonality("neuroticism", -dt * touch * 0.0045, "after repeated contact with poop.");
}
tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + touch * dt * (tarinai.isZunchiSlave ? 5.2 : 9.5), 0, 100);
{ const __prev = Number(tarinai.zunchiStain || 0) || 0; tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + touch * dt * (tarinai.isZunchiSlave ? 5.2 : 9.5), 0, 100); maybePlayOchibiDirtySpeech(tarinai, (Number(tarinai.zunchiStain || 0) || 0) - __prev); }
const emergencyHunger = (tarinai.hunger || 0) >= 104 || foodNeed >= 94;
const zunchiTooFresh = zunchiEatingBlocked(tarinai, it, tarinai.world) || (!emergencyHunger && !tarinai.isZunchiSlave && it.stage === "fresh" && (it.age || 0) < 150 && foodNeed < 94 && (tarinai.hunger || 0) < 104);
if (canZunchiBite && !zunchiTooFresh) {
@ -171,7 +199,7 @@ if (it.type === "zunchi") {
tarinai.recoverHealth(bite * (tarinai.isZunchiSlave ? 0.10 : 0.02));
applyNeedShock(tarinai, { health: bite * (tarinai.isZunchiSlave ? 0.2 : 1.2), safety: bite * (tarinai.isZunchiSlave ? 0.1 : 0.6) });
if (!tarinai.isZunchiSlave) tarinai.adjustPersonality("neuroticism", -bite * 0.0015, "after repeated contact with poop.");
tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + bite * (tarinai.isZunchiSlave ? 0.75 : 1.75), 0, 100);
{ const __prev = Number(tarinai.zunchiStain || 0) || 0; tarinai.zunchiStain = clamp((tarinai.zunchiStain || 0) + bite * (tarinai.isZunchiSlave ? 0.75 : 1.75), 0, 100); maybePlayOchibiDirtySpeech(tarinai, (Number(tarinai.zunchiStain || 0) || 0) - __prev); }
tarinai.lastMealColor = "#6b8d3f";
tarinai.eatTimer = Math.max(tarinai.eatTimer, 1.0);
tarinai.eatCooldown = rand(1.0, 1.22);

View file

@ -295,7 +295,7 @@
const x = this.x - side * this.radius * rand(0.72, 0.96);
const y = this.y + this.radius * rand(0.25, 0.48);
this.poopCount = (this.poopCount || 0) + 1;
this.world.spawnZunchi?.(x, y);
this.world.spawnZunchi?.(x, y, this);
this.digest = Math.max(0, (this.digest || 0) - 3.0);
const now = this.world?.time || 0;
this.bubble("\u3076\u308a\u3085\u3063", 1.2, effectColor("laxative"), { force: true });

View file

@ -182,6 +182,15 @@ function updateNeeds(tarinai, world, dt = 0) {
raw.health *= clamp(1 + Math.max(0, neuroticism) * 0.14, 0.9, 1.22);
raw.fulfill *= clamp(1 + Math.max(0, openness) * 0.14, 0.9, 1.24);
// おちびは、通常個体よりも精神的な負荷が高い状態を基準にする。
// ここで加算することで、初回の needs 再計算で初期ストレスが消えない。
if (tarinai.isAcquiredTarinai) {
raw.health += 44;
raw.safety += 84;
raw.social += 20;
raw.fulfill += 34;
}
decayNeedSatisfaction(tarinai, dt);
const effective = createDefaultNeeds();
const display = createDefaultNeeds();
@ -651,7 +660,17 @@ function protectSleepSession(tarinai, world, needs) {
const now = world?.time || 0;
if (now >= (tarinai.nextSleepBubbleAt || 0)) {
tarinai.nextSleepBubbleAt = now + rand(3.6, 5.6);
world?.spawnBubble?.(tarinai.x, tarinai.y - (tarinai.radius || 20) * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
if (tarinai.isAcquiredTarinai && Math.random() < 0.42) {
const sleepLine = Math.random() < 0.5
? { id: "voice_ochibi_sleep_002", text: "おててしゃん..." }
: { id: "voice_ochibi_sleep_003", text: "あんよしゃん..." };
if (tarinai.bubble?.(sleepLine.text, 2.8, "rgba(65,70,92,0.72)", { force: true })) {
const a = globalThis.TarinaiAudio;
if (a?.playSample) a.playSample(sleepLine.id, 1.0, "voice") || a.play?.(sleepLine.id, { category: "voice", minGap: 1.0 });
}
} else {
world?.spawnBubble?.(tarinai.x, tarinai.y - (tarinai.radius || 20) * 1.18, "Zzz...", "rgba(65,70,92,0.72)");
}
}
return true;
}
@ -898,7 +917,7 @@ function resolveNeedsCore(dt) {
if (this.isZunchiSlave) return this.zunchiSlaveSpriteId();
const low = this.lowHealthSprite();
if (low) return low;
if ((this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back";
if (!this.isAcquiredTarinai && (this.state === "seek_food" || this.state === "idle" || this.state === "follow_parent") && (this.vy < -18 || (this.target && this.target.y < this.y - 36 && Math.abs(this.target.y - this.y) > Math.abs(this.target.x - this.x) * 0.55))) return "back";
if (this.explosionDisease && Math.sin((this.world?.time || 0) * 8) > 0.15) return "pokan";
if (this.fightDisease) return "stress_dizzy";
if ((this.oshiriByoZunchiStock || 0) >= 6) return this.variantSprite ? this.variantSprite("hurt") : "weak";
@ -912,7 +931,9 @@ function resolveNeedsCore(dt) {
if (this.birthRitualTimer > 0.04 || this.state === "birth_ritual") return "birth_ritual";
if (foodNeedForSprite >= 72) return "hungry_70";
if (this.defeatedTimer > 0.04) return this.variantSprite("flee");
if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) return this.variantSprite("intimidate");
if (this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04) {
return globalThis.prepareTarinaiIntimidatePresentation?.(this) || this.intimidateSpriteVariant || "intimidate";
}
if (this.state === "panic" || this.state === "cursor_enemy" || this.fearTimer > 0.08 || this.intimidatedTimer > 0.08) return this.variantSprite("fear");
if (this.state === "fight" || this.state === "seek_enemy" || this.fightTimer > 0.04) return "angry";
if (this.stress > 62) return this.variantSprite("stress");
@ -947,7 +968,7 @@ function resolveNeedsCore(dt) {
const isCurrentIntimidator = this.state === "intimidate" || this.state === "ant_intimidate" || this.intimidateTimer > 0.04;
const isCurrentIntimidated = !isCurrentIntimidator && this.intimidatedTimer > 0.04;
const trappedNow = Number(this._bearTrapHoldUntil || 0) > now;
const forceImmediate = trappedNow || String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || next === "intimidate" || isCurrentIntimidator || isCurrentIntimidated || (this.visibleSpriteId === "intimidate" && next !== "intimidate");
const forceImmediate = trappedNow || String(next || "").startsWith("zunchi_slave") || next === "birth_ritual" || String(next || "").startsWith("intimidate") || isCurrentIntimidator || isCurrentIntimidated || (String(this.visibleSpriteId || "").startsWith("intimidate") && !String(next || "").startsWith("intimidate"));
if (!this.visibleSpriteId || forceImmediate) {
this.visibleSpriteId = next;
this.spriteLockUntil = forceImmediate ? now : now + 1.0;
@ -1043,6 +1064,7 @@ function resolveNeedsCore(dt) {
},
canStartSunbath(options = {}) {
if (this.isAcquiredTarinai || this.mamekusaredakeDisease) return false;
if (this.dead || this.sleeping) return false;
if (!Number.isFinite(this.x) || !Number.isFinite(this.y)) return false;
if (!Number.isFinite(this.world?.w) || !Number.isFinite(this.world?.h)) return false;
@ -1125,8 +1147,24 @@ function resolveNeedsCore(dt) {
},
bubble(text, cooldown = 2.4, color = "rgba(42,36,29,0.78)", options = {}) {
const bubbleText = String(text || "");
if (this.isAcquiredTarinai) {
const allowSubstring = ["Zzz", "あちゅい", "もぐ", "ぶるぶる"];
const allowExact = new Set([
"パパずんはどこなのだ?", "ママずんはどこなのだ?", "こんなはずじゃなかったのだ", "どうして", "おまえをゆるさないのだ", "のろってやるのだ",
"てあしをかえせなのだ", "すべすべしっぽしゃんが", "くちょにんげん",
"ぼくはゆうしゅうなのだ", "しあわせなのだ", "てあしがなくてもげんきなのだ", "へこへこしたいのだ", "のだのだのだ",
"くちょおやがいなくてもぼくはたたかえるのだ", "ぼくはげんきいっぱいなのだ", "かっきにみちみちしているのだ", "ずんちでそう",
"くちゃいのだ", "ずんちがめだまについたのだ!", "おえええ!", "ずんちがおくちについたのだ!",
"いちゃい!", "ごふっ", "ああああああああ",
"ぷきゅー!", "くるち、くるち", "おててしゃん...", "あんよしゃん...", "ちね!", "きょわいのだ...", "びりびりちゃ!", "おみじゅ...", "なんでぼくがこんなめに..."
]);
const plainPunct = /^[!?!?…\.。ー〜~]+$/.test(bubbleText);
const allowed = plainPunct || allowExact.has(bubbleText) || allowSubstring.some(part => bubbleText.includes(part));
if (!allowed) return false;
}
if (!options.force && this.world.time < this.nextBubbleAt) return false;
const spawned = this.world.spawnBubble(this.x, this.y - this.radius * 1.35, text, color);
const spawned = this.world.spawnBubble(this.x, this.y - this.radius * 1.35, bubbleText, color);
if (!spawned) return false;
this.nextBubbleAt = this.world.time + cooldown;
return true;

View file

@ -1,5 +1,13 @@
"use strict";
function drawAcquiredTarinaiOverlay(ctx, tarinai, drawW, drawH) {
if (!tarinai?.isAcquiredTarinai) return false;
const overlay = getRenderableImage("acquired_tarinai_overlay", "");
if (!overlay) return false;
ctx.drawImage(overlay, -drawW / 2, -drawH / 2, drawW, drawH);
return true;
}
function tarinaiSpriteReversesFacing(sprite) {
const id = String(sprite || "");
return id === "back" || id === "tarinai12_back" || /(^|_)12_back$/i.test(id);
@ -170,6 +178,7 @@ function tarinaiSpriteReversesFacing(sprite) {
ctx.fill();
ctx.stroke();
}
drawAcquiredTarinaiOverlay(ctx, this, drawW, drawH);
ctx.restore();
if (showDecor) {
@ -200,6 +209,11 @@ function tarinaiSpriteReversesFacing(sprite) {
const showDecor = shouldShowTarinaiDecor(this);
if (sunbathActive) this.preloadSunbathSprites?.();
let sprite = this.spriteId();
if (this.isAcquiredTarinai && tarinaiSpriteReversesFacing(sprite)) {
const fallbackOchibiSprite = this.applyTemperatureSprite?.(this.goodMode || this.type || "smile") || this.goodMode || this.type || "smile";
this.visibleSpriteId = "";
sprite = fallbackOchibiSprite;
}
if (!sunbathActive && String(sprite || "").startsWith("sunbath")) {
this.visibleSpriteId = "";
this.spriteLockUntil = 0;
@ -273,7 +287,7 @@ function tarinaiSpriteReversesFacing(sprite) {
drawW *= 1.16;
drawH *= 1.24;
}
if (sprite === "intimidate") {
if (String(sprite || "").startsWith("intimidate")) {
drawW *= 1.32;
drawH *= 1.32;
}
@ -526,6 +540,12 @@ function tarinaiSpriteReversesFacing(sprite) {
ctx.stroke();
ctx.restore();
}
drawAcquiredTarinaiOverlay(ctx, this, dw, dh);
globalThis.TarinaiMamekusaredakeSystem?.drawOnTarinai?.(ctx, this, dw, dh, faceRight, img, {
originX: this.x + intimidateShake + coldShakeX,
originY: this.y + bob + fearShakeY + coldShakeY,
angle
});
if (img && (this.zunchiStain || 0) > 3) {
const stainCanvas = getCachedStainOverlayCanvas(renderSprite || sprite, img, cacheW, cacheH, clamp((this.zunchiStain || 0) / 100, 0, 1), this.zunchiStainSeed || 0.5);
if (stainCanvas) {

View file

@ -376,6 +376,17 @@
o._achievementLastFightDamageAt = this.world.time || 0;
this.damage(damageToThis, "\u55a7\u5629");
o.damage(damageToOther, "\u55a7\u5629");
const nowFightVoice = Number(this.world?.time || 0) || 0;
const ochibiAttackVoice = attacker => {
if (!attacker?.isAcquiredTarinai || nowFightVoice < Number(attacker.ochibiFightHitVoiceAt || 0)) return;
attacker.ochibiFightHitVoiceAt = nowFightVoice + 1.25;
if (attacker.bubble?.("ちね!", 1.0, "rgba(120,54,44,0.84)", { force: true })) {
const a = globalThis.TarinaiAudio;
if (a?.playSample) a.playSample("voice_ochibi_fight_hit_004", 0.8, "voice") || a.play?.("voice_ochibi_fight_hit_004", { category: "voice", minGap: 0.8 });
}
};
if (damageToOther > 0.01) ochibiAttackVoice(this);
if (damageToThis > 0.01) ochibiAttackVoice(o);
this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis);
this.world.maybeDefeatFromFightDamage?.(o, this, damageToOther);
this.hurtTimer = Math.max(this.hurtTimer, 1.4);
@ -815,6 +826,7 @@
if (this.sleepDisease) return "\u306d\u3080\u308a\u75c5";
if (this.explosionDisease) return "\u7206\u767a\u75c5";
if (this.fightDisease) return "\u304d\u305a\u3064\u304d\u75c5";
if (this.mamekusaredakeDisease) return "マメクサレダケ病";
return "";
};
if (this.energy <= 0) {
@ -834,6 +846,7 @@
this.dead = true;
this.deathReason = finalReason;
this.deathTime = Number(this.world?.time || 0) || 0;
if (this.mamekusaredakeDisease) globalThis.TarinaiMamekusaredakeSystem?.plantFromHost?.(this);
this.world.syncTarinaiCountEntry?.(this);
this.world.deadCount += 1;
globalThis.TarinaiAchievements?.evaluateEvent?.(this.world, "population", { tarinai: this, delta: -1, reason: finalReason });

View file

@ -18,8 +18,62 @@
return global.deterministicChance(worldRef, salt, chance, ...parts);
}
const OCHIBI_HIGH_STRESS_SPEECHES = [
{ id: "voice_acquired_stress_001", text: "パパずんはどこなのだ?" },
{ id: "voice_acquired_stress_002", text: "ママずんはどこなのだ?" },
{ id: "voice_acquired_stress_003", text: "こんなはずじゃなかったのだ" },
{ id: "voice_acquired_stress_004", text: "どうして" },
{ id: "voice_acquired_stress_005", text: "おまえをゆるさないのだ" },
{ id: "voice_acquired_stress_006", text: "のろってやるのだ" },
{ id: "voice_acquired_stress_007", text: "てあしをかえせなのだ" },
{ id: "voice_acquired_stress_008", text: "すべすべしっぽしゃんが" },
{ id: "voice_acquired_stress_009", text: "くちょにんげん" },
{ id: "voice_ochibi_stress_008", text: "なんでぼくがこんなめに..." }
];
const OCHIBI_LOW_STRESS_SPEECHES = [
{ id: "voice_ochibi_calm_001", text: "ぼくはゆうしゅうなのだ" },
{ id: "voice_ochibi_calm_002", text: "しあわせなのだ" },
{ id: "voice_ochibi_calm_003", text: "てあしがなくてもげんきなのだ" },
{ id: "voice_ochibi_calm_004", text: "へこへこしたいのだ" },
{ id: "voice_ochibi_calm_005", text: "のだのだのだ" },
{ id: "voice_ochibi_calm_006", text: "くちょおやがいなくてもぼくはたたかえるのだ" },
{ id: "voice_ochibi_calm_007", text: "ぼくはげんきいっぱいなのだ" },
{ id: "voice_ochibi_calm_008", text: "かっきにみちみちしているのだ" },
{ id: "voice_ochibi_calm_009", text: "ずんちでそう" }
];
function maybePlayOchibiIdleSpeech(t) {
if (!t || t.dead || !t.isAcquiredTarinai || String(t.state || "") !== "idle") return false;
const now = Number(t.world?.time || 0) || 0;
const stress = Number(t.stress || 0) || 0;
const highStress = stress >= 68;
const pool = highStress ? OCHIBI_HIGH_STRESS_SPEECHES : OCHIBI_LOW_STRESS_SPEECHES;
if (!pool.length) return false;
if (!Number.isFinite(t.acquiredStressSpeechAt)) t.acquiredStressSpeechAt = now + detRange(t.world, "ochibi-idle-speech-init", 2.8, 6.5, t);
if ((t.acquiredStressSpeechCooldownUntil || 0) > now || (t.nextBubbleAt || 0) > now || now < t.acquiredStressSpeechAt) return false;
const idx = Math.max(0, Math.min(pool.length - 1, Math.floor(detRange(t.world, highStress ? "ochibi-high-line" : "ochibi-low-line", 0, pool.length, t, Math.floor(now * 10)))));
const line = pool[idx] || pool[0];
if (!line) return false;
const color = highStress ? "rgba(58,48,63,0.82)" : "rgba(62,84,45,0.78)";
const bubbled = typeof t.bubble === "function" ? t.bubble(line.text, 5.8, color) : false;
if (!bubbled) return false;
const audio = global.TarinaiAudio;
if (audio?.playSample) audio.playSample(line.id, 1.5, "voice") || audio.play(line.id, { category: "voice", minGap: 1.5 });
else if (audio?.play) audio.play(line.id, { category: "voice", minGap: 1.5 });
t.acquiredStressSpeechCooldownUntil = now + 1.5;
t.acquiredStressSpeechAt = now + detRange(t.world, highStress ? "ochibi-high-next" : "ochibi-low-next", highStress ? 6.5 : 7.0, highStress ? 12.0 : 13.0, t, line.id, Math.floor(now));
return true;
}
function update(t, dt, context = {}) {
if (!t || t.dead) return { done: true };
if (t.isAcquiredTarinai && (t.state === "sunbath" || (t.sunbathTimer || 0) > 0.04)) {
t.sunbathTimer = 0;
t.sunbathLeisure = false;
t.sunbathCooldown = Math.max(t.sunbathCooldown || 0, 18);
t.visibleSpriteId = "";
t.setActionState?.("idle", { target: null, reason: "おちびは日光浴をしない", sleeping: false, clearTarget: true }); // acquired-sunbath-cancel
}
t.prevX = t.x;
t.prevY = t.y;
@ -47,6 +101,7 @@
t._achievementLaxativeActiveThisFrame = Boolean(t.hasItemEffect?.("laxative"));
if (t.updateBurning) t.updateBurning(dt);
if (t.updateItemEffects) t.updateItemEffects(dt);
global.TarinaiMamekusaredakeSystem?.updateTarinai?.(t, dt);
if (t.hasItemEffect?.("sedative")) {
t.fearTimer = Math.max(0, (t.fearTimer || 0) - dt * 1.8);
t.intimidatedTimer = Math.max(0, (t.intimidatedTimer || 0) - dt * 1.4);
@ -139,10 +194,11 @@
t.setActionState?.("idle", { target: null, reason: "\u8db3\u3064\u307c\u306e\u7a81\u8d77\u306b\u3064\u307e\u3065\u3044\u305f\u3002", sleeping: false, clearTarget: false });
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0)) t.bubble?.("\u3046\u3063", 1.6, "rgba(86,70,96,0.78)");
}
if (t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && detChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
maybePlayOchibiIdleSpeech(t);
if (!t.isAcquiredTarinai && t.stress > 92 && t.state !== "sleep" && t.state !== "fight" && t.state !== "eat" && detChance(t.world, "stress-breath-bubble", dt * 0.050, t)) {
t.bubble("\u306f\u3063...\u306f\u3063...", 3.4, "rgba(58,48,63,0.80)");
}
if (t.state === "idle" && t.stress < 78 && detChance(t.world, "idle-bubble", dt * 0.018, t)) {
if (!t.isAcquiredTarinai && t.state === "idle" && t.stress < 78 && detChance(t.world, "idle-bubble", dt * 0.018, t)) {
t.bubble(pick(["\u306f\u3046", "\u306f\u3046\u3045", "\u306f\u3046\u3063", "\u306f\u3045"]), 4.2, "rgba(62,84,45,0.76)");
}
if (t.hurtTimer > 0 && detChance(t.world, "hurt-bleed-effect", dt * 5.2, t)) {
@ -176,7 +232,14 @@
const stressRate = excess * (0.66 + Math.min(20, excess) * 0.035);
applyNeedShock(t, { safety: stressRate * dt, health: stressRate * dt * 0.42 });
if ((t.world?.time || 0) >= (t.nextBubbleAt || 0) && detChance(t.world, "temperature-stress-bubble", dt * 0.18, t)) {
t.bubble(tempStatus.direction === "cold" ? "\u3076\u308B\u3076\u308B" : "\u3042\u3061\u3085\u3044", 2.4, "rgba(76,62,72,0.78)");
if (t.isAcquiredTarinai && tempStatus.direction === "hot" && Math.random() < 0.68) {
if (t.bubble("おみじゅ...", 2.4, "rgba(76,62,72,0.78)")) {
const a = global.TarinaiAudio;
if (a?.playSample) a.playSample("voice_ochibi_hot_007", 1.2, "voice") || a.play?.("voice_ochibi_hot_007", { category: "voice", minGap: 1.2 });
}
} else {
t.bubble(tempStatus.direction === "cold" ? "ぶるぶる" : "あちゅい", 2.4, "rgba(76,62,72,0.78)");
}
}
}
if ((tempStatus.harmExcess || 0) > 0) {

View file

@ -195,6 +195,25 @@
}
function beginGrab(p, clientX, clientY, options = {}) {
// マメクサレダケ病の除去判定は、キノコ自体の小さな見た目ではなく
// 通常のたりないをつまむ時と同じ個体当たり判定を使う。
// 一度命中すれば recover() が病気フラグを解除するため、表示中の全キノコが一括で抜ける。
let infectedTarinai = null;
for (let i = (world.tarinai?.length || 0) - 1; i >= 0; i--) {
const candidate = world.tarinai[i];
if (!candidate || candidate.dead || !candidate.mamekusaredakeDisease || world.isTarinaiHiddenInNestBox?.(candidate)) continue;
const hit = candidate.contains
? candidate.contains(p.x, p.y)
: distXY(p.x, p.y, candidate.x, candidate.y) <= (candidate.radius || 22) * 1.4;
if (hit) { infectedTarinai = candidate; break; }
}
if (infectedTarinai) {
global.TarinaiHistory.capture(world, "pinch-mamekusaredake");
global.TarinaiMamekusaredakeSystem?.recover?.(infectedTarinai, "つまんで取れた");
audio.grab?.();
world.drawListDirty = true;
return true;
}
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
if (!target) {
if (options.notifyNoTarget) showToast("\u3064\u307e\u3081\u308b\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002");

View file

@ -228,6 +228,11 @@ function selectTool(tool) {
const wasCopyArmed = world.tool === "copy" && tool === "copy" && Boolean(world.copyBuffer);
const result = window.TarinaiCommands.dispatch(world, { type: "tool.select", toolId: tool });
const activeTool = result?.toolId || world.tool || tool;
// A freshly selected tool must not inherit drag/click-suppression state from
// the previous tool. Reset these transients for every tool, not for one item.
if (typeof uiCache === "object" && uiCache) {
for (const key of ["areaDeleteMoved", "hoseMoved", "grabMoved", "shootSuppressClick", "panMoved"]) uiCache[key] = false;
}
if (!["rope", "rod", "spring", "wire", "insulated_wire"].includes(activeTool)) world.pendingLinkEndpoint = null;
if (activeTool !== "copy") world.copyBuffer = null;
else if (wasCopyArmed) {

View file

@ -222,7 +222,7 @@ function renderSelected() {
relationEntries(t, "friend").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), relationEntries(t, "enemy").map(r => `${r.id}:${fmt(r.score)}:${r.name}`).join(","), fightWinRateLabel(t),
t.isZunchiSlave ? 1 : 0, t.isTarinaiChampion ? 1 : 0, fmt(t.totalFightWins || 0), fmt(t.totalFightLosses || 0), fmt(t.loveMochiTimer || 0), fmt(t.fightMochiTimer || 0),
t.zunchiDisease ? 1 : 0, fmt(t.zunchiDiseaseSeverity || 0), fmt(t.zunchiStain || 0),
t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.favorite ? 1 : 0,
t.sleepDisease ? 1 : 0, t.explosionDisease ? 1 : 0, fmt(t.explosionDiseaseTimer || 0), t.fightDisease ? 1 : 0, t.mamekusaredakeDisease ? 1 : 0, fmt(t.mamekusaredakeGrowth || 0), t.favorite ? 1 : 0,
personalitySnapshot, personalityDailySnapshot(t),
["life", "attack", "speed", "size"].map(key => geneticPercentText(t, key)).join(","), geneticComfortTemperatureText(t), fmt((t.growth || 0) * 100),
itemEffectSummary.map(e => `${e.id}:${e.detail}`).join(","),
@ -241,6 +241,7 @@ function renderSelected() {
if (t.sleepDisease) effectBadges.push(`<span class="badge">\u306d\u3080\u308a\u75c5</span>`);
if (t.explosionDisease) effectBadges.push(`<span class="badge">\u7206\u767a\u75c5 ${fmt(t.explosionDiseaseTimer || 0)}</span>`);
if (t.fightDisease) effectBadges.push(`<span class="badge">\u304d\u305a\u3064\u304d\u75c5</span>`);
if (t.mamekusaredakeDisease) effectBadges.push(`<span class="badge">マメクサレダケ病 ${fmt((t.mamekusaredakeGrowth || 0) * 100)}%</span>`);
if ((t.loveMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3078\u3053\u9905 ${fmt(t.loveMochiTimer)}</span>`);
if ((t.fightMochiTimer || 0) > 0.1) effectBadges.push(`<span class="badge">\u3051\u3093\u304b\u9905 ${fmt(t.fightMochiTimer)}</span>`);
const personalityTagHtml = personalityTags.length

View file

@ -371,6 +371,7 @@ function attachToolSpritePreview(btn, def, toolId) {
const itemType = def?.itemType || toolId;
if (!btn || !itemType || !def?.placeable || TEXT_ICON_TOOL_IDS.has(toolId)) return;
if (toolId === "reciprocator") return;
if (toolId === "mamekusaredake") return;
if (toolId === "ball" || itemType === "ball") return;
if (typeof window.drawToolItemPreview !== "function") return;

View file

@ -1,8 +1,8 @@
"use strict";
(function () {
const APP_VERSION = "39.18.3";
const APP_BUILD = "tarinai-ui-v39-18-3";
const APP_VERSION = "39.18.18";
const APP_BUILD = "tarinai-ui-v39-18-18";
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;

View file

@ -268,7 +268,7 @@
this.spawnFallEffect(t.x, t.y + t.radius * 0.45, 1.1 + p);
const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15;
if (!t.dead && p > 0.22 && deterministicChance(this, "firecracker-explosion-disease", t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance, it, t)) t.infectExplosionDisease?.(it);
if (!t.dead && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
if (!t.dead && !t.isAcquiredTarinai && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
}
if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it);
this.applyExplosionExternalForces?.(x, y, blastRadius, it, it.blastScale || 1, "firecracker");
@ -368,7 +368,7 @@
this.spawnFallEffect(o.x, o.y + o.radius * 0.45, 1.1 + q);
const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15;
if (!o.dead && q > 0.22 && deterministicChance(this, "disease-explosion-spread", o.diseaseChance ? o.diseaseChance(explosionChance) : explosionChance, t, o)) o.infectExplosionDisease?.(t);
if (!o.dead && deterministicChance(this, "disease-explosion-panic-bubble", 0.45, t, o)) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
if (!o.dead && !o.isAcquiredTarinai && deterministicChance(this, "disease-explosion-panic-bubble", 0.45, t, o)) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
}
this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", {
push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale,
@ -568,6 +568,9 @@
},
spawnZunchi(x, y, source = null) {
if (source?.mamekusaredakeDisease && Math.random() < 0.05) {
globalThis.TarinaiMamekusaredakeSystem?.recover?.(source, "ずんちで取れた");
}
const spawnX = clamp(x, 40, this.w - 40);
const spawnY = clamp(y, 40, this.h - 40);
const sand = this.toiletSandAt?.(spawnX, spawnY) || null;

View file

@ -998,9 +998,11 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
spawnChild(a, b, index = 0, total = 1) {
const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave);
const ochibiChild = !!(a.isAcquiredTarinai && b.isAcquiredTarinai);
const child = this.addTarinai({
seedParents: [a, b],
isZunchiSlave: zunchiSlaveChild,
isAcquiredTarinai: ochibiChild,
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),
@ -1115,6 +1117,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
target.intimidationCooldown = Math.max(target.intimidationCooldown || 0, deterministicRange(this, "fight-mochi-intimidate-repeat-cooldown-target", 4.5, 7.5, actor, target));
actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, deterministicRange(this, "fight-mochi-intimidate-actor-timer", 0.32, 0.58, actor, target));
actor.intimidateTargetId = target.id;
globalThis.prepareTarinaiIntimidatePresentation?.(actor);
actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28);
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "fight-mochi-intimidate-target-timer", 0.30, 0.56, actor, target));
const targetProfile = socialPersonalityProfile(target);
@ -1126,7 +1129,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
size: actor.radius * 0.62,
color: "rgba(154, 88, 42, 0.74)",
});
this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "!", "rgba(120,62,38,0.82)");
if (actor.intimidateSpriteVariant !== "intimidate_alt" && !actor.isAcquiredTarinai) this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "!", "rgba(120,62,38,0.82)");
return true;
},
@ -1189,7 +1192,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
size: actor.radius * 0.55,
color: "rgba(145, 106, 55, 0.70)",
});
this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)");
if (actor.intimidateSpriteVariant !== "intimidate_alt" && !actor.isAcquiredTarinai) this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)");
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-target-prep-timer", 0.28, 0.52, actor, target));
target.fearTimer = Math.max(target.fearTimer, 0.65 * targetProfile.fear);
if (!deterministicChance(this, "intimidation-success", successChance, actor, target)) {
@ -1212,7 +1215,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-success-timer", 0.34, 0.70, actor, target));
if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.35, wake: true, cause: "intimidated" });
else { target.fearTimer = Math.max(target.fearTimer, 1.35 * targetProfile.fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); }
this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
if (!target.isAcquiredTarinai) this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
target.vx += (target.x < actor.x ? -1 : 1) * deterministicRange(this, "intimidation-knockback-x", 26, 54, actor, target);
target.vy += deterministicRange(this, "intimidation-knockback-y", -18, 18, actor, target);
actor.adjustRelation(target, -0.08, 0.06, "intimidate");

View file

@ -1539,7 +1539,7 @@
handleClick(x, y) {
const tool = this.tool;
if (!isPlacementTool(tool) && !["delete", "area_delete", "copy", "poke", "pinch", "water_hose", "rope", "rod", "spring", "wire", "insulated_wire", "new", "bell"].includes(tool)) {
if (!isPlacementTool(tool) && !["delete", "area_delete", "copy", "poke", "pinch", "water_hose", "rope", "rod", "spring", "wire", "insulated_wire", "new", "acquired_tarinai", "bell"].includes(tool)) {
const editable = findEditableItemAt(this, x, y);
if (editable && openEditableItemEditor(editable, this)) {
global.TarinaiCommands.setSelection(this, null) || (this.selected = null);
@ -1629,7 +1629,28 @@
global.TarinaiAchievements?.recordManualTarinaiAdded?.({ world: this, tarinai: t, tool: "new" });
return;
}
if (tool === "acquired_tarinai") {
const spot = this.edgeSpawnPoint(x, y, 42, true);
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, generation: 1, entryTimer: 2.0, isAcquiredTarinai: true });
if (!t) { showToast(`\u305f\u308a\u306a\u3044\u500b\u4f53\u6570\u306f${this.tarinaiPopulationLimit}\u5339\u307e\u3067\u3067\u3059\u3002`); return; }
t.wanderAngle = spot.angle;
if (Math.random() < 0.5 && typeof t.enterPanic === "function") {
t.enterPanic({
target: null,
reason: "おちびになって混乱している",
fear: 1.25,
wake: true,
cause: "acquired_tarinai_spawn",
bubble: "!!",
bubbleCooldown: 0.75
});
}
global.TarinaiAchievements?.recordManualTarinaiAdded?.({ world: this, tarinai: t, tool: "acquired_tarinai" });
return;
}
if (tool === "copy") { this.useCopyToolAt?.(x, y); return; }
if (this.useToolAt?.(x, y)) return;

View file

@ -1,6 +1,6 @@
"use strict";
const APP_VERSION = "39.18.3";
const APP_VERSION = "39.18.18";
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
const v = `v=${APP_VERSION}`;
// Static runtime list synchronized with app_manifest.json for this release.
@ -19,6 +19,12 @@ const mediaAssetNames = [
"assets/objects/zunchi.webp",
"assets/objects/zunchi_02.webp",
"assets/sounds/achievement_unlock.mp3",
"assets/sounds/acquired_stress_001.wav",
"assets/sounds/acquired_stress_002.wav",
"assets/sounds/acquired_stress_003.wav",
"assets/sounds/acquired_stress_004.wav",
"assets/sounds/acquired_stress_005.wav",
"assets/sounds/acquired_stress_006.wav",
"assets/sounds/bicycle_bell.mp3",
"assets/sounds/firecracker_explosion.mp3",
"assets/sounds/major_damage_01.wav",
@ -36,6 +42,14 @@ const mediaAssetNames = [
"assets/sounds/voice_007_sunbath_pokapoka.wav",
"assets/sounds/voice_008_temperature_buruburu.wav",
"assets/sounds/voice_009_temperature_achui.wav",
"assets/sounds/intimidate_alt_001.wav",
"assets/sounds/ochibi_sleep_002.wav",
"assets/sounds/ochibi_sleep_003.wav",
"assets/sounds/ochibi_fight_hit_004.wav",
"assets/sounds/ochibi_panic_005.wav",
"assets/sounds/ochibi_electric_006.wav",
"assets/sounds/ochibi_hot_007.wav",
"assets/sounds/ochibi_stress_008.wav",
"assets/sprites/tarinai_01_smile.webp",
"assets/sprites/tarinai_02_angry.webp",
"assets/sprites/tarinai_03_teary.webp",
@ -75,9 +89,12 @@ const mediaAssetNames = [
"assets/sprites/tarinai_37_cold_01.webp",
"assets/sprites/tarinai_38_cold_02.webp",
"assets/sprites/tarinai_39_cold_03.webp",
"assets/sprites/tarinai_40_intimidate_alt.webp",
"assets/sprites/tarinai_acquired_overlay.png",
"assets/ui/apple-touch-icon.png",
"assets/ui/ecology_hp_stress.webp",
"assets/ui/favicon.png",
"assets/ui/tool_acquired_tarinai.webp",
"assets/ui/tool_ammo.webp",
"assets/ui/tool_ant_nest.webp",
"assets/ui/tool_ball.webp",
@ -173,10 +190,13 @@ self.addEventListener("fetch", event => {
if (isStaticAsset(request)) {
const responsePromise = caches.match(request).then(async cached => {
if (cached) return cached;
// Media are precached at their canonical URL. Match versioned and unversioned
// requests to the same entry so dynamically requested assets remain offline-safe.
const canonical = await caches.match(request, { ignoreSearch: true });
if (canonical) return canonical;
// Media are precached at their canonical URL. JS/CSS query versions must not
// resolve to an older cached file, otherwise UI fixes can remain stale.
const isCodeAsset = /\.(?:js|css)$/i.test(url.pathname);
if (!isCodeAsset) {
const canonical = await caches.match(request, { ignoreSearch: true });
if (canonical) return canonical;
}
const response = await fetch(request);
await putCacheSafely(request, response);
return response;