あ
This commit is contained in:
parent
8f9f5b5100
commit
f1c3af2b89
49 changed files with 989 additions and 310 deletions
|
|
@ -6,7 +6,7 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th
|
||||||
|
|
||||||
- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order.
|
- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order.
|
||||||
- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader.
|
- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader.
|
||||||
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.164`.
|
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.167`.
|
||||||
- Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render.
|
- Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render.
|
||||||
- Performance/display settings: `js/perf_profiler.js` owns manual visual settings and profiler buckets; display tiers affect rendering/visuals, not simulation-quality branches.
|
- Performance/display settings: `js/perf_profiler.js` owns manual visual settings and profiler buckets; display tiers affect rendering/visuals, not simulation-quality branches.
|
||||||
- Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`.
|
- Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,23 @@
|
||||||
|
tarinai_physics_tuning_v167
|
||||||
|
- Replaced the 8-sided circle template with a 24-sided, 20px-grid-aligned contour.
|
||||||
|
- Raised bounce-fence restitution from 1.08 to 2.40 and increased minimum/capped launch speeds.
|
||||||
|
- Made rope endpoint and particle constraints inextensible while retaining slack/flexibility.
|
||||||
|
- Set sticky-bomb blastScale to 0.68, matching the small firecracker.
|
||||||
|
|
||||||
|
tarinai_mechanical_collision_grid_limit_audit_v1
|
||||||
|
- Restored shared SAT geometry exports so collision-enabled rotators, reciprocators, and poison blocks physically interact with one another.
|
||||||
|
- Mechanical shape editors now snap all geometry to a coarse 20px lattice.
|
||||||
|
- Removed central colony-limit bypass options and added direct-insertion, capacity, save-roundtrip, and over-limit regression checks.
|
||||||
|
- Version: 39.15.167
|
||||||
|
|
||||||
|
tarinai_champion_limits_mechanical_defaults_v1
|
||||||
|
- Tarinai champions actively prioritize other champions as breeding partners, with stronger mate drive and shorter champion-pair cooldowns.
|
||||||
|
- Reciprocators now default to 90 degrees.
|
||||||
|
- Poison blocks now use a smaller default footprint.
|
||||||
|
- Colony panel now supports optional tarinai-population and object-count limits; blank/0 means unlimited.
|
||||||
|
- Colony limits are saved in snapshot/binary data.
|
||||||
|
- Version: 39.15.165
|
||||||
|
|
||||||
tarinai_plushie_fling_speed_v1
|
tarinai_plushie_fling_speed_v1
|
||||||
- Reduced plushie poke-fling velocity to one third.
|
- Reduced plushie poke-fling velocity to one third.
|
||||||
- Lowered plushie fling minimum-speed normalization to the same one-third scale.
|
- Lowered plushie fling minimum-speed normalization to the same one-third scale.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
|
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
|
||||||
"version": "39.15.164",
|
"version": "39.15.167",
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"app": "index.html",
|
"app": "index.html",
|
||||||
"runtime": "js/main.js",
|
"runtime": "js/main.js",
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
This document describes the playable features of `tarinai_`: what the player sees, what tools do, and how tarinai behave. It is intentionally player/design-facing, not an implementation map.
|
This document describes the playable features of `tarinai_`: what the player sees, what tools do, and how tarinai behave. It is intentionally player/design-facing, not an implementation map.
|
||||||
|
|
||||||
|
## v39.15.167 mechanical interaction and limit audit
|
||||||
|
|
||||||
|
- 回転体・往復体・毒ブロックは、双方の「当たり判定ON」時にSAT矩形判定で相互作用する。当たり判定OFFの機構は機構同士の衝突対象から除外される。
|
||||||
|
- 3種類の輪郭編集は20px間隔のグリッド格子点へスナップする。直線、自由描画、消去位置、テンプレート、再編集時の既存端点に同じ規則を適用する。
|
||||||
|
- コロニー上限は中央生成APIで常時適用され、通常実行時に上限を迂回するオプションは持たない。既存要素やセーブ復元要素は削除せず、その後の新規生成のみを抑止する。
|
||||||
|
|
||||||
|
## v39.15.165 behavior and colony controls
|
||||||
|
|
||||||
|
- たりない王は、繁殖可能な別のたりない王を広い範囲から優先して探し、通常より強い繁殖欲求と短い再繁殖待機時間を持つ。
|
||||||
|
- 往復体の新規配置時の標準方向は90度。
|
||||||
|
- 毒ブロックの新規配置時の標準輪郭は小型(半径44、輪郭104×40、太さ11)。
|
||||||
|
- コロニーパネルで、たりない個体数と生存オブジェクト総数の上限を設定できる。空欄または0は上限なし。既存要素は削除せず、新規追加だけを抑止する。
|
||||||
|
- 上限値はスナップショットとバイナリセーブに保存される。
|
||||||
|
|
||||||
## Game Overview
|
## Game Overview
|
||||||
|
|
||||||
`tarinai_` is an observation sandbox about small creatures called tarinai. The player places food, furniture, hazards, machines, and environment tools, then watches how individuals live, move, eat, sleep, panic, fight, reproduce, get sick, recover, and leave family records.
|
`tarinai_` is an observation sandbox about small creatures called tarinai. The player places food, furniture, hazards, machines, and environment tools, then watches how individuals live, move, eat, sleep, panic, fight, reproduce, get sick, recover, and leave family records.
|
||||||
|
|
@ -76,10 +90,10 @@ The core loop is:
|
||||||
|
|
||||||
### Physics
|
### Physics
|
||||||
|
|
||||||
- **ヒモ / rope**: connect two targets with a flexible link.
|
- **ヒモ / rope**: connect two targets with a flexible, inextensible link that may go slack but does not lengthen.
|
||||||
- **棒 / rod**: connect two targets with a rigid link.
|
- **棒 / rod**: connect two targets with a rigid link.
|
||||||
- **柵 / fence**: blocks tarinai movement; Q/E rotate by 45 degrees, Shift+Q/E by 5 degrees.
|
- **柵 / fence**: blocks tarinai movement; Q/E rotate by 45 degrees, Shift+Q/E by 5 degrees.
|
||||||
- **バウンス柵 / bounce fence**: fence that rebounds things that hit it.
|
- **バウンス柵 / bounce fence**: high-restitution fence that strongly launches things that hit it.
|
||||||
- **ゲート柵 / gate fence**: click to open or close.
|
- **ゲート柵 / gate fence**: click to open or close.
|
||||||
- **回転体 / rotator**: custom mechanical shape that rotates; click to edit shape and speed.
|
- **回転体 / rotator**: custom mechanical shape that rotates; click to edit shape and speed.
|
||||||
- **毒ブロック / poison block**: custom hazard block; touching it damages skin; click to edit shape.
|
- **毒ブロック / poison block**: custom hazard block; touching it damages skin; click to edit shape.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
||||||
|
|
||||||
## Runtime Shape
|
## Runtime Shape
|
||||||
|
|
||||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.164`.
|
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.167`.
|
||||||
- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots.
|
- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots.
|
||||||
- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases.
|
- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases.
|
||||||
- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`.
|
- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "39.15.164",
|
"version": "39.15.167",
|
||||||
"css": [
|
"css": [
|
||||||
"css/base.css",
|
"css/base.css",
|
||||||
"css/layout.css",
|
"css/layout.css",
|
||||||
|
|
|
||||||
|
|
@ -829,6 +829,27 @@
|
||||||
opacity: 0.52;
|
opacity: 0.52;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.colony-limit-control {
|
||||||
|
margin: 8px 0 4px;
|
||||||
|
padding: 10px 11px;
|
||||||
|
border: 1px solid rgba(118, 92, 61, 0.16);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 251, 244, 0.56);
|
||||||
|
}
|
||||||
|
.colony-limit-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
color: rgba(68, 55, 41, 0.84);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.colony-limit-head small { color: var(--muted); font-size: 10px; }
|
||||||
|
.colony-limit-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 8px; }
|
||||||
|
.colony-limit-grid label { display: grid; gap: 4px; min-width: 0; font-size: 11px; color: var(--muted); }
|
||||||
|
.colony-limit-grid input { width: 100%; min-width: 0; box-sizing: border-box; border: 1px solid rgba(100, 78, 52, 0.22); border-radius: 8px; padding: 6px 7px; background: rgba(255,255,255,0.78); color: var(--ink); font: inherit; }
|
||||||
|
.colony-limit-status { margin-top: 7px; color: rgba(68, 55, 41, 0.78); font-size: 10px; line-height: 1.35; }
|
||||||
|
|
||||||
|
|
||||||
/* v39.15.131: keep the folded tool card usable when other right-side cards are expanded. */
|
/* v39.15.131: keep the folded tool card usable when other right-side cards are expanded. */
|
||||||
.tool-card.collapsed {
|
.tool-card.collapsed {
|
||||||
|
|
|
||||||
348
index.html
348
index.html
|
|
@ -4,14 +4,14 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>たりない観察</title>
|
<title>たりない観察</title>
|
||||||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.15.164" />
|
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.15.167" />
|
||||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.15.164" />
|
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.15.167" />
|
||||||
<link rel="shortcut icon" href="favicon.ico?v=39.15.164" />
|
<link rel="shortcut icon" href="favicon.ico?v=39.15.167" />
|
||||||
<link rel="stylesheet" href="css/base.css?v=39.15.164" />
|
<link rel="stylesheet" href="css/base.css?v=39.15.167" />
|
||||||
<link rel="stylesheet" href="css/layout.css?v=39.15.164" />
|
<link rel="stylesheet" href="css/layout.css?v=39.15.167" />
|
||||||
<link rel="stylesheet" href="css/panel.css?v=39.15.164" />
|
<link rel="stylesheet" href="css/panel.css?v=39.15.167" />
|
||||||
<link rel="stylesheet" href="css/components.css?v=39.15.164" />
|
<link rel="stylesheet" href="css/components.css?v=39.15.167" />
|
||||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.164" />
|
<link rel="stylesheet" href="css/mobile.css?v=39.15.167" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||||
|
|
@ -107,6 +107,14 @@
|
||||||
<div class="temperature-control-head"><span>気温</span><strong id="temperatureValue">10℃</strong><label class="temperature-auto"><input id="temperatureAutoToggle" type="checkbox" checked> 自動</label></div>
|
<div class="temperature-control-head"><span>気温</span><strong id="temperatureValue">10℃</strong><label class="temperature-auto"><input id="temperatureAutoToggle" type="checkbox" checked> 自動</label></div>
|
||||||
<input id="temperatureSlider" class="temperature-slider" type="range" min="-100" max="120" step="1" value="10">
|
<input id="temperatureSlider" class="temperature-slider" type="range" min="-100" max="120" step="1" value="10">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="colony-limit-control" aria-label="コロニー上限設定">
|
||||||
|
<div class="colony-limit-head"><span>上限設定</span><small>空欄・0は上限なし</small></div>
|
||||||
|
<div class="colony-limit-grid">
|
||||||
|
<label><span>たりない個体数</span><input id="tarinaiPopulationLimitInput" type="number" min="0" step="1" inputmode="numeric" placeholder="上限なし"></label>
|
||||||
|
<label><span>オブジェクト総数</span><input id="objectLimitInput" type="number" min="0" step="1" inputmode="numeric" placeholder="上限なし"></label>
|
||||||
|
</div>
|
||||||
|
<div id="colonyLimitStatus" class="colony-limit-status">たりない 0 / ∞ オブジェクト 0 / ∞</div>
|
||||||
|
</div>
|
||||||
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">個体数</button><button data-chart="life">欲求</button><button data-chart="objects">環境</button></div>
|
<div id="colonyChartTabs" class="chart-tabs"><button data-chart="population" class="active">個体数</button><button data-chart="life">欲求</button><button data-chart="objects">環境</button></div>
|
||||||
<div id="colonyChartLegend" class="colony-chart-legend"></div>
|
<div id="colonyChartLegend" class="colony-chart-legend"></div>
|
||||||
<div id="colonyChart" class="colony-chart" aria-label="コロニーの推移グラフ" role="img"></div>
|
<div id="colonyChart" class="colony-chart" aria-label="コロニーの推移グラフ" role="img"></div>
|
||||||
|
|
@ -252,167 +260,167 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||||
<script src="js/version.js?v=39.15.164" defer></script>
|
<script src="js/version.js?v=39.15.167" defer></script>
|
||||||
<script src="js/event_bus.js?v=39.15.164" defer></script>
|
<script src="js/event_bus.js?v=39.15.167" defer></script>
|
||||||
<script src="js/domain_ids.js?v=39.15.164" defer></script>
|
<script src="js/domain_ids.js?v=39.15.167" defer></script>
|
||||||
<script src="js/registry_base.js?v=39.15.164" defer></script>
|
<script src="js/registry_base.js?v=39.15.167" defer></script>
|
||||||
<script src="js/disease_registry.js?v=39.15.164" defer></script>
|
<script src="js/disease_registry.js?v=39.15.167" defer></script>
|
||||||
<script src="js/sound_pack.js?v=39.15.164" defer></script>
|
<script src="js/sound_pack.js?v=39.15.167" defer></script>
|
||||||
<script src="js/deterministic_helpers.js?v=39.15.164" defer></script>
|
<script src="js/deterministic_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_tool_metadata.js?v=39.15.164" defer></script>
|
<script src="js/item_tool_metadata.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_tool_definitions.js?v=39.15.164" defer></script>
|
<script src="js/item_tool_definitions.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_visual_definitions.js?v=39.15.164" defer></script>
|
<script src="js/item_visual_definitions.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_food_definitions.js?v=39.15.164" defer></script>
|
<script src="js/item_food_definitions.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_effect_definitions.js?v=39.15.164" defer></script>
|
<script src="js/item_effect_definitions.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_registry.js?v=39.15.164" defer></script>
|
<script src="js/item_registry.js?v=39.15.167" defer></script>
|
||||||
<script src="js/data.js?v=39.15.164" defer></script>
|
<script src="js/data.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ground_types.js?v=39.15.164" defer></script>
|
<script src="js/ground_types.js?v=39.15.167" defer></script>
|
||||||
<script src="js/input_mode_manager.js?v=39.15.164" defer></script>
|
<script src="js/input_mode_manager.js?v=39.15.167" defer></script>
|
||||||
<script src="js/math.js?v=39.15.164" defer></script>
|
<script src="js/math.js?v=39.15.167" defer></script>
|
||||||
<script src="js/geometry_helpers.js?v=39.15.164" defer></script>
|
<script src="js/geometry_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/collision_footprint_system.js?v=39.15.164" defer></script>
|
<script src="js/collision_footprint_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/physics_helpers.js?v=39.15.164" defer></script>
|
<script src="js/physics_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/placement_preview_system.js?v=39.15.164" defer></script>
|
<script src="js/placement_preview_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/pin_attachment_system.js?v=39.15.164" defer></script>
|
<script src="js/pin_attachment_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/physics_shape_editor_system.js?v=39.15.164" defer></script>
|
<script src="js/physics_shape_editor_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/mechanical_system.js?v=39.15.164" defer></script>
|
<script src="js/mechanical_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/mechanical_shape_bridge.js?v=39.15.164" defer></script>
|
<script src="js/mechanical_shape_bridge.js?v=39.15.167" defer></script>
|
||||||
<script src="js/constraint_system.js?v=39.15.164" defer></script>
|
<script src="js/constraint_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/physics_world_system.js?v=39.15.164" defer></script>
|
<script src="js/physics_world_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/physics_projection_system.js?v=39.15.164" defer></script>
|
<script src="js/physics_projection_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/assets.js?v=39.15.164" defer></script>
|
<script src="js/assets.js?v=39.15.167" defer></script>
|
||||||
<script src="js/audio.js?v=39.15.164" defer></script>
|
<script src="js/audio.js?v=39.15.167" defer></script>
|
||||||
<script src="js/perf_profiler.js?v=39.15.164" defer></script>
|
<script src="js/perf_profiler.js?v=39.15.167" defer></script>
|
||||||
<script src="js/display_helpers.js?v=39.15.164" defer></script>
|
<script src="js/display_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/render.js?v=39.15.164" defer></script>
|
<script src="js/render.js?v=39.15.167" defer></script>
|
||||||
<script src="js/sim_core.js?v=39.15.164" defer></script>
|
<script src="js/sim_core.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_seed_factory.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_seed_factory.js?v=39.15.167" defer></script>
|
||||||
<script src="js/structures.js?v=39.15.164" defer></script>
|
<script src="js/structures.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_type_catalog.js?v=39.15.164" defer></script>
|
<script src="js/item_type_catalog.js?v=39.15.167" defer></script>
|
||||||
<script src="js/items.js?v=39.15.164" defer></script>
|
<script src="js/items.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_type_initializers.js?v=39.15.164" defer></script>
|
<script src="js/item_type_initializers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_support.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_support.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_update_policy.js?v=39.15.164" defer></script>
|
<script src="js/item_update_policy.js?v=39.15.167" defer></script>
|
||||||
<script src="js/fire_runtime_system.js?v=39.15.164" defer></script>
|
<script src="js/fire_runtime_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/robot_cleaner_system.js?v=39.15.164" defer></script>
|
<script src="js/robot_cleaner_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_dynamic_tool_system.js?v=39.15.164" defer></script>
|
<script src="js/item_dynamic_tool_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_dynamic_ball_system.js?v=39.15.164" defer></script>
|
<script src="js/item_dynamic_ball_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.164" defer></script>
|
<script src="js/item_dynamic_duplicator_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_dynamic_pin_system.js?v=39.15.164" defer></script>
|
<script src="js/item_dynamic_pin_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.164" defer></script>
|
<script src="js/item_dynamic_zunchi_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_dynamic_system.js?v=39.15.164" defer></script>
|
<script src="js/item_dynamic_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_decay_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_growth_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_step_frame.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_step_decay.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_step_growth.js?v=39.15.167" defer></script>
|
||||||
<script src="js/update_step_pipeline_runner.js?v=39.15.164" defer></script>
|
<script src="js/update_step_pipeline_runner.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.164" defer></script>
|
<script src="js/item_lifecycle_pipeline.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_runtime.js?v=39.15.164" defer></script>
|
<script src="js/item_runtime.js?v=39.15.167" defer></script>
|
||||||
<script src="js/structure_lifecycle.js?v=39.15.164" defer></script>
|
<script src="js/structure_lifecycle.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_render_helpers.js?v=39.15.164" defer></script>
|
<script src="js/item_render_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_render_runtime.js?v=39.15.164" defer></script>
|
<script src="js/item_render_runtime.js?v=39.15.167" defer></script>
|
||||||
<script src="js/burn_motion_util.js?v=39.15.164" defer></script>
|
<script src="js/burn_motion_util.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ants.js?v=39.15.164" defer></script>
|
<script src="js/ants.js?v=39.15.167" defer></script>
|
||||||
<script src="js/health.js?v=39.15.164" defer></script>
|
<script src="js/health.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai.js?v=39.15.164" defer></script>
|
<script src="js/tarinai.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_action_spec.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_action_spec.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_behavior_state.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_behavior_state.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_identity_social.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_identity_social.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_action_state.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_action_state.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_disease_nest.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_disease_nest.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_item_effects.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_item_effects.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_needs_core.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_needs_core.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_behavior_text.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_behavior_text.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_forced_behavior.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_forced_behavior.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_nest_sleep_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_nest_sleep_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_item_targeting.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_item_targeting.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_consumable_behavior.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_social_action_runtime.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_building_behavior.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_building_behavior.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_action_definitions.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_action_definitions.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_needs_items.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_needs_items.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_food_prototype_mixin.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_need_planner_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_need_planner_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_item_interaction_context.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_food_interaction_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_contact_item_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_contact_item_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_item_interaction_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_sunbath_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_sunbath_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_cursor_care_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_local_environment_system.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_local_environment_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_social_move_life.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_social_move_life.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_step_frame.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_step_frame.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_step_ai.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_step_ai.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_step_environment.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_step_environment.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_step_movement.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_step_movement.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_step_health.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_step_health.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_pipeline.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_pipeline.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_runtime.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_runtime.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_render.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_render.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world.js?v=39.15.164" defer></script>
|
<script src="js/world.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_view.js?v=39.15.164" defer></script>
|
<script src="js/world_view.js?v=39.15.167" defer></script>
|
||||||
<script src="js/family_graph.js?v=39.15.164" defer></script>
|
<script src="js/family_graph.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_reset_presets.js?v=39.15.164" defer></script>
|
<script src="js/world_reset_presets.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_family_social.js?v=39.15.164" defer></script>
|
<script src="js/world_family_social.js?v=39.15.167" defer></script>
|
||||||
<script src="js/impact_core_system.js?v=39.15.164" defer></script>
|
<script src="js/impact_core_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/impact_response_system.js?v=39.15.164" defer></script>
|
<script src="js/impact_response_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/collision_response_system.js?v=39.15.164" defer></script>
|
<script src="js/collision_response_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_combat_effects.js?v=39.15.164" defer></script>
|
<script src="js/world_combat_effects.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_environment.js?v=39.15.164" defer></script>
|
<script src="js/world_environment.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_temperature_system.js?v=39.15.164" defer></script>
|
<script src="js/world_temperature_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_pathfinding_system.js?v=39.15.164" defer></script>
|
<script src="js/world_pathfinding_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_grass_placement_system.js?v=39.15.164" defer></script>
|
<script src="js/world_grass_placement_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_spatial_budget.js?v=39.15.164" defer></script>
|
<script src="js/world_spatial_budget.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_ants_system.js?v=39.15.164" defer></script>
|
<script src="js/world_ants_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/weather_system.js?v=39.15.164" defer></script>
|
<script src="js/weather_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/item_update_scheduler.js?v=39.15.164" defer></script>
|
<script src="js/item_update_scheduler.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_runtime_helpers.js?v=39.15.164" defer></script>
|
<script src="js/simulation_runtime_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/tarinai_update_policy.js?v=39.15.164" defer></script>
|
<script src="js/tarinai_update_policy.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_environment_system.js?v=39.15.164" defer></script>
|
<script src="js/simulation_environment_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_item_ant_system.js?v=39.15.164" defer></script>
|
<script src="js/simulation_item_ant_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_effects_system.js?v=39.15.164" defer></script>
|
<script src="js/simulation_effects_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_creature_system.js?v=39.15.164" defer></script>
|
<script src="js/simulation_creature_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_maintenance_system.js?v=39.15.164" defer></script>
|
<script src="js/simulation_maintenance_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_ambient_system.js?v=39.15.164" defer></script>
|
<script src="js/simulation_ambient_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation_systems.js?v=39.15.164" defer></script>
|
<script src="js/simulation_systems.js?v=39.15.167" defer></script>
|
||||||
<script src="js/system_order.js?v=39.15.164" defer></script>
|
<script src="js/system_order.js?v=39.15.167" defer></script>
|
||||||
<script src="js/simulation.js?v=39.15.164" defer></script>
|
<script src="js/simulation.js?v=39.15.167" defer></script>
|
||||||
<script src="js/colony_situation_system.js?v=39.15.164" defer></script>
|
<script src="js/colony_situation_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_update.js?v=39.15.164" defer></script>
|
<script src="js/world_update.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_tool_actions.js?v=39.15.164" defer></script>
|
<script src="js/world_tool_actions.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_placement_log.js?v=39.15.164" defer></script>
|
<script src="js/world_placement_log.js?v=39.15.167" defer></script>
|
||||||
<script src="js/world_event_effects.js?v=39.15.164" defer></script>
|
<script src="js/world_event_effects.js?v=39.15.167" defer></script>
|
||||||
<script src="js/command_dispatcher.js?v=39.15.164" defer></script>
|
<script src="js/command_dispatcher.js?v=39.15.167" defer></script>
|
||||||
<script src="js/text_catalog.js?v=39.15.164" defer></script>
|
<script src="js/text_catalog.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui.js?v=39.15.164" defer></script>
|
<script src="js/ui.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_log.js?v=39.15.164" defer></script>
|
<script src="js/ui_log.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_helpers.js?v=39.15.164" defer></script>
|
<script src="js/ui_helpers.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_selected.js?v=39.15.164" defer></script>
|
<script src="js/ui_selected.js?v=39.15.167" defer></script>
|
||||||
<script src="js/save_schema.js?v=39.15.164" defer></script>
|
<script src="js/save_schema.js?v=39.15.167" defer></script>
|
||||||
<script src="js/snapshot_system.js?v=39.15.164" defer></script>
|
<script src="js/snapshot_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/restore_coordinator.js?v=39.15.164" defer></script>
|
<script src="js/restore_coordinator.js?v=39.15.167" defer></script>
|
||||||
<script src="js/history_system.js?v=39.15.164" defer></script>
|
<script src="js/history_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/save_codec.js?v=39.15.164" defer></script>
|
<script src="js/save_codec.js?v=39.15.167" defer></script>
|
||||||
<script src="js/save_storage.js?v=39.15.164" defer></script>
|
<script src="js/save_storage.js?v=39.15.167" defer></script>
|
||||||
<script src="js/save_system.js?v=39.15.164" defer></script>
|
<script src="js/save_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_tooltips.js?v=39.15.164" defer></script>
|
<script src="js/ui_tooltips.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_layout_dialogs.js?v=39.15.164" defer></script>
|
<script src="js/ui_layout_dialogs.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_ground.js?v=39.15.164" defer></script>
|
<script src="js/ui_ground.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_tools.js?v=39.15.164" defer></script>
|
<script src="js/ui_tools.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_input_shared.js?v=39.15.164" defer></script>
|
<script src="js/ui_input_shared.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_pointer_action_system.js?v=39.15.164" defer></script>
|
<script src="js/ui_pointer_action_system.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_input_touch.js?v=39.15.164" defer></script>
|
<script src="js/ui_input_touch.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_input_mouse.js?v=39.15.164" defer></script>
|
<script src="js/ui_input_mouse.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_bind.js?v=39.15.164" defer></script>
|
<script src="js/ui_bind.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_charts.js?v=39.15.164" defer></script>
|
<script src="js/ui_charts.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_family_data.js?v=39.15.164" defer></script>
|
<script src="js/ui_family_data.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_family_async.js?v=39.15.164" defer></script>
|
<script src="js/ui_family_async.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_family_layout.js?v=39.15.164" defer></script>
|
<script src="js/ui_family_layout.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_family_paths.js?v=39.15.164" defer></script>
|
<script src="js/ui_family_paths.js?v=39.15.167" defer></script>
|
||||||
<script src="js/ui_family_render.js?v=39.15.164" defer></script>
|
<script src="js/ui_family_render.js?v=39.15.167" defer></script>
|
||||||
<script src="js/main.js?v=39.15.164" defer></script>
|
<script src="js/main.js?v=39.15.167" defer></script>
|
||||||
<script src="js/debug_tools.js?v=39.15.164" defer></script>
|
<script src="js/debug_tools.js?v=39.15.167" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -414,7 +414,7 @@ class AntActor {
|
||||||
audio.antDie?.();
|
audio.antDie?.();
|
||||||
const corpse = new Item("ant_corpse", this.x, this.y);
|
const corpse = new Item("ant_corpse", this.x, this.y);
|
||||||
corpse.amount = 24;
|
corpse.amount = 24;
|
||||||
this.world.addItem?.(corpse, "ant-corpse") || this.world.items.push(corpse);
|
this.world.addItem?.(corpse, "ant-corpse");
|
||||||
this.world.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 10, life: 0.44, color: "rgba(82,61,42,0.28)" }));
|
this.world.effects?.push(new Effect("zunchi_miasma", this.x, this.y - 4, { size: 10, life: 0.44, color: "rgba(82,61,42,0.28)" }));
|
||||||
if (home) syncAntNestCount(this.world, home);
|
if (home) syncAntNestCount(this.world, home);
|
||||||
this.world.ensureItemBuckets?.("ant-corpse");
|
this.world.ensureItemBuckets?.("ant-corpse");
|
||||||
|
|
@ -439,8 +439,9 @@ class AntActor {
|
||||||
base.antCount = ANT_NEST_START_COUNT;
|
base.antCount = ANT_NEST_START_COUNT;
|
||||||
base.antWorkers = buildAntWorkerPool(ANT_NEST_START_COUNT);
|
base.antWorkers = buildAntWorkerPool(ANT_NEST_START_COUNT);
|
||||||
if (!force && this.world.placementBlocked?.(base)) return null;
|
if (!force && this.world.placementBlocked?.(base)) return null;
|
||||||
|
const addedNest = this.world.addItem?.(base, "ant-nest-built") || null;
|
||||||
|
if (!addedNest) return null;
|
||||||
audio.antNest?.();
|
audio.antNest?.();
|
||||||
this.world.addItem?.(base, "ant-nest-built") || this.world.items.push(base);
|
|
||||||
this.world.ensureSpatial?.("ant-nest-built");
|
this.world.ensureSpatial?.("ant-nest-built");
|
||||||
this.world.drawListDirty = true;
|
this.world.drawListDirty = true;
|
||||||
this.world.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u30a2\u30ea\u306e\u5de3\u3092\u4f5c\u3063\u305f\u3002", "event");
|
this.world.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u30a2\u30ea\u306e\u5de3\u3092\u4f5c\u3063\u305f\u3002", "event");
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
global.TarinaiCollisionFootprints = Object.freeze({
|
global.TarinaiCollisionFootprints = Object.freeze({
|
||||||
|
// Mechanical-body narrowphase consumes the same SAT primitives as
|
||||||
|
// placement/hit testing. Keep these delegates public so collision-enabled
|
||||||
|
// rotators, reciprocators, and poison blocks can physically interact.
|
||||||
|
rectLocalPoint,
|
||||||
|
rectWorldVector,
|
||||||
|
rectCorners,
|
||||||
|
rectAxes,
|
||||||
|
projectPoints,
|
||||||
|
aabbOverlap,
|
||||||
|
rectOverlapInfo,
|
||||||
|
rectsOverlap,
|
||||||
rectCircleOverlap,
|
rectCircleOverlap,
|
||||||
isSoftPlacementIgnoredType,
|
isSoftPlacementIgnoredType,
|
||||||
itemFootprintRadius,
|
itemFootprintRadius,
|
||||||
|
|
|
||||||
|
|
@ -275,23 +275,36 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function resolveTarinaiHighSpeedCollisions(worldRef, dt) {
|
function highSpeedThreshold() {
|
||||||
|
return PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTarinaiHighSpeedCollisions(worldRef, dt, opts = {}) {
|
||||||
if (!worldRef) return undefined;
|
if (!worldRef) return undefined;
|
||||||
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
const threshold = Math.max(1, Number(opts.threshold || PHYSICAL_DAMAGE_SPEED_THRESHOLD) || PHYSICAL_DAMAGE_SPEED_THRESHOLD);
|
||||||
const now = worldRef.time || 0;
|
const now = worldRef.time || 0;
|
||||||
if (!worldRef.tarinaiCollisionMemo) worldRef.tarinaiCollisionMemo = new Map();
|
if (!worldRef.tarinaiCollisionMemo) worldRef.tarinaiCollisionMemo = new Map();
|
||||||
const liveSource = worldRef.tarinai || [];
|
const providedFastSources = Array.isArray(opts.fastSources);
|
||||||
const live = [];
|
const liveSource = providedFastSources ? opts.fastSources : (worldRef.tarinai || []);
|
||||||
|
const fastSources = [];
|
||||||
for (const t of liveSource) {
|
for (const t of liveSource) {
|
||||||
if (t && !t.dead && !worldRef.isTarinaiHiddenInNestBox(t)) live.push(t);
|
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue;
|
||||||
|
if (Math.hypot(t.vx || 0, t.vy || 0) >= threshold) fastSources.push(t);
|
||||||
|
}
|
||||||
|
if (!fastSources.length) return;
|
||||||
|
const live = providedFastSources ? null : [];
|
||||||
|
if (!providedFastSources) {
|
||||||
|
for (const t of worldRef.tarinai || []) {
|
||||||
|
if (t && !t.dead && !worldRef.isTarinaiHiddenInNestBox(t)) live.push(t);
|
||||||
|
}
|
||||||
|
if (live.length < 2) return;
|
||||||
|
live.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
||||||
}
|
}
|
||||||
if (live.length < 2) return;
|
|
||||||
live.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
|
||||||
const candidates = [];
|
const candidates = [];
|
||||||
const maxCandidates = Math.max(24, Math.min(320, live.length * 6));
|
const maxCandidates = Math.max(24, Math.min(320, fastSources.length * 12));
|
||||||
const seenPairs = new Set();
|
const seenPairs = new Set();
|
||||||
for (let i = 0; i < live.length; i += 1) {
|
for (let i = 0; i < fastSources.length; i += 1) {
|
||||||
const a = live[i];
|
const a = fastSources[i];
|
||||||
const avx = a.vx || 0;
|
const avx = a.vx || 0;
|
||||||
const avy = a.vy || 0;
|
const avy = a.vy || 0;
|
||||||
const aSpeed = Math.hypot(avx, avy);
|
const aSpeed = Math.hypot(avx, avy);
|
||||||
|
|
@ -306,7 +319,7 @@
|
||||||
const queryX = ax0 + asx * 0.5;
|
const queryX = ax0 + asx * 0.5;
|
||||||
const queryY = ay0 + asy * 0.5;
|
const queryY = ay0 + asy * 0.5;
|
||||||
const queryRadius = Math.max(140, (a.radius || 22) * 1.6 + moveDist + threshold * Math.max(0.016, Number(dt || 0.016) || 0.016) + 96);
|
const queryRadius = Math.max(140, (a.radius || 22) * 1.6 + moveDist + threshold * Math.max(0.016, Number(dt || 0.016) || 0.016) + 96);
|
||||||
const nearby = typeof worldRef.nearbyTarinai === "function" ? Array.from(worldRef.nearbyTarinai(queryX, queryY, queryRadius, true) || []) : live;
|
const nearby = typeof worldRef.nearbyTarinai === "function" ? (worldRef.nearbyTarinai(queryX, queryY, queryRadius, true) || []) : (live || worldRef.tarinai || []);
|
||||||
for (const b of nearby) {
|
for (const b of nearby) {
|
||||||
if (!b || b === a || b.dead || worldRef.isTarinaiHiddenInNestBox(b)) continue;
|
if (!b || b === a || b.dead || worldRef.isTarinaiHiddenInNestBox(b)) continue;
|
||||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||||
|
|
@ -410,6 +423,7 @@
|
||||||
|
|
||||||
|
|
||||||
global.TarinaiCollisionResponseSystem = Object.freeze({
|
global.TarinaiCollisionResponseSystem = Object.freeze({
|
||||||
|
highSpeedThreshold,
|
||||||
resolveBallBallCollisions,
|
resolveBallBallCollisions,
|
||||||
resolveBallInteractions,
|
resolveBallInteractions,
|
||||||
resolveTarinaiHighSpeedCollisions,
|
resolveTarinaiHighSpeedCollisions,
|
||||||
|
|
|
||||||
|
|
@ -446,11 +446,11 @@
|
||||||
const ib = Number.isFinite(mb) ? 1 / Math.max(0.1, mb) : 0;
|
const ib = Number.isFinite(mb) ? 1 / Math.max(0.1, mb) : 0;
|
||||||
const sum = ia + ib || 1;
|
const sum = ia + ib || 1;
|
||||||
const step = Math.max(0.012, Math.min(0.05, Number(dt || 0.016) || 0.016));
|
const step = Math.max(0.012, Math.min(0.05, Number(dt || 0.016) || 0.016));
|
||||||
const compliance = mode === "rod" ? 0.085 / (step * 60) : 0.46 / (step * 60);
|
const compliance = mode === "rod" ? 0.085 / (step * 60) : 0;
|
||||||
const target = delta / (1 + compliance);
|
const target = delta / (1 + compliance);
|
||||||
const cap = mode === "rod"
|
const cap = mode === "rod"
|
||||||
? Math.max(6.0, Math.min(46, length * maxFraction))
|
? Math.max(6.0, Math.min(46, length * maxFraction))
|
||||||
: Math.max(1.5, length * maxFraction);
|
: Math.max(12, length * maxFraction);
|
||||||
const corr = clamp(target, -cap, cap);
|
const corr = clamp(target, -cap, cap);
|
||||||
let changed = false;
|
let changed = false;
|
||||||
changed = moveEndpointObject(pa.obj, nx * corr * (ia / sum), ny * corr * (ia / sum), ep(item, 0), dt, strength, 0, mode) || changed;
|
changed = moveEndpointObject(pa.obj, nx * corr * (ia / sum), ny * corr * (ia / sum), ep(item, 0), dt, strength, 0, mode) || changed;
|
||||||
|
|
@ -459,7 +459,15 @@
|
||||||
};
|
};
|
||||||
let changed = false;
|
let changed = false;
|
||||||
if (mode === "rope") {
|
if (mode === "rope") {
|
||||||
changed = solveOnce(a, b, 0.58, 0.044) || changed;
|
// A rope may go slack, but must not become longer than its recorded length.
|
||||||
|
// Re-read anchors between projections because the endpoint objects move.
|
||||||
|
const iterations = 8;
|
||||||
|
for (let i = 0; i < iterations; i += 1) {
|
||||||
|
const aa = endpointWorld(ep(item, 0), item.world || null) || a;
|
||||||
|
const bb = endpointWorld(ep(item, 1), item.world || null) || b;
|
||||||
|
if (distXY(aa.x, aa.y, bb.x, bb.y) <= length + 0.25) break;
|
||||||
|
changed = solveOnce(aa, bb, 1.0, 0.35) || changed;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const iterations = 5;
|
const iterations = 5;
|
||||||
for (let i = 0; i < iterations; i += 1) {
|
for (let i = 0; i < iterations; i += 1) {
|
||||||
|
|
@ -738,6 +746,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let iter = 0; iter < 4; iter += 1) {
|
||||||
|
ps[0].x = a.x; ps[0].y = a.y;
|
||||||
|
ps[ps.length - 1].x = b.x; ps[ps.length - 1].y = b.y;
|
||||||
|
for (let i = 0; i < ps.length - 1; i += 1) {
|
||||||
|
const p1 = ps[i], p2 = ps[i + 1];
|
||||||
|
const dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||||
|
const d = Math.max(0.001, Math.hypot(dx, dy));
|
||||||
|
if (d <= rest + 0.02) continue;
|
||||||
|
const corrX = dx * ((d - rest) / d);
|
||||||
|
const corrY = dy * ((d - rest) / d);
|
||||||
|
if (i === 0) { p2.x -= corrX; p2.y -= corrY; }
|
||||||
|
else if (i + 1 === ps.length - 1) { p1.x += corrX; p1.y += corrY; }
|
||||||
|
else { p1.x += corrX * 0.5; p1.y += corrY * 0.5; p2.x -= corrX * 0.5; p2.y -= corrY * 0.5; }
|
||||||
|
}
|
||||||
|
}
|
||||||
ps[0].x = a.x; ps[0].y = a.y;
|
ps[0].x = a.x; ps[0].y = a.y;
|
||||||
ps[ps.length - 1].x = b.x; ps[ps.length - 1].y = b.y;
|
ps[ps.length - 1].x = b.x; ps[ps.length - 1].y = b.y;
|
||||||
const mid = ps[Math.floor(ps.length * 0.5)] || ps[0];
|
const mid = ps[Math.floor(ps.length * 0.5)] || ps[0];
|
||||||
|
|
|
||||||
|
|
@ -549,9 +549,10 @@ function updateFirecracker(item, dt, worldRef) {
|
||||||
fire.flameLife = Math.max(1.2, Number(opts.life || fire.flameLife || 7.5));
|
fire.flameLife = Math.max(1.2, Number(opts.life || fire.flameLife || 7.5));
|
||||||
fire.vx = 0;
|
fire.vx = 0;
|
||||||
fire.vy = 0;
|
fire.vy = 0;
|
||||||
worldRef.addItem?.(fire, "spawn-fire", { terrain: false });
|
const addedFire = worldRef.addItem?.(fire, "spawn-fire", { terrain: false }) || null;
|
||||||
|
if (!addedFire) return null;
|
||||||
worldRef.drawListDirty = true;
|
worldRef.drawListDirty = true;
|
||||||
return fire;
|
return addedFire;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scatterFireFrom(item, worldRef) {
|
function scatterFireFrom(item, worldRef) {
|
||||||
|
|
|
||||||
|
|
@ -516,8 +516,9 @@
|
||||||
if (!Number.isFinite(Number(drop.vx))) drop.vx = vx;
|
if (!Number.isFinite(Number(drop.vx))) drop.vx = vx;
|
||||||
if (!Number.isFinite(Number(drop.vy))) drop.vy = vy;
|
if (!Number.isFinite(Number(drop.vy))) drop.vy = vy;
|
||||||
if (!Number.isFinite(Number(drop.splashFlightTimer))) drop.splashFlightTimer = flight;
|
if (!Number.isFinite(Number(drop.splashFlightTimer))) drop.splashFlightTimer = flight;
|
||||||
if (worldRef.addItem?.(drop, "water-balloon-splash", { terrain: false })) spawned += 1;
|
const addedDrop = worldRef.addItem?.(drop, "water-balloon-splash", { terrain: false }) || null;
|
||||||
else { worldRef.items?.push?.(drop); spawned += 1; }
|
if (!addedDrop) continue;
|
||||||
|
spawned += 1;
|
||||||
if (i < 8) {
|
if (i < 8) {
|
||||||
worldRef.effects?.push(new Effect("fight", drop.x, drop.y, {
|
worldRef.effects?.push(new Effect("fight", drop.x, drop.y, {
|
||||||
size: deterministicRange(worldRef || balloon, "water-balloon-drop-spark-size", 3.5, 7.0, balloon, i),
|
size: deterministicRange(worldRef || balloon, "water-balloon-drop-spark-size", 3.5, 7.0, balloon, i),
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
|
|
||||||
const STICKY_BOMB_PANIC_TEXT = "粘着ボムが付いてパニックになっている。";
|
const STICKY_BOMB_PANIC_TEXT = "粘着ボムが付いてパニックになっている。";
|
||||||
const STICKY_BOMB_FUSE_SECONDS = 12.0;
|
const STICKY_BOMB_FUSE_SECONDS = 12.0;
|
||||||
|
const STICKY_BOMB_BLAST_SCALE = 0.68; // identical to TOOL_SIZE_SCALES.small
|
||||||
|
|
||||||
function tarinaiById(worldRef, id = "") {
|
function tarinaiById(worldRef, id = "") {
|
||||||
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === id) || null;
|
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === id) || null;
|
||||||
|
|
@ -96,7 +97,7 @@
|
||||||
if (!item || item.dead) return false;
|
if (!item || item.dead) return false;
|
||||||
item.stickyBombCarrierId = "";
|
item.stickyBombCarrierId = "";
|
||||||
item.stickyBombTransferCooldown = 0;
|
item.stickyBombTransferCooldown = 0;
|
||||||
item.blastScale = Number(item.blastScale || 1) || 1;
|
item.blastScale = STICKY_BOMB_BLAST_SCALE;
|
||||||
if (worldRef?.explodeFirecracker) {
|
if (worldRef?.explodeFirecracker) {
|
||||||
return Boolean(worldRef.explodeFirecracker(item));
|
return Boolean(worldRef.explodeFirecracker(item));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,12 +92,7 @@ function duplicatorApplyStoredPin(duplicator, worldRef) {
|
||||||
pin.noStickTargetId = "";
|
pin.noStickTargetId = "";
|
||||||
pin.noStickTargetUntil = 0;
|
pin.noStickTargetUntil = 0;
|
||||||
const added = worldRef?.addItem?.(pin, "duplicator-pin", { terrain: false }) || null;
|
const added = worldRef?.addItem?.(pin, "duplicator-pin", { terrain: false }) || null;
|
||||||
if (!added && worldRef?.items) {
|
if (!added) return false;
|
||||||
pin.world = worldRef;
|
|
||||||
worldRef.items.push(pin);
|
|
||||||
worldRef.markItemBucketsDirty?.("duplicator-pin");
|
|
||||||
worldRef.markSpatialDirty?.("duplicator-pin");
|
|
||||||
}
|
|
||||||
const ok = global.TarinaiItemDynamicPinSystem.attach(pin, t, worldRef, distXY(dish.x, dish.y, t.x, t.y));
|
const ok = global.TarinaiItemDynamicPinSystem.attach(pin, t, worldRef, distXY(dish.x, dish.y, t.x, t.y));
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
pin.amount = 0;
|
pin.amount = 0;
|
||||||
|
|
|
||||||
|
|
@ -1152,8 +1152,8 @@ draw(ctx, t, lighting = null) {
|
||||||
const angle = itemAngleFor(this);
|
const angle = itemAngleFor(this);
|
||||||
const pb = window.TarinaiPhysicsBodySystem;
|
const pb = window.TarinaiPhysicsBodySystem;
|
||||||
const enabled = pb?.scalar?.(this, "solid", true) !== false;
|
const enabled = pb?.scalar?.(this, "solid", true) !== false;
|
||||||
const segments = (pb?.segments?.(this) || [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]]);
|
const segments = (pb?.segments?.(this) || [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]]);
|
||||||
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 14) || 14) || 14));
|
const thick = Math.max(4, Math.min(34, Number(pb?.scalar?.(this, "thickness", 11) || 11) || 11));
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.rotate(angle);
|
ctx.rotate(angle);
|
||||||
ctx.lineCap = "round";
|
ctx.lineCap = "round";
|
||||||
|
|
|
||||||
|
|
@ -58,11 +58,11 @@ const TOOL_DEFINITIONS = Object.freeze({
|
||||||
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
fence_v: { id: "fence_v", itemType: "fence_v", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_v.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||||
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
fence_h: { id: "fence_h", itemType: "fence_h", label: "\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, icon: "assets/ui/tool_fence_h.webp", collisionShape: "oriented_rect", tooltip: "\u305f\u308a\u306a\u3044\u3092\u901a\u305b\u3093\u307c\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||||
glass_wall: { id: "glass_wall", itemType: "glass_wall", label: "\u30ac\u30e9\u30b9\u58c1", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "\u25ad", collisionShape: "oriented_rect", tooltip: "\u900f\u660e\u306a\u58c1\u3002\u305f\u308a\u306a\u3044\u306f\u901a\u308c\u306a\u3044\u304c\u3001\u898b\u3048\u3066\u3044\u308b\u5bfe\u8c61\u3078\u306f\u8fc2\u56de\u305b\u305a\u306b\u5411\u304b\u3046\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
glass_wall: { id: "glass_wall", itemType: "glass_wall", label: "\u30ac\u30e9\u30b9\u58c1", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "\u25ad", collisionShape: "oriented_rect", tooltip: "\u900f\u660e\u306a\u58c1\u3002\u305f\u308a\u306a\u3044\u306f\u901a\u308c\u306a\u3044\u304c\u3001\u898b\u3048\u3066\u3044\u308b\u5bfe\u8c61\u3078\u306f\u8fc2\u56de\u305b\u305a\u306b\u5411\u304b\u3046\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||||
bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
bounce_fence: { id: "bounce_fence", itemType: "bounce_fence", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u5f37\u70c8\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||||
bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u901f\u3081\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
bounce_fence_v: { id: "bounce_fence_v", itemType: "bounce_fence_v", label: "\u30d0\u30a6\u30f3\u30b9\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u4f55\u304b\u304c\u5f53\u305f\u308b\u3068\u5f37\u70c8\u306b\u8df3\u306d\u8fd4\u3059\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||||
gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
gate_fence: { id: "gate_fence", itemType: "gate_fence", label: "\u30b2\u30fc\u30c8\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, collisionShape: "oriented_rect", tooltip: "\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u958b\u9589\u3059\u308b\u67f5\u3002Q/E\u306745\u5ea6\u3001Shift+Q/E\u30675\u5ea6\u305a\u3064\u5411\u304d\u3092\u5909\u3048\u308b\u3002" },
|
||||||
rotator: { id: "rotator", itemType: "rotator", label: "\u56de\u8ee2\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21bb", collisionShape: "custom_rotator", tooltip: "\u56de\u8ee2\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
rotator: { id: "rotator", itemType: "rotator", label: "\u56de\u8ee2\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21bb", collisionShape: "custom_rotator", tooltip: "\u56de\u8ee2\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||||
poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" },
|
poison_block: { id: "poison_block", itemType: "poison_block", label: "\u6bd2\u30d6\u30ed\u30c3\u30af", placeable: true, scalable: false, radius: 44, amount: 999, iconText: "\u2620", collisionShape: "custom_poison_block", tooltip: "\u5f53\u305f\u308b\u3068\u76ae\u819a\u304c\u75fa\u308c\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u5f62\u72b6\u304c\u5909\u66f4\u3067\u304d\u308b\u3002" },
|
||||||
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
reciprocator: { id: "reciprocator", itemType: "reciprocator", label: "\u5f80\u5fa9\u4f53", placeable: true, scalable: false, radius: 64, amount: 999, iconText: "\u21c4", collisionShape: "custom_reciprocator", tooltip: "\u5f80\u5fa9\u3059\u308b\u6a5f\u68b0\u3002\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u5f62\u72b6\u3084\u901f\u5ea6\u3092\u8a2d\u5b9a\u3067\u304d\u308b\u3002" },
|
||||||
water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" },
|
water: { id: "water", itemType: "water", label: "\u6c34\u6ef4", placeable: true, scalable: true, radius: 12, amount: 64, iconText: "\ud83d\udca7", tooltip: "\u96e8\u306e\u65e5\u306b\u3082\u73fe\u308c\u308b\u6c34\u6ef4\u3002\u305f\u308a\u306a\u3044\u304c\u98f2\u3081\u308b\u3002\u8907\u88fd\u6a5f\u306b\u3082\u30bb\u30c3\u30c8\u3067\u304d\u308b\u3002" },
|
||||||
trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
trace: { id: "trace", itemType: "trace", label: "\u6b7b\u9ab8", placeable: false, scalable: false, radius: 16, amount: 220, simulationOnly: true },
|
||||||
|
|
@ -91,7 +91,7 @@ function isFenceItemType(type = "") { return FENCE_ITEM_TYPES.has(String(type ||
|
||||||
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
|
function isRotatableItemType(type = "") { return ROTATABLE_ITEM_TYPES.has(String(type || "")); }
|
||||||
function defaultItemAngle(type = "") {
|
function defaultItemAngle(type = "") {
|
||||||
const t = String(type || "");
|
const t = String(type || "");
|
||||||
if (t === "fence_v" || t === "bounce_fence_v") return Math.PI / 2;
|
if (t === "fence_v" || t === "bounce_fence_v" || t === "reciprocator") return Math.PI / 2;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
function normalizedItemAngle(angle = 0, fallback = 0) {
|
function normalizedItemAngle(angle = 0, fallback = 0) {
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ function initializeItemTypeState(item, type, x, y) {
|
||||||
}
|
}
|
||||||
if (type === "sticky_bomb") {
|
if (type === "sticky_bomb") {
|
||||||
item.fuseTimer = 12.0;
|
item.fuseTimer = 12.0;
|
||||||
|
item.blastScale = 0.68;
|
||||||
item.fuseMax = 12.0;
|
item.fuseMax = 12.0;
|
||||||
item.vx = rand(-8, 8);
|
item.vx = rand(-8, 8);
|
||||||
item.vy = rand(-8, 8);
|
item.vy = rand(-8, 8);
|
||||||
|
|
@ -170,7 +171,7 @@ function initializeItemTypeState(item, type, x, y) {
|
||||||
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
||||||
motor: { powered: false, speed: 0, direction: 1 },
|
motor: { powered: false, speed: 0, direction: 1 },
|
||||||
rail: null,
|
rail: null,
|
||||||
shape: { model: "segments", thickness: 14, segments: [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]], version: 0 },
|
shape: { model: "segments", thickness: 11, segments: [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]], version: 0 },
|
||||||
collision: { solid: true, hazard: true },
|
collision: { solid: true, hazard: true },
|
||||||
hazard: { kind: "poison", damage: 7 },
|
hazard: { kind: "poison", damage: 7 },
|
||||||
sleep: { awakeUntil: 0 },
|
sleep: { awakeUntil: 0 },
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
if (out.length >= 128) break;
|
if (out.length >= 128) break;
|
||||||
}
|
}
|
||||||
if (!out.length) {
|
if (!out.length) {
|
||||||
if (item?.type === "poison_block") out.push([-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]);
|
if (item?.type === "poison_block") out.push([-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]);
|
||||||
else out.push(item?.type === "rotator" ? [-78, 0, 78, 0] : [-78, 0, 78, 0], ...(item?.type === "rotator" ? [[0, -52, 0, 52]] : []));
|
else out.push(item?.type === "rotator" ? [-78, 0, 78, 0] : [-78, 0, 78, 0], ...(item?.type === "rotator" ? [[0, -52, 0, 52]] : []));
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|
@ -236,7 +236,7 @@
|
||||||
const cache = geomCache(item);
|
const cache = geomCache(item);
|
||||||
const shape = bodyShape(item);
|
const shape = bodyShape(item);
|
||||||
const raw = Array.isArray(shape?.segments) ? shape.segments : [];
|
const raw = Array.isArray(shape?.segments) ? shape.segments : [];
|
||||||
const thickKey = Number.isFinite(Number(shape?.thickness)) ? shape.thickness : ps(item, "thickness", item?.type === "poison_block" ? 14 : 12);
|
const thickKey = Number.isFinite(Number(shape?.thickness)) ? shape.thickness : ps(item, "thickness", item?.type === "poison_block" ? 11 : 12);
|
||||||
const key = `${item?.type || ""}|${shapeVersion(item)}|${raw.length}|${rawSegmentSignature(raw)}|${thickKey || ""}`;
|
const key = `${item?.type || ""}|${shapeVersion(item)}|${raw.length}|${rawSegmentSignature(raw)}|${thickKey || ""}`;
|
||||||
if (cache?.localKey === key && cache.localSegments) return cache.localSegments;
|
if (cache?.localKey === key && cache.localSegments) return cache.localSegments;
|
||||||
const normalized = normalizeRawSegments(item);
|
const normalized = normalizeRawSegments(item);
|
||||||
|
|
@ -248,7 +248,7 @@
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function thickness(item) { return Math.max(4, Math.min(34, num(bodyShape(item)?.thickness, ps(item, "thickness", item?.type === "poison_block" ? 14 : 12)))); }
|
function thickness(item) { return Math.max(4, Math.min(34, num(bodyShape(item)?.thickness, ps(item, "thickness", item?.type === "poison_block" ? 11 : 12)))); }
|
||||||
|
|
||||||
function extent(item) {
|
function extent(item) {
|
||||||
const cache = geomCache(item);
|
const cache = geomCache(item);
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function bounceCircleOffRect(obj, rect, radius, restitution = 1.08, worldRef = null, opts = {}) {
|
function bounceCircleOffRect(obj, rect, radius, restitution = 2.40, worldRef = null, opts = {}) {
|
||||||
if (!obj || !rect) return false;
|
if (!obj || !rect) return false;
|
||||||
if (rect.oriented) return circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts);
|
if (rect.oriented) return circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts);
|
||||||
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
|
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
|
||||||
|
|
@ -194,9 +194,9 @@
|
||||||
const rects = worldRef.nearbySolidObstacleRects(item.x, item.y, searchRadius, { include: it => it?.type === "bounce_fence" || it?.type === "bounce_fence_v", exclude: item, maxChecks: 16 }) || [];
|
const rects = worldRef.nearbySolidObstacleRects(item.x, item.y, searchRadius, { include: it => it?.type === "bounce_fence" || it?.type === "bounce_fence_v", exclude: item, maxChecks: 16 }) || [];
|
||||||
for (const rect of rects) {
|
for (const rect of rects) {
|
||||||
if (!rect?.bounce) continue;
|
if (!rect?.bounce) continue;
|
||||||
if (bounceCircleOffRect(item, rect, radius, Number(rect.restitution || 1.08), worldRef, opts)) {
|
if (bounceCircleOffRect(item, rect, radius, Number(rect.restitution || 2.40), worldRef, opts)) {
|
||||||
bounced = true;
|
bounced = true;
|
||||||
const maxSpeed = opts.bounceFenceMaxSpeed ?? 760;
|
const maxSpeed = opts.bounceFenceMaxSpeed ?? 1180;
|
||||||
item.vx = clamp(item.vx || 0, -maxSpeed, maxSpeed);
|
item.vx = clamp(item.vx || 0, -maxSpeed, maxSpeed);
|
||||||
item.vy = clamp(item.vy || 0, -maxSpeed, maxSpeed);
|
item.vy = clamp(item.vy || 0, -maxSpeed, maxSpeed);
|
||||||
const now = worldRef.time || 0;
|
const now = worldRef.time || 0;
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,33 @@
|
||||||
undo: "\u21B6 \u4E00\u3064\u623B\u3059",
|
undo: "\u21B6 \u4E00\u3064\u623B\u3059",
|
||||||
clear: "\uD83D\uDDD1 \u5168\u6D88\u3057",
|
clear: "\uD83D\uDDD1 \u5168\u6D88\u3057",
|
||||||
});
|
});
|
||||||
|
const DEFAULT_GRID_SIZE = 20;
|
||||||
|
function normalizedGridSize(value = DEFAULT_GRID_SIZE) {
|
||||||
|
return Math.max(10, Math.min(80, Math.round(Number(value || DEFAULT_GRID_SIZE) || DEFAULT_GRID_SIZE)));
|
||||||
|
}
|
||||||
|
function snapValue(value, gridSize = DEFAULT_GRID_SIZE) {
|
||||||
|
const grid = normalizedGridSize(gridSize);
|
||||||
|
return Math.round((Number(value) || 0) / grid) * grid;
|
||||||
|
}
|
||||||
|
function snapPoint(point, gridSize = DEFAULT_GRID_SIZE) {
|
||||||
|
return { x: snapValue(point?.x, gridSize), y: snapValue(point?.y, gridSize) };
|
||||||
|
}
|
||||||
function templateSegments(name) {
|
function templateSegments(name) {
|
||||||
if (name === "bar") return [[-130, 0, 130, 0]];
|
if (name === "bar") return [[-120, 0, 120, 0]];
|
||||||
if (name === "cross") return [[-118, 0, 118, 0], [0, -90, 0, 90]];
|
if (name === "cross") return [[-120, 0, 120, 0], [0, -80, 0, 80]];
|
||||||
if (name === "circle") {
|
if (name === "circle") {
|
||||||
const segments = [];
|
// 24-sided lattice circle. More vertices preserve a round silhouette while
|
||||||
const n = 18, r = 108;
|
// every endpoint remains on the shared 20px editor grid.
|
||||||
for (let i = 0; i < n; i += 1) {
|
const points = [
|
||||||
const a = i / n * Math.PI * 2;
|
[120, 0], [120, 40], [100, 60], [80, 80], [60, 100], [40, 120],
|
||||||
const b = (i + 1) / n * Math.PI * 2;
|
[0, 120], [-40, 120], [-60, 100], [-80, 80], [-100, 60], [-120, 40],
|
||||||
segments.push([Math.cos(a) * r, Math.sin(a) * r, Math.cos(b) * r, Math.sin(b) * r]);
|
[-120, 0], [-120, -40], [-100, -60], [-80, -80], [-60, -100], [-40, -120],
|
||||||
}
|
[0, -120], [40, -120], [60, -100], [80, -80], [100, -60], [120, -40],
|
||||||
return segments;
|
];
|
||||||
|
return points.map((point, index) => {
|
||||||
|
const next = points[(index + 1) % points.length];
|
||||||
|
return [point[0], point[1], next[0], next[1]];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -65,23 +80,32 @@
|
||||||
function cleanSegments(segments = [], opts = {}) {
|
function cleanSegments(segments = [], opts = {}) {
|
||||||
const limit = Math.max(1, Number(opts.limit || 96) || 96);
|
const limit = Math.max(1, Number(opts.limit || 96) || 96);
|
||||||
const bound = Math.max(1, Number(opts.bound || 420) || 420);
|
const bound = Math.max(1, Number(opts.bound || 420) || 420);
|
||||||
const minLength = Math.max(0, Number(opts.minLength || 4) || 4);
|
const gridSize = normalizedGridSize(opts.gridSize);
|
||||||
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-78, 0, 78, 0];
|
const snap = opts.snap !== false;
|
||||||
|
const minLength = Math.max(gridSize, Number(opts.minLength || gridSize) || gridSize);
|
||||||
|
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-80, 0, 80, 0];
|
||||||
|
const coord = value => {
|
||||||
|
const raw = snap ? snapValue(value, gridSize) : (Number(value) || 0);
|
||||||
|
return Math.max(-bound, Math.min(bound, raw));
|
||||||
|
};
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const seg of segments || []) {
|
for (const seg of segments || []) {
|
||||||
if (!Array.isArray(seg) || seg.length < 4) continue;
|
if (!Array.isArray(seg) || seg.length < 4) continue;
|
||||||
const x1 = Math.max(-bound, Math.min(bound, Number(seg[0]) || 0));
|
const x1 = coord(seg[0]);
|
||||||
const y1 = Math.max(-bound, Math.min(bound, Number(seg[1]) || 0));
|
const y1 = coord(seg[1]);
|
||||||
const x2 = Math.max(-bound, Math.min(bound, Number(seg[2]) || 0));
|
const x2 = coord(seg[2]);
|
||||||
const y2 = Math.max(-bound, Math.min(bound, Number(seg[3]) || 0));
|
const y2 = coord(seg[3]);
|
||||||
if (Math.hypot(x2 - x1, y2 - y1) >= minLength) out.push([x1, y1, x2, y2]);
|
if (Math.hypot(x2 - x1, y2 - y1) >= minLength) out.push([x1, y1, x2, y2]);
|
||||||
if (out.length >= limit) break;
|
if (out.length >= limit) break;
|
||||||
}
|
}
|
||||||
if (!out.length) out.push(fallback.slice(0, 4));
|
if (!out.length) {
|
||||||
|
const snappedFallback = fallback.slice(0, 4).map(value => coord(value));
|
||||||
|
out.push(snappedFallback);
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawEditorBase(ctx, canvas, cx = canvas?.width / 2 || 0, cy = canvas?.height / 2 || 0) {
|
function drawEditorBase(ctx, canvas, cx = canvas?.width / 2 || 0, cy = canvas?.height / 2 || 0, gridSize = DEFAULT_GRID_SIZE) {
|
||||||
if (!ctx || !canvas) return false;
|
if (!ctx || !canvas) return false;
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
@ -90,8 +114,9 @@
|
||||||
ctx.translate(cx, cy);
|
ctx.translate(cx, cy);
|
||||||
ctx.strokeStyle = "rgba(100,80,60,0.16)";
|
ctx.strokeStyle = "rgba(100,80,60,0.16)";
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
for (let x = -cx; x <= cx; x += 40) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); }
|
const grid = normalizedGridSize(gridSize);
|
||||||
for (let y = -cy; y <= cy; y += 40) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); }
|
for (let x = -cx; x <= cx; x += grid) { ctx.beginPath(); ctx.moveTo(x, -cy); ctx.lineTo(x, cy); ctx.stroke(); }
|
||||||
|
for (let y = -cy; y <= cy; y += grid) { ctx.beginPath(); ctx.moveTo(-cx, y); ctx.lineTo(cx, y); ctx.stroke(); }
|
||||||
ctx.strokeStyle = "rgba(70,60,50,0.38)";
|
ctx.strokeStyle = "rgba(70,60,50,0.38)";
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.beginPath(); ctx.moveTo(-cx, 0); ctx.lineTo(cx, 0); ctx.moveTo(0, -cy); ctx.lineTo(0, cy); ctx.stroke();
|
ctx.beginPath(); ctx.moveTo(-cx, 0); ctx.lineTo(cx, 0); ctx.moveTo(0, -cy); ctx.lineTo(0, cy); ctx.stroke();
|
||||||
|
|
@ -173,19 +198,20 @@
|
||||||
const prefix = opts.prefix || "rotator";
|
const prefix = opts.prefix || "rotator";
|
||||||
const cx = opts.cx ?? canvas.width / 2;
|
const cx = opts.cx ?? canvas.width / 2;
|
||||||
const cy = opts.cy ?? canvas.height / 2;
|
const cy = opts.cy ?? canvas.height / 2;
|
||||||
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-78, 0, 78, 0];
|
const gridSize = normalizedGridSize(opts.gridSize);
|
||||||
|
const fallback = Array.isArray(opts.fallback) ? opts.fallback : [-80, 0, 80, 0];
|
||||||
let mode = "line";
|
let mode = "line";
|
||||||
let segments = (opts.segments || []).map(seg => Array.isArray(seg) ? seg.slice(0, 4) : seg).filter(seg => Array.isArray(seg) && seg.length >= 4);
|
let segments = cleanSegments(opts.segments || [], { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength, gridSize, snap: true });
|
||||||
let draft = null;
|
let draft = null;
|
||||||
let drawing = false;
|
let drawing = false;
|
||||||
|
|
||||||
const lineWidth = () => Math.max(1, Number(typeof opts.lineWidth === "function" ? opts.lineWidth() : opts.lineWidth || 12) || 12);
|
const lineWidth = () => Math.max(1, Number(typeof opts.lineWidth === "function" ? opts.lineWidth() : opts.lineWidth || 12) || 12);
|
||||||
const strokeStyle = () => typeof opts.strokeStyle === "function" ? opts.strokeStyle() : opts.strokeStyle;
|
const strokeStyle = () => typeof opts.strokeStyle === "function" ? opts.strokeStyle() : opts.strokeStyle;
|
||||||
const clean = () => { segments = cleanSegments(segments, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength }); };
|
const clean = () => { segments = cleanSegments(segments, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength, gridSize, snap: true }); };
|
||||||
const toLocal = (e) => canvasLocalPoint(canvas, e, cx, cy);
|
const toLocal = (e) => snapPoint(canvasLocalPoint(canvas, e, cx, cy), gridSize);
|
||||||
|
|
||||||
const draw = () => {
|
const draw = () => {
|
||||||
drawEditorBase(ctx, canvas, cx, cy);
|
drawEditorBase(ctx, canvas, cx, cy, gridSize);
|
||||||
if (typeof opts.drawBeforeSegments === "function") opts.drawBeforeSegments(ctx, { cx, cy, lineWidth: lineWidth(), segments: segments.slice() });
|
if (typeof opts.drawBeforeSegments === "function") opts.drawBeforeSegments(ctx, { cx, cy, lineWidth: lineWidth(), segments: segments.slice() });
|
||||||
const lw = lineWidth();
|
const lw = lineWidth();
|
||||||
drawSegments(ctx, segments, { lineWidth: lw, strokeStyle: strokeStyle() || "rgba(80,150,205,0.92)" });
|
drawSegments(ctx, segments, { lineWidth: lw, strokeStyle: strokeStyle() || "rgba(80,150,205,0.92)" });
|
||||||
|
|
@ -240,7 +266,7 @@
|
||||||
const last = segments[segments.length - 1];
|
const last = segments[segments.length - 1];
|
||||||
if (!last) return;
|
if (!last) return;
|
||||||
const lx = last[2], ly = last[3];
|
const lx = last[2], ly = last[3];
|
||||||
if (Math.hypot(p.x - lx, p.y - ly) >= 5) {
|
if (Math.hypot(p.x - lx, p.y - ly) >= gridSize) {
|
||||||
last[2] = p.x; last[3] = p.y;
|
last[2] = p.x; last[3] = p.y;
|
||||||
segments.push([p.x, p.y, p.x, p.y]);
|
segments.push([p.x, p.y, p.x, p.y]);
|
||||||
}
|
}
|
||||||
|
|
@ -257,7 +283,7 @@
|
||||||
if (!draft) return;
|
if (!draft) return;
|
||||||
if (mode === "line") {
|
if (mode === "line") {
|
||||||
const seg = [draft[0], draft[1], p.x, p.y];
|
const seg = [draft[0], draft[1], p.x, p.y];
|
||||||
if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= 4) segments.push(seg);
|
if (Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) >= gridSize) segments.push(seg);
|
||||||
}
|
}
|
||||||
draft = null;
|
draft = null;
|
||||||
clean();
|
clean();
|
||||||
|
|
@ -281,7 +307,7 @@
|
||||||
draw,
|
draw,
|
||||||
getSegments: () => segments.map(seg => seg.slice(0, 4)),
|
getSegments: () => segments.map(seg => seg.slice(0, 4)),
|
||||||
setMode,
|
setMode,
|
||||||
setSegments(next = []) { segments = next.map(seg => Array.isArray(seg) ? seg.slice(0, 4) : seg).filter(seg => Array.isArray(seg) && seg.length >= 4); draft = null; draw(); },
|
setSegments(next = []) { segments = cleanSegments(next, { fallback, limit: opts.limit, bound: opts.bound, minLength: opts.minLength, gridSize, snap: true }); draft = null; draw(); },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,5 +315,9 @@
|
||||||
decorateToolbar,
|
decorateToolbar,
|
||||||
createSegmentEditorController,
|
createSegmentEditorController,
|
||||||
syncInputPair,
|
syncInputPair,
|
||||||
|
snapPoint,
|
||||||
|
cleanSegments,
|
||||||
|
templateSegments,
|
||||||
|
gridSize: DEFAULT_GRID_SIZE,
|
||||||
});
|
});
|
||||||
})(typeof window !== "undefined" ? window : globalThis);
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
function isPhysicsType(typeOrItem) { return isBodyType(typeOrItem) || isConstraintType(typeOrItem); }
|
function isPhysicsType(typeOrItem) { return isBodyType(typeOrItem) || isConstraintType(typeOrItem); }
|
||||||
|
|
||||||
function defaultSegments(type) {
|
function defaultSegments(type) {
|
||||||
if (type === "poison_block") return [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]];
|
if (type === "poison_block") return [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]];
|
||||||
if (type === "rotator") return [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
if (type === "rotator") return [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||||
return [[-78, 0, 78, 0]];
|
return [[-78, 0, 78, 0]];
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
velocity: { x: 0, y: 0, angular: 0, linear: 0 },
|
||||||
motor: { powered: type !== "poison_block", speed: type === "rotator" ? Math.PI * 0.65 : (type === "reciprocator" ? 92 : 0), direction: 1 },
|
motor: { powered: type !== "poison_block", speed: type === "rotator" ? Math.PI * 0.65 : (type === "reciprocator" ? 92 : 0), direction: 1 },
|
||||||
rail: type === "reciprocator" ? { axisAngle: angle, travel: 150, phase: 0, anchorX: x, anchorY: y } : null,
|
rail: type === "reciprocator" ? { axisAngle: angle, travel: 150, phase: 0, anchorX: x, anchorY: y } : null,
|
||||||
shape: { model: "segments", thickness: type === "poison_block" ? 14 : 12, segments: cloneSegments(null, defaultSegments(type)), version: 0 },
|
shape: { model: "segments", thickness: type === "poison_block" ? 11 : 12, segments: cloneSegments(null, defaultSegments(type)), version: 0 },
|
||||||
collision: { solid: true, hazard: type === "poison_block" },
|
collision: { solid: true, hazard: type === "poison_block" },
|
||||||
hazard: type === "poison_block" ? { kind: "poison", damage: 7 } : null,
|
hazard: type === "poison_block" ? { kind: "poison", damage: 7 } : null,
|
||||||
sleep: { awakeUntil: 0 },
|
sleep: { awakeUntil: 0 },
|
||||||
|
|
@ -191,12 +191,12 @@
|
||||||
b.velocity = b.velocity || {};
|
b.velocity = b.velocity || {};
|
||||||
b.motor = b.motor || {};
|
b.motor = b.motor || {};
|
||||||
b.rail = b.rail || (item.type === "reciprocator" ? { axisAngle: num(item.angle), travel: 150, phase: 0, anchorX: num(item.x), anchorY: num(item.y) } : null);
|
b.rail = b.rail || (item.type === "reciprocator" ? { axisAngle: num(item.angle), travel: 150, phase: 0, anchorX: num(item.x), anchorY: num(item.y) } : null);
|
||||||
b.shape = b.shape || { model: "segments", thickness: item.type === "poison_block" ? 14 : 12, segments: defaultSegments(item.type), version: 0 };
|
b.shape = b.shape || { model: "segments", thickness: item.type === "poison_block" ? 11 : 12, segments: defaultSegments(item.type), version: 0 };
|
||||||
b.collision = b.collision || { solid: true, hazard: item.type === "poison_block" };
|
b.collision = b.collision || { solid: true, hazard: item.type === "poison_block" };
|
||||||
b.sleep = b.sleep || { awakeUntil: 0 };
|
b.sleep = b.sleep || { awakeUntil: 0 };
|
||||||
if (item.type === "poison_block") b.hazard = b.hazard || { kind: "poison", damage: 7 };
|
if (item.type === "poison_block") b.hazard = b.hazard || { kind: "poison", damage: 7 };
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "thickness": b.shape.thickness = Math.max(4, Math.min(34, num(value, item.type === "poison_block" ? 14 : 12))); b.shape.version = (Number(b.shape.version || 0) || 0) + 1; break;
|
case "thickness": b.shape.thickness = Math.max(4, Math.min(34, num(value, item.type === "poison_block" ? 11 : 12))); b.shape.version = (Number(b.shape.version || 0) || 0) + 1; break;
|
||||||
case "motorOn": b.motor.powered = item.type === "poison_block" ? false : !!value; break;
|
case "motorOn": b.motor.powered = item.type === "poison_block" ? false : !!value; break;
|
||||||
case "motorSpeed": b.motor.speed = Math.max(0, num(value)); break;
|
case "motorSpeed": b.motor.speed = Math.max(0, num(value)); break;
|
||||||
case "spin": b.velocity.angular = num(value); break;
|
case "spin": b.velocity.angular = num(value); break;
|
||||||
|
|
@ -466,7 +466,7 @@
|
||||||
body.velocity = { x: num(velArr[0]) / 10, y: num(velArr[1]) / 10, angular: num(velArr[2]) / 1000, linear: num(velArr[3]) / 10 };
|
body.velocity = { x: num(velArr[0]) / 10, y: num(velArr[1]) / 10, angular: num(velArr[2]) / 1000, linear: num(velArr[3]) / 10 };
|
||||||
body.motor = { powered: bool(motorArr[0], true), speed: num(motorArr[1]) / 10, direction: Math.sign(num(motorArr[2], 1)) || 1 };
|
body.motor = { powered: bool(motorArr[0], true), speed: num(motorArr[1]) / 10, direction: Math.sign(num(motorArr[2], 1)) || 1 };
|
||||||
body.rail = railArr ? { axisAngle: num(railArr[0]) / 1000, travel: Math.max(24, num(railArr[1], 1500) / 10), phase: clamp(num(railArr[2]) / 1000, -1, 1), anchorX: num(railArr[3], item.x), anchorY: num(railArr[4], item.y) } : null;
|
body.rail = railArr ? { axisAngle: num(railArr[0]) / 1000, travel: Math.max(24, num(railArr[1], 1500) / 10), phase: clamp(num(railArr[2]) / 1000, -1, 1), anchorX: num(railArr[3], item.x), anchorY: num(railArr[4], item.y) } : null;
|
||||||
body.shape = { model: "segments", thickness: Math.max(4, Math.min(34, num(shapeArr[0], item.type === "poison_block" ? 140 : 120) / 10)), segments: cloneSegments(shapeArr[1], defaultSegments(item.type)), version: 0 };
|
body.shape = { model: "segments", thickness: Math.max(4, Math.min(34, num(shapeArr[0], item.type === "poison_block" ? 110 : 120) / 10)), segments: cloneSegments(shapeArr[1], defaultSegments(item.type)), version: 0 };
|
||||||
body.collision = { solid: collArr[0] !== 0, hazard: collArr[1] !== 0 };
|
body.collision = { solid: collArr[0] !== 0, hazard: collArr[1] !== 0 };
|
||||||
body.hazard = hazardArr ? { kind: hazardArr[0] || "poison", damage: Math.max(1, num(hazardArr[1], 70) / 10) } : null;
|
body.hazard = hazardArr ? { kind: hazardArr[0] || "poison", damage: Math.max(1, num(hazardArr[1], 70) / 10) } : null;
|
||||||
body.mass = Math.max(0.2, num(p.mass, Math.round(num(body.mass, 1) * 10)) / 10);
|
body.mass = Math.max(0.2, num(p.mass, Math.round(num(body.mass, 1) * 10)) / 10);
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
pb?.setScalar?.(item, "thickness", 12, "preview");
|
pb?.setScalar?.(item, "thickness", 12, "preview");
|
||||||
pb?.setSegments?.(item, [[-78, 0, 78, 0], [0, -52, 0, 52]], "preview");
|
pb?.setSegments?.(item, [[-78, 0, 78, 0], [0, -52, 0, 52]], "preview");
|
||||||
} else if (item.type === "poison_block") {
|
} else if (item.type === "poison_block") {
|
||||||
pb?.setScalar?.(item, "thickness", 14, "preview");
|
pb?.setScalar?.(item, "thickness", 11, "preview");
|
||||||
item.world = worldRef || null;
|
item.world = worldRef || null;
|
||||||
} else if (item.type === "reciprocator") {
|
} else if (item.type === "reciprocator") {
|
||||||
pb?.setScalar?.(item, "railOn", true, "preview");
|
pb?.setScalar?.(item, "railOn", true, "preview");
|
||||||
|
|
|
||||||
|
|
@ -361,23 +361,29 @@
|
||||||
if (valuesDiffer(w[6], 1)) mask |= 2;
|
if (valuesDiffer(w[6], 1)) mask |= 2;
|
||||||
if (valuesDiffer(w[7], 0)) mask |= 4;
|
if (valuesDiffer(w[7], 0)) mask |= 4;
|
||||||
if (valuesDiffer(w[9], 0)) mask |= 8;
|
if (valuesDiffer(w[9], 0)) mask |= 8;
|
||||||
writeBitFields(writer, [mask], [4]);
|
if (valuesDiffer(w[10], 0)) mask |= 16;
|
||||||
|
if (valuesDiffer(w[11], 0)) mask |= 32;
|
||||||
|
writeBitFields(writer, [mask], [6]);
|
||||||
if (mask & 1) writer.s(w[5] || 0); // lastBirthAt10 can be negative
|
if (mask & 1) writer.s(w[5] || 0); // lastBirthAt10 can be negative
|
||||||
if (mask & 2) writer.u(w[6] || 1); // generation
|
if (mask & 2) writer.u(w[6] || 1); // generation
|
||||||
if (mask & 4) writer.u(w[7] || 0); // dead count
|
if (mask & 4) writer.u(w[7] || 0); // dead count
|
||||||
writeSaveString(writer, w[8] || ""); // worldSeed
|
writeSaveString(writer, w[8] || ""); // worldSeed
|
||||||
if (mask & 8) writer.u(w[9] || 0); // birthSerial
|
if (mask & 8) writer.u(w[9] || 0); // birthSerial
|
||||||
|
if (mask & 16) writer.u(w[10] || 0); // tarinai population limit
|
||||||
|
if (mask & 32) writer.u(w[11] || 0); // object limit
|
||||||
}
|
}
|
||||||
function readWorld(reader) {
|
function readWorld(reader) {
|
||||||
const [field, ground, mood, weather] = readBitFields(reader, [2, 3, 4, 3]);
|
const [field, ground, mood, weather] = readBitFields(reader, [2, 3, 4, 3]);
|
||||||
const tick = reader.u();
|
const tick = reader.u();
|
||||||
const [mask] = readBitFields(reader, [4]);
|
const [mask] = readBitFields(reader, [6]);
|
||||||
const lastBirthAt = (mask & 1) ? reader.s() : -9990;
|
const lastBirthAt = (mask & 1) ? reader.s() : -9990;
|
||||||
const generation = (mask & 2) ? reader.u() : 1;
|
const generation = (mask & 2) ? reader.u() : 1;
|
||||||
const deadCount = (mask & 4) ? reader.u() : 0;
|
const deadCount = (mask & 4) ? reader.u() : 0;
|
||||||
const seed = readSaveString(reader);
|
const seed = readSaveString(reader);
|
||||||
const birthSerial = (mask & 8) ? reader.u() : 0;
|
const birthSerial = (mask & 8) ? reader.u() : 0;
|
||||||
return [field, ground, mood, tick, weather, lastBirthAt, generation, deadCount, seed, birthSerial];
|
const tarinaiPopulationLimit = (mask & 16) ? reader.u() : 0;
|
||||||
|
const objectLimit = (mask & 32) ? reader.u() : 0;
|
||||||
|
return [field, ground, mood, tick, weather, lastBirthAt, generation, deadCount, seed, birthSerial, tarinaiPopulationLimit, objectLimit];
|
||||||
}
|
}
|
||||||
|
|
||||||
function seedPersonalityArray(seed = "", flags = 0) {
|
function seedPersonalityArray(seed = "", flags = 0) {
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
if (!ids) throw new Error("TarinaiDomainIds must be loaded before save_schema.js");
|
if (!ids) throw new Error("TarinaiDomainIds must be loaded before save_schema.js");
|
||||||
if (!itemCatalog) throw new Error("TarinaiItemTypeCatalog must be loaded before save_schema.js");
|
if (!itemCatalog) throw new Error("TarinaiItemTypeCatalog must be loaded before save_schema.js");
|
||||||
|
|
||||||
const SNAPSHOT_VERSION = 22;
|
const SNAPSHOT_VERSION = 23;
|
||||||
const BINARY_SCHEMA_VERSION = 27;
|
const BINARY_SCHEMA_VERSION = 28;
|
||||||
|
|
||||||
function moodValue(index, fallback = "relaxed") {
|
function moodValue(index, fallback = "relaxed") {
|
||||||
return ids.canonicalMoodId(ids.enumValue(ids.MOOD_IDS, index, fallback));
|
return ids.canonicalMoodId(ids.enumValue(ids.MOOD_IDS, index, fallback));
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
// Owns low-frequency ambient Tarinai/grass/rain-water creation.
|
// Owns low-frequency ambient Tarinai/grass/rain-water creation.
|
||||||
(function (global) {
|
(function (global) {
|
||||||
function spawnAmbientTarinai(worldRef, dt) {
|
function spawnAmbientTarinai(worldRef, dt) {
|
||||||
if (worldRef.tarinai.length > 0 && worldRef.tarinai.length <= 10 && worldRef.time - (worldRef.lastBirthAt || -999) > 12 && Math.random() < dt * 0.022) {
|
if ((worldRef.canAddTarinai?.(1) ?? true) && worldRef.tarinai.length > 0 && worldRef.tarinai.length <= 10 && worldRef.time - (worldRef.lastBirthAt || -999) > 12 && Math.random() < dt * 0.022) {
|
||||||
const spot = worldRef.edgeSpawnPoint(null, null, 42, true);
|
const spot = worldRef.edgeSpawnPoint(null, null, 42, true);
|
||||||
const t = worldRef.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, entryTimer: 2.0 });
|
const t = worldRef.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, entryTimer: 2.0 });
|
||||||
|
if (!t) return;
|
||||||
t.wanderAngle = spot.angle;
|
t.wanderAngle = spot.angle;
|
||||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35);
|
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.35);
|
||||||
worldRef.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
|
worldRef.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
|
||||||
|
|
@ -27,8 +28,7 @@
|
||||||
if (spot) {
|
if (spot) {
|
||||||
const sprout = new Item("grass", spot.x, spot.y);
|
const sprout = new Item("grass", spot.x, spot.y);
|
||||||
global.TarinaiGrass.setStage(sprout, 0, { force: true });
|
global.TarinaiGrass.setStage(sprout, 0, { force: true });
|
||||||
if (worldRef.addItem) worldRef.addItem(sprout, "grass-spawn");
|
worldRef.addItem?.(sprout, "grass-spawn");
|
||||||
else worldRef.items.push(sprout);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
// share the rain overlay renderer.
|
// share the rain overlay renderer.
|
||||||
if (global.TarinaiWeatherSystem.shouldDropRainWater(worldRef, dt)) {
|
if (global.TarinaiWeatherSystem.shouldDropRainWater(worldRef, dt)) {
|
||||||
const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef);
|
const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef);
|
||||||
worldRef.addItem?.(drop, "rain-water") || worldRef.items.push(drop);
|
worldRef.addItem?.(drop, "rain-water");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,17 +119,23 @@
|
||||||
worldRef.limitBallChasers();
|
worldRef.limitBallChasers();
|
||||||
worldRef.resolveBallInteractions(dt);
|
worldRef.resolveBallInteractions(dt);
|
||||||
if (endBallInteractions) endBallInteractions();
|
if (endBallInteractions) endBallInteractions();
|
||||||
const hasFastTarinai = (worldRef.tarinai || []).some(t => {
|
let fastTarinai = null;
|
||||||
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) return false;
|
const collisionSpeedThreshold = global.TarinaiCollisionResponseSystem?.highSpeedThreshold?.() || 165;
|
||||||
return Math.hypot(t.vx || 0, t.vy || 0) >= 150;
|
for (const t of worldRef.tarinai || []) {
|
||||||
});
|
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||||
const collisionInterval = hasFastTarinai ? 0 : 0.55;
|
if (Math.hypot(t.vx || 0, t.vy || 0) < collisionSpeedThreshold) continue;
|
||||||
if (hasFastTarinai || now >= (worldRef.nextTarinaiCollisionCheckAt || 0)) {
|
if (!fastTarinai) fastTarinai = [];
|
||||||
worldRef.nextTarinaiCollisionCheckAt = now + collisionInterval;
|
fastTarinai.push(t);
|
||||||
|
}
|
||||||
|
if (fastTarinai?.length) {
|
||||||
|
worldRef.nextTarinaiCollisionCheckAt = now;
|
||||||
worldRef.workStats && (worldRef.workStats.collisionRuns = (worldRef.workStats.collisionRuns || 0) + 1);
|
worldRef.workStats && (worldRef.workStats.collisionRuns = (worldRef.workStats.collisionRuns || 0) + 1);
|
||||||
const endCollision = global.TarinaiPerf.begin("update.tarinai.collisions") || null;
|
const endCollision = global.TarinaiPerf.begin("update.tarinai.collisions") || null;
|
||||||
try { worldRef.resolveTarinaiHighSpeedCollisions(dt); }
|
try { worldRef.resolveTarinaiHighSpeedCollisions(dt, { fastSources: fastTarinai, threshold: collisionSpeedThreshold }); }
|
||||||
finally { if (endCollision) endCollision(); }
|
finally { if (endCollision) endCollision(); }
|
||||||
|
} else if (now >= (worldRef.nextTarinaiCollisionCheckAt || 0)) {
|
||||||
|
worldRef.nextTarinaiCollisionCheckAt = now + 0.55;
|
||||||
|
worldRef.workStats && (worldRef.workStats.collisionSkips = (worldRef.workStats.collisionSkips || 0) + 1);
|
||||||
} else {
|
} else {
|
||||||
worldRef.workStats && (worldRef.workStats.collisionSkips = (worldRef.workStats.collisionSkips || 0) + 1);
|
worldRef.workStats && (worldRef.workStats.collisionSkips = (worldRef.workStats.collisionSkips || 0) + 1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,8 @@
|
||||||
q(worldRef?.deadCount, 1, 0),
|
q(worldRef?.deadCount, 1, 0),
|
||||||
worldRef?.worldSeed || global.TarinaiSeedFactory.ensureWorldSeed(worldRef) || "",
|
worldRef?.worldSeed || global.TarinaiSeedFactory.ensureWorldSeed(worldRef) || "",
|
||||||
q(worldRef?.birthSerial, 1, 0),
|
q(worldRef?.birthSerial, 1, 0),
|
||||||
|
q(worldRef?.tarinaiPopulationLimit, 1, 0),
|
||||||
|
q(worldRef?.objectLimit, 1, 0),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
function applyCompactWorld(worldRef, arr = []) {
|
function applyCompactWorld(worldRef, arr = []) {
|
||||||
|
|
@ -157,6 +159,8 @@
|
||||||
worldRef.weather = enumValue(WEATHER_IDS, arr[4], "sunny");
|
worldRef.weather = enumValue(WEATHER_IDS, arr[4], "sunny");
|
||||||
worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory.createWorldSeed() || `w${Date.now().toString(36)}`;
|
worldRef.worldSeed = String(arr[8] || "") || global.TarinaiSeedFactory.createWorldSeed() || `w${Date.now().toString(36)}`;
|
||||||
worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0));
|
worldRef.birthSerial = Math.max(0, u(arr[9], 1, 0));
|
||||||
|
worldRef.tarinaiPopulationLimit = Math.max(0, u(arr[10], 1, 0));
|
||||||
|
worldRef.objectLimit = Math.max(0, u(arr[11], 1, 0));
|
||||||
worldRef.day = Math.max(1, Math.floor(totalTime / dayLength) + 1);
|
worldRef.day = Math.max(1, Math.floor(totalTime / dayLength) + 1);
|
||||||
worldRef.time = totalTime - (worldRef.day - 1) * dayLength;
|
worldRef.time = totalTime - (worldRef.day - 1) * dayLength;
|
||||||
worldRef.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
worldRef.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,12 @@ function selectOwnedStructure(type, range = Infinity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMateTarget(tarinai, world, range = 440) {
|
function selectMateTarget(tarinai, world, range = 440) {
|
||||||
return world?.nearestOther?.(tarinai, range, other => !world.areParentChild?.(tarinai, other) && !!tarinai.isZunchiSlave === !!other.isZunchiSlave && !other.sleepDisease && !other.fightDisease) || null;
|
const eligible = other => other && other !== tarinai && !other.dead && !world.areParentChild?.(tarinai, other) && !!tarinai.isZunchiSlave === !!other.isZunchiSlave && !other.sleepDisease && !other.fightDisease;
|
||||||
|
if (tarinai?.isTarinaiChampion) {
|
||||||
|
const champion = world?.nearestOther?.(tarinai, range + 260, other => eligible(other) && other.isTarinaiChampion) || null;
|
||||||
|
if (champion) return champion;
|
||||||
|
}
|
||||||
|
return world?.nearestOther?.(tarinai, range, eligible) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function basicCanStartWithTarget(t, world, ctx = {}) {
|
function basicCanStartWithTarget(t, world, ctx = {}) {
|
||||||
|
|
@ -380,7 +385,7 @@ function createApproachMateActionSpec() {
|
||||||
phrase: "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b",
|
phrase: "\u7e41\u6b96\u3067\u304d\u308b\u76f8\u624b\u306b\u8fd1\u3065\u3044\u3066\u3044\u308b",
|
||||||
weight: 48,
|
weight: 48,
|
||||||
selectTarget(t, world) { return selectMateTarget(t, world, 520); },
|
selectTarget(t, world) { return selectMateTarget(t, world, 520); },
|
||||||
canStart(t, world, ctx = {}) { return (t.reproductionTimer || 0) <= 12 && !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
canStart(t, world, ctx = {}) { return (t.reproductionTimer || 0) <= 12 && (world?.canAddTarinai?.(1) ?? true) && !!(ctx.target ?? this.selectTarget(t, world, ctx)); },
|
||||||
start(t, world, ctx = {}) {
|
start(t, world, ctx = {}) {
|
||||||
const other = ctx.target ?? this.selectTarget(t, world, ctx);
|
const other = ctx.target ?? this.selectTarget(t, world, ctx);
|
||||||
if (!other) return false;
|
if (!other) return false;
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOwnedStructureFromGrass(t, world, type, label) {
|
function buildOwnedStructureFromGrass(t, world, type, label) {
|
||||||
|
if (!(world?.canAddObjects?.(1) ?? true)) return false;
|
||||||
const material = findNearbyBuildMaterial(world, t, type, 520);
|
const material = findNearbyBuildMaterial(world, t, type, 520);
|
||||||
if (!material || !globalThis.StructureRegistry) return false;
|
if (!material || !globalThis.StructureRegistry) return false;
|
||||||
t.buildPlan = {
|
t.buildPlan = {
|
||||||
|
|
@ -208,6 +209,9 @@ function continueBuildPlan(t, world, dt) {
|
||||||
if (!spot) {
|
if (!spot) {
|
||||||
return stopFailedBuildPlan(t, world, plan.type, "\u4F5C\u308B\u5834\u6240\u304C\u898B\u3064\u304B\u3089\u306A\u3044");
|
return stopFailedBuildPlan(t, world, plan.type, "\u4F5C\u308B\u5834\u6240\u304C\u898B\u3064\u304B\u3089\u306A\u3044");
|
||||||
}
|
}
|
||||||
|
if (!(world?.canAddObjects?.(1) ?? true)) {
|
||||||
|
return stopFailedBuildPlan(t, world, plan.type, "\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u4e0a\u9650\u306b\u9054\u3057\u3066\u3044\u308b");
|
||||||
|
}
|
||||||
consumeGrassMaterial(material, structureGrassStageCost(plan.type));
|
consumeGrassMaterial(material, structureGrassStageCost(plan.type));
|
||||||
const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world);
|
const structure = globalThis.StructureRegistry.create(plan.type, t, spot.x, spot.y, world);
|
||||||
if (!structure) {
|
if (!structure) {
|
||||||
|
|
@ -219,7 +223,7 @@ function continueBuildPlan(t, world, dt) {
|
||||||
structure.x = t.x;
|
structure.x = t.x;
|
||||||
structure.y = t.y - Math.max(28, (t.radius || 24) * 1.28);
|
structure.y = t.y - Math.max(28, (t.radius || 24) * 1.28);
|
||||||
}
|
}
|
||||||
const addedStructure = world.addItem?.(structure, `build:${plan.type}`) || (world.items.push(structure), structure);
|
const addedStructure = world.addItem?.(structure, `build:${plan.type}`) || null;
|
||||||
if (!addedStructure) {
|
if (!addedStructure) {
|
||||||
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
return stopFailedBuildPlan(t, world, plan.type, "\u4f5c\u308b\u5834\u6240\u304c\u898b\u3064\u304b\u3089\u306a\u3044");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@ function processForcedBehaviorQueue(tarinai, world, needs) {
|
||||||
tarinai.sleepSession = null;
|
tarinai.sleepSession = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const started = startNeedAction(tarinai, world, choice, action, reasonText, {
|
const started = globalThis.TarinaiNeedsRuntime?.startNeedAction?.(tarinai, world, choice, action, reasonText, {
|
||||||
forced: true,
|
forced: true,
|
||||||
forcedId: entry.uid,
|
forcedId: entry.uid,
|
||||||
forcedReasonText: entry.reasonText || reasonText,
|
forcedReasonText: entry.reasonText || reasonText,
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ function startNeedDrivenEmergencyReaction(tarinai, breaker = null, reason = "\u3
|
||||||
tarinai.conflictUrge = Math.max(tarinai.conflictUrge || 0, 72);
|
tarinai.conflictUrge = Math.max(tarinai.conflictUrge || 0, 72);
|
||||||
const choice = { need: "social", tiedNeeds: ["social", "safety"], max: Number(tarinai.needRaw?.social || tarinai.needs?.social || 0) || 72 };
|
const choice = { need: "social", tiedNeeds: ["social", "safety"], max: Number(tarinai.needRaw?.social || tarinai.needs?.social || 0) || 72 };
|
||||||
const reasonText = buildReasonText("social", choice.tiedNeeds, fightAction, tarinai, world);
|
const reasonText = buildReasonText("social", choice.tiedNeeds, fightAction, tarinai, world);
|
||||||
if (startNeedAction(tarinai, world, choice, fightAction, reasonText)) {
|
if (globalThis.TarinaiNeedsRuntime?.startNeedAction?.(tarinai, world, choice, fightAction, reasonText)) {
|
||||||
tarinai.lastNeedShockBreaker = null;
|
tarinai.lastNeedShockBreaker = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -232,7 +232,7 @@ function startNeedDrivenEmergencyReaction(tarinai, breaker = null, reason = "\u3
|
||||||
if (!panicAction) return false;
|
if (!panicAction) return false;
|
||||||
const choice = { need: "safety", tiedNeeds: ["safety"], max: Number(tarinai.needRaw?.safety || tarinai.needs?.safety || 0) || 80 };
|
const choice = { need: "safety", tiedNeeds: ["safety"], max: Number(tarinai.needRaw?.safety || tarinai.needs?.safety || 0) || 80 };
|
||||||
const reasonText = reason && reason.endsWith("\u3002") ? reason : buildReasonText("safety", choice.tiedNeeds, panicAction, tarinai, world);
|
const reasonText = reason && reason.endsWith("\u3002") ? reason : buildReasonText("safety", choice.tiedNeeds, panicAction, tarinai, world);
|
||||||
if (startNeedAction(tarinai, world, choice, panicAction, reasonText)) {
|
if (globalThis.TarinaiNeedsRuntime?.startNeedAction?.(tarinai, world, choice, panicAction, reasonText)) {
|
||||||
tarinai.lastNeedShockBreaker = null;
|
tarinai.lastNeedShockBreaker = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,11 @@ function updateNeeds(tarinai, world, dt = 0) {
|
||||||
socialParts.bond = (tarinai.loneliness || 0) * (0.88 + Math.max(0, sociability) * 0.16);
|
socialParts.bond = (tarinai.loneliness || 0) * (0.88 + Math.max(0, sociability) * 0.16);
|
||||||
raw.social = socialParts.bond;
|
raw.social = socialParts.bond;
|
||||||
if (tarinai.parentToFollow?.()) { socialParts.family += 18; raw.social += 18; }
|
if (tarinai.parentToFollow?.()) { socialParts.family += 18; raw.social += 18; }
|
||||||
if ((tarinai.reproductionTimer || 0) <= 8 && !tarinai.hasPaired) { socialParts.mate += 16 + ((tarinai.loveMochiTimer || 0) > 0.04 ? 42 : 0); raw.social += socialParts.mate; }
|
const championMate = tarinai.isTarinaiChampion && (tarinai.reproductionTimer || 0) <= 8 && (world?.canAddTarinai?.(1) ?? true)
|
||||||
|
? (world?.nearestOther?.(tarinai, 780, other => other && other !== tarinai && !other.dead && other.isTarinaiChampion && !world.areParentChild?.(tarinai, other) && !!tarinai.isZunchiSlave === !!other.isZunchiSlave && !other.sleepDisease && !other.fightDisease && (other.reproductionTimer || 0) <= 10) || null)
|
||||||
|
: null;
|
||||||
|
if (championMate) { socialParts.mate += 58; socialParts.championMate = 1; raw.social += 58; }
|
||||||
|
else if ((tarinai.reproductionTimer || 0) <= 8 && !tarinai.hasPaired) { socialParts.mate += 16 + ((tarinai.loveMochiTimer || 0) > 0.04 ? 42 : 0); raw.social += socialParts.mate; }
|
||||||
const enemy = tarinai.strongestRelation?.("fear");
|
const enemy = tarinai.strongestRelation?.("fear");
|
||||||
if (enemy?.score > 12) { socialParts.fearSocial += 10; raw.social += 10; }
|
if (enemy?.score > 12) { socialParts.fearSocial += 10; raw.social += 10; }
|
||||||
const conflict = evaluateConflictUrge(tarinai, world, 112);
|
const conflict = evaluateConflictUrge(tarinai, world, 112);
|
||||||
|
|
@ -289,6 +293,7 @@ function socialFightMateActionWeights(tarinai) {
|
||||||
fightWeight *= clamp(1 - Math.max(0, Number(parts.bond || 0) || 0) / 180, 0.55, 1.04);
|
fightWeight *= clamp(1 - Math.max(0, Number(parts.bond || 0) || 0) / 180, 0.55, 1.04);
|
||||||
if ((tarinai?.fightMochiTimer || 0) > 0.04) { fightWeight *= 3.4; mateWeight *= 0.55; }
|
if ((tarinai?.fightMochiTimer || 0) > 0.04) { fightWeight *= 3.4; mateWeight *= 0.55; }
|
||||||
if ((tarinai?.loveMochiTimer || 0) > 0.04) { mateWeight *= 3.15; fightWeight *= 0.58; }
|
if ((tarinai?.loveMochiTimer || 0) > 0.04) { mateWeight *= 3.15; fightWeight *= 0.58; }
|
||||||
|
if (parts.championMate) { mateWeight *= 5.0; fightWeight *= 0.35; }
|
||||||
const total = Math.max(0, fightWeight) + Math.max(0, mateWeight);
|
const total = Math.max(0, fightWeight) + Math.max(0, mateWeight);
|
||||||
return { fightWeight, mateWeight, fightProbability: total > 0 ? fightWeight / total : 0.4 };
|
return { fightWeight, mateWeight, fightProbability: total > 0 ? fightWeight / total : 0.4 };
|
||||||
}
|
}
|
||||||
|
|
@ -469,9 +474,6 @@ function startNeedAction(tarinai, world, choice, action, reasonText, options = {
|
||||||
return Boolean(ran);
|
return Boolean(ran);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Needed by tarinai_needs_core.js and tarinai_forced_behavior.js, which run outside
|
|
||||||
// this module's private IIFE but start need-driven actions through the same runtime path.
|
|
||||||
global.startNeedAction = startNeedAction;
|
|
||||||
|
|
||||||
function continueCurrentBehavior(tarinai, world, needs, dt) {
|
function continueCurrentBehavior(tarinai, world, needs, dt) {
|
||||||
const behavior = currentTarinaiBehavior(tarinai);
|
const behavior = currentTarinaiBehavior(tarinai);
|
||||||
|
|
@ -1189,5 +1191,6 @@ function resolveNeedsCore(dt) {
|
||||||
|
|
||||||
global.TarinaiNeedsRuntime = Object.freeze({
|
global.TarinaiNeedsRuntime = Object.freeze({
|
||||||
updateNeeds,
|
updateNeeds,
|
||||||
|
startNeedAction,
|
||||||
});
|
});
|
||||||
})(typeof window !== "undefined" ? window : globalThis);
|
})(typeof window !== "undefined" ? window : globalThis);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,12 @@ function mateTargetFor(t, world, maxDist = 520) {
|
||||||
if (!t || !world) return null;
|
if (!t || !world) return null;
|
||||||
const current = currentBehaviorTarget(t) || t.target;
|
const current = currentBehaviorTarget(t) || t.target;
|
||||||
const ok = o => o && o !== t && !o.dead && !world.areParentChild?.(t, o) && !!t.isZunchiSlave === !!o.isZunchiSlave && !o.sleepDisease && !o.fightDisease && (o.reproductionTimer || 0) <= 10 && (o.fightTimer || 0) <= 0.04 && (o.birthRitualTimer || 0) <= 0.04;
|
const ok = o => o && o !== t && !o.dead && !world.areParentChild?.(t, o) && !!t.isZunchiSlave === !!o.isZunchiSlave && !o.sleepDisease && !o.fightDisease && (o.reproductionTimer || 0) <= 10 && (o.fightTimer || 0) <= 0.04 && (o.birthRitualTimer || 0) <= 0.04;
|
||||||
|
const championOk = o => ok(o) && !!t.isTarinaiChampion && !!o.isTarinaiChampion;
|
||||||
|
if (championOk(current) && dist(t, current) <= maxDist + 260) return current;
|
||||||
|
if (t.isTarinaiChampion) {
|
||||||
|
const champion = world.nearestOther?.(t, maxDist + 260, championOk) || null;
|
||||||
|
if (champion) return champion;
|
||||||
|
}
|
||||||
if (ok(current) && dist(t, current) <= maxDist + 120) return current;
|
if (ok(current) && dist(t, current) <= maxDist + 120) return current;
|
||||||
return world.nearestOther?.(t, maxDist, ok) || null;
|
return world.nearestOther?.(t, maxDist, ok) || null;
|
||||||
}
|
}
|
||||||
|
|
@ -45,12 +51,15 @@ function socialFightMateWeights(t, other, world, options = {}) {
|
||||||
&& (world.fightPairCooldownRemaining?.(t, other) || 0) <= 0.04
|
&& (world.fightPairCooldownRemaining?.(t, other) || 0) <= 0.04
|
||||||
&& d <= Math.max(Number(options.fightRange || 58) || 58, (t.radius || 20) + (other.radius || 20) + 18);
|
&& d <= Math.max(Number(options.fightRange || 58) || 58, (t.radius || 20) + (other.radius || 20) + 18);
|
||||||
const sameStatus = !!t.isZunchiSlave === !!other.isZunchiSlave;
|
const sameStatus = !!t.isZunchiSlave === !!other.isZunchiSlave;
|
||||||
|
const championPair = Boolean(t.isTarinaiChampion && other.isTarinaiChampion);
|
||||||
const mateWindow = Number.isFinite(Number(options.mateTimerWindow)) ? Number(options.mateTimerWindow) : 10;
|
const mateWindow = Number.isFinite(Number(options.mateTimerWindow)) ? Number(options.mateTimerWindow) : 10;
|
||||||
const requireBirthReady = options.requireBirthReady !== false;
|
const requireBirthReady = options.requireBirthReady !== false;
|
||||||
const timerOk = requireBirthReady ? ((t.reproductionTimer || 0) <= 0.04 && (other.reproductionTimer || 0) <= 0.04) : ((t.reproductionTimer || 0) <= mateWindow && (other.reproductionTimer || 0) <= mateWindow);
|
const timerOk = requireBirthReady ? ((t.reproductionTimer || 0) <= 0.04 && (other.reproductionTimer || 0) <= 0.04) : ((t.reproductionTimer || 0) <= mateWindow && (other.reproductionTimer || 0) <= mateWindow);
|
||||||
const mateDistanceOk = d <= Math.max(loveMochi ? 54 : 44, (t.radius || 20) + (other.radius || 20) + 14);
|
const mateDistanceOk = d <= Math.max(loveMochi ? 54 : 44, (t.radius || 20) + (other.radius || 20) + 14);
|
||||||
const enough = loveMochi ? ((t.hunger || 0) < 90 && (other.hunger || 0) < 92 && (t.energy || 0) > 18 && (other.energy || 0) > 18) : ((t.hunger || 0) < 55 && (other.hunger || 0) < 60 && (t.energy || 0) > 44 && (other.energy || 0) > 44);
|
const enough = championPair
|
||||||
const lonelyBond = loveMochi ? ((t.loneliness || 0) > 4 || (other.loneliness || 0) > 4 || (t.affection || 0) > 0 || (other.affection || 0) > 0) : ((t.loneliness || 0) > 24 || (other.loneliness || 0) > 24);
|
? ((t.hunger || 0) < 76 && (other.hunger || 0) < 78 && (t.energy || 0) > 30 && (other.energy || 0) > 30)
|
||||||
|
: (loveMochi ? ((t.hunger || 0) < 90 && (other.hunger || 0) < 92 && (t.energy || 0) > 18 && (other.energy || 0) > 18) : ((t.hunger || 0) < 55 && (other.hunger || 0) < 60 && (t.energy || 0) > 44 && (other.energy || 0) > 44));
|
||||||
|
const lonelyBond = championPair || (loveMochi ? ((t.loneliness || 0) > 4 || (other.loneliness || 0) > 4 || (t.affection || 0) > 0 || (other.affection || 0) > 0) : ((t.loneliness || 0) > 24 || (other.loneliness || 0) > 24));
|
||||||
const mateEligible = sameStatus
|
const mateEligible = sameStatus
|
||||||
&& timerOk
|
&& timerOk
|
||||||
&& mateDistanceOk
|
&& mateDistanceOk
|
||||||
|
|
@ -73,6 +82,7 @@ function socialFightMateWeights(t, other, world, options = {}) {
|
||||||
fightWeight *= clamp(1 - affinity / 150, 0.34, 1.08);
|
fightWeight *= clamp(1 - affinity / 150, 0.34, 1.08);
|
||||||
fightWeight *= clamp(1 - fear / 170, fightMochi ? 0.72 : 0.42, 1.05);
|
fightWeight *= clamp(1 - fear / 170, fightMochi ? 0.72 : 0.42, 1.05);
|
||||||
fightWeight *= clamp(1.14 - Math.max(0, fearTrait - 1) * 0.16, 0.62, 1.22);
|
fightWeight *= clamp(1.14 - Math.max(0, fearTrait - 1) * 0.16, 0.62, 1.22);
|
||||||
|
if (championPair) fightWeight *= 0.35;
|
||||||
if (options.mode === "conflict") fightWeight *= 1.35;
|
if (options.mode === "conflict") fightWeight *= 1.35;
|
||||||
}
|
}
|
||||||
if (mateWeight > 0) {
|
if (mateWeight > 0) {
|
||||||
|
|
@ -82,6 +92,7 @@ function socialFightMateWeights(t, other, world, options = {}) {
|
||||||
mateWeight *= 1 + Math.min(0.70, affinity / 140);
|
mateWeight *= 1 + Math.min(0.70, affinity / 140);
|
||||||
if (loveMochi) mateWeight *= 3.15;
|
if (loveMochi) mateWeight *= 3.15;
|
||||||
if (fightMochi) mateWeight *= 0.52;
|
if (fightMochi) mateWeight *= 0.52;
|
||||||
|
if (championPair) mateWeight *= 5.0;
|
||||||
if (options.mode === "mate") mateWeight *= 1.42;
|
if (options.mode === "mate") mateWeight *= 1.42;
|
||||||
}
|
}
|
||||||
const total = Math.max(0, fightWeight) + Math.max(0, mateWeight);
|
const total = Math.max(0, fightWeight) + Math.max(0, mateWeight);
|
||||||
|
|
|
||||||
|
|
@ -713,14 +713,13 @@
|
||||||
this.world.drawListDirty = true;
|
this.world.drawListDirty = true;
|
||||||
this.world.markItemBucketsDirty?.("tarinai-death-plushie-cleanup");
|
this.world.markItemBucketsDirty?.("tarinai-death-plushie-cleanup");
|
||||||
this.world.markSpatialDirty?.("tarinai-death-plushie-cleanup");
|
this.world.markSpatialDirty?.("tarinai-death-plushie-cleanup");
|
||||||
this.world.addItem?.(new Item("trace", this.x, this.y), "tarinai-death-trace") || this.world.items.push(new Item("trace", this.x, this.y));
|
this.world.addItem?.(new Item("trace", this.x, this.y), "tarinai-death-trace");
|
||||||
this.world.addItem?.(new Item("splat", this.x, this.y), "tarinai-death-splat") || this.world.items.push(new Item("splat", this.x, this.y));
|
this.world.addItem?.(new Item("splat", this.x, this.y), "tarinai-death-splat");
|
||||||
if (deterministicChance(this.world, "death-grass-spawn", 0.55, this, finalReason)) {
|
if (deterministicChance(this.world, "death-grass-spawn", 0.55, this, finalReason)) {
|
||||||
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 });
|
const spot = this.world.findGrassPlantingSpot(this.x, this.y, { allowOriginal: false, minRadius: 26, maxRadius: 76 });
|
||||||
if (spot) {
|
if (spot) {
|
||||||
const deathGrass = new Item("grass", spot.x, spot.y);
|
const deathGrass = new Item("grass", spot.x, spot.y);
|
||||||
if (this.world.addItem) this.world.addItem(deathGrass, "tarinai-death-grass");
|
this.world.addItem?.(deathGrass, "tarinai-death-grass");
|
||||||
else this.world.items.push(deathGrass);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.world.markDead(this, finalReason);
|
this.world.markDead(this, finalReason);
|
||||||
|
|
|
||||||
3
js/ui.js
3
js/ui.js
|
|
@ -44,6 +44,9 @@ const ui = {
|
||||||
temperatureValue: document.getElementById("temperatureValue"),
|
temperatureValue: document.getElementById("temperatureValue"),
|
||||||
temperatureSlider: document.getElementById("temperatureSlider"),
|
temperatureSlider: document.getElementById("temperatureSlider"),
|
||||||
temperatureAutoToggle: document.getElementById("temperatureAutoToggle"),
|
temperatureAutoToggle: document.getElementById("temperatureAutoToggle"),
|
||||||
|
tarinaiPopulationLimitInput: document.getElementById("tarinaiPopulationLimitInput"),
|
||||||
|
objectLimitInput: document.getElementById("objectLimitInput"),
|
||||||
|
colonyLimitStatus: document.getElementById("colonyLimitStatus"),
|
||||||
statGroundType: document.getElementById("statGroundType"),
|
statGroundType: document.getElementById("statGroundType"),
|
||||||
groundTypeBtn: document.getElementById("groundTypeBtn"),
|
groundTypeBtn: document.getElementById("groundTypeBtn"),
|
||||||
colonyChart: document.getElementById("colonyChart"),
|
colonyChart: document.getElementById("colonyChart"),
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,19 @@ function bindUI() {
|
||||||
ui.temperatureSlider?.addEventListener("input", applyTemperatureSlider);
|
ui.temperatureSlider?.addEventListener("input", applyTemperatureSlider);
|
||||||
ui.temperatureSlider?.addEventListener("change", applyTemperatureSlider);
|
ui.temperatureSlider?.addEventListener("change", applyTemperatureSlider);
|
||||||
|
|
||||||
|
const applyColonyLimit = (kind) => {
|
||||||
|
if (!world) return;
|
||||||
|
const input = kind === "tarinai" ? ui.tarinaiPopulationLimitInput : ui.objectLimitInput;
|
||||||
|
if (!input) return;
|
||||||
|
const value = input.value.trim() === "" ? 0 : Number(input.value);
|
||||||
|
if (kind === "tarinai") world.setTarinaiPopulationLimit?.(value);
|
||||||
|
else world.setObjectLimit?.(value);
|
||||||
|
audio.uiClick?.();
|
||||||
|
renderStats?.();
|
||||||
|
};
|
||||||
|
ui.tarinaiPopulationLimitInput?.addEventListener("change", () => applyColonyLimit("tarinai"));
|
||||||
|
ui.objectLimitInput?.addEventListener("change", () => applyColonyLimit("object"));
|
||||||
|
|
||||||
ui.panel?.addEventListener("scroll", () => {
|
ui.panel?.addEventListener("scroll", () => {
|
||||||
const panel = ui.panel;
|
const panel = ui.panel;
|
||||||
if (!panel || !ui.panelQuickTabs) return;
|
if (!panel || !ui.panelQuickTabs) return;
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,12 @@ function renderStats() {
|
||||||
ui.temperatureSlider.setAttribute("aria-valuetext", tempLabel);
|
ui.temperatureSlider.setAttribute("aria-valuetext", tempLabel);
|
||||||
}
|
}
|
||||||
if (ui.temperatureAutoToggle) ui.temperatureAutoToggle.checked = world.temperatureAuto !== false;
|
if (ui.temperatureAutoToggle) ui.temperatureAutoToggle.checked = world.temperatureAuto !== false;
|
||||||
|
const tarinaiLimit = Math.max(0, Number(world.tarinaiPopulationLimit) || 0);
|
||||||
|
const objectLimit = Math.max(0, Number(world.objectLimit) || 0);
|
||||||
|
const objectCount = world.activeObjectCount?.() ?? (world.items || []).filter(item => item && !item.dead).length;
|
||||||
|
if (ui.tarinaiPopulationLimitInput && document.activeElement !== ui.tarinaiPopulationLimitInput) ui.tarinaiPopulationLimitInput.value = tarinaiLimit > 0 ? String(tarinaiLimit) : "";
|
||||||
|
if (ui.objectLimitInput && document.activeElement !== ui.objectLimitInput) ui.objectLimitInput.value = objectLimit > 0 ? String(objectLimit) : "";
|
||||||
|
setTextIfChanged(ui.colonyLimitStatus, "colonyLimitStatus", `たりない ${values.pop} / ${tarinaiLimit || "∞"} オブジェクト ${objectCount} / ${objectLimit || "∞"}`);
|
||||||
bindColonyMoodTooltip();
|
bindColonyMoodTooltip();
|
||||||
window.TarinaiTooltips.refresh(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood);
|
window.TarinaiTooltips.refresh(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood);
|
||||||
window.TarinaiGroundUI.update(world);
|
window.TarinaiGroundUI.update(world);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const APP_VERSION = "39.15.164";
|
const APP_VERSION = "39.15.167";
|
||||||
const APP_BUILD = "tarinai-sleep-family-tooltip-v39-15-164";
|
const APP_BUILD = "tarinai-mechanical-collision-grid-limit-audit-v39-15-166";
|
||||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ class World { constructor() {
|
||||||
this.toolSize = "medium";
|
this.toolSize = "medium";
|
||||||
this.toolSizes = {};
|
this.toolSizes = {};
|
||||||
this.toolAngles = {};
|
this.toolAngles = {};
|
||||||
|
this.tarinaiPopulationLimit = 0;
|
||||||
|
this.objectLimit = 0;
|
||||||
this.resetRuntimeCollections();
|
this.resetRuntimeCollections();
|
||||||
this.pointer = { x: 0, y: 0, inside: false, motion: 0 };
|
this.pointer = { x: 0, y: 0, inside: false, motion: 0 };
|
||||||
this.hoverGrabTarget = null;
|
this.hoverGrabTarget = null;
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
const n = Math.max(1, Math.min(28, Math.floor(count || 1)));
|
const n = Math.max(1, Math.min(28, Math.floor(count || 1)));
|
||||||
const sx = Number.isFinite(source?.x) ? source.x : (pin?.x || this.w * 0.5);
|
const sx = Number.isFinite(source?.x) ? source.x : (pin?.x || this.w * 0.5);
|
||||||
const sy = Number.isFinite(source?.y) ? source.y : (pin?.y || this.h * 0.5);
|
const sy = Number.isFinite(source?.y) ? source.y : (pin?.y || this.h * 0.5);
|
||||||
|
let spawned = 0;
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const a = -Math.PI * 0.15 + deterministicRange(this, "zunchi-burst-angle", -0.92, 0.92, source, pin, i) + (i / Math.max(1, n - 1) - 0.5) * 1.2;
|
const a = -Math.PI * 0.15 + deterministicRange(this, "zunchi-burst-angle", -0.92, 0.92, source, pin, i) + (i / Math.max(1, n - 1) - 0.5) * 1.2;
|
||||||
const it = new Item("zunchi", clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40), clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40));
|
const it = new Item("zunchi", clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40), clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40));
|
||||||
|
|
@ -378,12 +379,14 @@
|
||||||
it.vx = Math.cos(a) * deterministicRange(this, "zunchi-burst-speed-x", 260, 560, source, pin, i) + deterministicRange(this, "zunchi-burst-jitter-x", -40, 40, source, pin, i);
|
it.vx = Math.cos(a) * deterministicRange(this, "zunchi-burst-speed-x", 260, 560, source, pin, i) + deterministicRange(this, "zunchi-burst-jitter-x", -40, 40, source, pin, i);
|
||||||
it.vy = Math.sin(a) * deterministicRange(this, "zunchi-burst-speed-y", 200, 480, source, pin, i) - deterministicRange(this, "zunchi-burst-up", 30, 160, source, pin, i);
|
it.vy = Math.sin(a) * deterministicRange(this, "zunchi-burst-speed-y", 200, 480, source, pin, i) - deterministicRange(this, "zunchi-burst-up", 30, 160, source, pin, i);
|
||||||
it.stage = "fresh";
|
it.stage = "fresh";
|
||||||
this.addItem?.(it, "zunchi-burst") || this.items.push(it);
|
if (this.addItem?.(it, "zunchi-burst")) spawned += 1;
|
||||||
}
|
}
|
||||||
|
if (!spawned) return 0;
|
||||||
this.effects?.push(new Effect("zunchi_miasma", sx, sy, { size: 28, life: 0.65, color: "rgba(74,91,50,0.48)" }));
|
this.effects?.push(new Effect("zunchi_miasma", sx, sy, { size: 28, life: 0.65, color: "rgba(74,91,50,0.48)" }));
|
||||||
this.log?.(`${source?.name || "\u305f\u308a\u306a\u3044"}\u304b\u3089\u6e9c\u307e\u3063\u3066\u3044\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3093\u3060\u3002`, "accident", { participants: source?.name ? [source] : [] });
|
this.log?.(`${source?.name || "\u305f\u308a\u306a\u3044"}\u304b\u3089\u6e9c\u307e\u3063\u3066\u3044\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3093\u3060\u3002`, "accident", { participants: source?.name ? [source] : [] });
|
||||||
audio.place?.("zunchi");
|
audio.place?.("zunchi");
|
||||||
this.ensureSpatial?.("zunchi-burst");
|
this.ensureSpatial?.("zunchi-burst");
|
||||||
|
return spawned;
|
||||||
},
|
},
|
||||||
|
|
||||||
spawnZunchi(x, y, source = null) {
|
spawnZunchi(x, y, source = null) {
|
||||||
|
|
@ -392,8 +395,10 @@
|
||||||
it.stage = "fresh";
|
it.stage = "fresh";
|
||||||
it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0);
|
it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0);
|
||||||
it.eatProtectedUntil = (this.time || 0) + 90;
|
it.eatProtectedUntil = (this.time || 0) + 90;
|
||||||
this.addItem?.(it, "zunchi-spawn") || this.items.push(it);
|
const added = this.addItem?.(it, "zunchi-spawn");
|
||||||
|
if (!added) return null;
|
||||||
audio.place?.("zunchi");
|
audio.place?.("zunchi");
|
||||||
|
return added;
|
||||||
},
|
},
|
||||||
|
|
||||||
spawnBubble(x, y, text, color = "rgba(42,36,29,0.78)") {
|
spawnBubble(x, y, text, color = "rgba(42,36,29,0.78)") {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
if (!World) throw new Error("World is not available for mixin: world_environment.js");
|
if (!World) throw new Error("World is not available for mixin: world_environment.js");
|
||||||
|
|
||||||
const Geometry = global.TarinaiGeometry;
|
const Geometry = global.TarinaiGeometry;
|
||||||
|
const BOUNCE_FENCE_RESTITUTION = 2.40;
|
||||||
|
const BOUNCE_FENCE_MIN_SPEED = 520;
|
||||||
|
const BOUNCE_FENCE_MAX_SPEED = 1180;
|
||||||
|
|
||||||
function angleForItemLocal(it) {
|
function angleForItemLocal(it) {
|
||||||
return itemAngleFor(it);
|
return itemAngleFor(it);
|
||||||
|
|
@ -274,7 +277,7 @@
|
||||||
cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
|
cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
|
||||||
vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)),
|
vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)),
|
||||||
bounce, gate, glassWall, gateOpen: Boolean(it.gateOpen),
|
bounce, gate, glassWall, gateOpen: Boolean(it.gateOpen),
|
||||||
restitution: bounce ? 1.08 : undefined, minBounceSpeed: bounce ? 260 : undefined
|
restitution: bounce ? BOUNCE_FENCE_RESTITUTION : undefined, minBounceSpeed: bounce ? BOUNCE_FENCE_MIN_SPEED : undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -666,7 +669,7 @@
|
||||||
t.lastSolidObstacleCollisionSource = r.item || null;
|
t.lastSolidObstacleCollisionSource = r.item || null;
|
||||||
t.lastSolidObstacleCollisionAt = this.time || 0;
|
t.lastSolidObstacleCollisionAt = this.time || 0;
|
||||||
if (r.bounce) {
|
if (r.bounce) {
|
||||||
const restitution = Number.isFinite(r.restitution) ? r.restitution : 1.08;
|
const restitution = Number.isFinite(r.restitution) ? r.restitution : BOUNCE_FENCE_RESTITUTION;
|
||||||
const vx = t.vx || 0;
|
const vx = t.vx || 0;
|
||||||
const vy = t.vy || 0;
|
const vy = t.vy || 0;
|
||||||
const toward = vx * nx + vy * ny;
|
const toward = vx * nx + vy * ny;
|
||||||
|
|
@ -684,13 +687,13 @@
|
||||||
const now = this.time || 0;
|
const now = this.time || 0;
|
||||||
if (currentOut < minBounceSpeed && ((t.lastBounceFenceImpulseAt || -999) + 0.16 <= now || toward < 0)) {
|
if (currentOut < minBounceSpeed && ((t.lastBounceFenceImpulseAt || -999) + 0.16 <= now || toward < 0)) {
|
||||||
const add = minBounceSpeed - currentOut;
|
const add = minBounceSpeed - currentOut;
|
||||||
t.impulseVx = clamp((t.impulseVx || 0) + nx * add, -520, 520);
|
t.impulseVx = clamp((t.impulseVx || 0) + nx * add, -BOUNCE_FENCE_MAX_SPEED, BOUNCE_FENCE_MAX_SPEED);
|
||||||
t.impulseVy = clamp((t.impulseVy || 0) + ny * add, -520, 520);
|
t.impulseVy = clamp((t.impulseVy || 0) + ny * add, -BOUNCE_FENCE_MAX_SPEED, BOUNCE_FENCE_MAX_SPEED);
|
||||||
t.lastBounceFenceImpulseAt = now;
|
t.lastBounceFenceImpulseAt = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.vx = clamp(t.vx || 0, -520, 520);
|
t.vx = clamp(t.vx || 0, -BOUNCE_FENCE_MAX_SPEED, BOUNCE_FENCE_MAX_SPEED);
|
||||||
t.vy = clamp(t.vy || 0, -520, 520);
|
t.vy = clamp(t.vy || 0, -BOUNCE_FENCE_MAX_SPEED, BOUNCE_FENCE_MAX_SPEED);
|
||||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.18);
|
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.18);
|
||||||
if (t.bubble && (this.time || 0) >= (t.lastBounceFenceBubbleAt || -999) + 0.55) {
|
if (t.bubble && (this.time || 0) >= (t.lastBounceFenceBubbleAt || -999) + 0.55) {
|
||||||
t.lastBounceFenceBubbleAt = this.time || 0;
|
t.lastBounceFenceBubbleAt = this.time || 0;
|
||||||
|
|
|
||||||
|
|
@ -494,6 +494,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
addTarinai(opts = {}) {
|
addTarinai(opts = {}) {
|
||||||
|
if (!(this.canAddTarinai?.(1) ?? true)) return null;
|
||||||
opts = global.TarinaiSeedFactory.prepareAddOptions(this, opts) || opts;
|
opts = global.TarinaiSeedFactory.prepareAddOptions(this, opts) || opts;
|
||||||
const seedParents = Array.isArray(opts.seedParents) ? opts.seedParents.filter(p => p && typeof p === "object") : [];
|
const seedParents = Array.isArray(opts.seedParents) ? opts.seedParents.filter(p => p && typeof p === "object") : [];
|
||||||
if (!opts.genetics && seedParents.length >= 2 && typeof inheritedGenetics === "function") {
|
if (!opts.genetics && seedParents.length >= 2 && typeof inheritedGenetics === "function") {
|
||||||
|
|
@ -611,6 +612,7 @@
|
||||||
|
|
||||||
startBirthRitual(a, b) {
|
startBirthRitual(a, b) {
|
||||||
if (!a || !b || a.dead || b.dead) return false;
|
if (!a || !b || a.dead || b.dead) return false;
|
||||||
|
if (!(this.canAddTarinai?.(1) ?? true)) return false;
|
||||||
if (!this.tarinai?.includes?.(a) || !this.tarinai?.includes?.(b)) return false;
|
if (!this.tarinai?.includes?.(a) || !this.tarinai?.includes?.(b)) return false;
|
||||||
if (!!a.isZunchiSlave !== !!b.isZunchiSlave) return false;
|
if (!!a.isZunchiSlave !== !!b.isZunchiSlave) return false;
|
||||||
if (this.areParentChild(a, b)) return false;
|
if (this.areParentChild(a, b)) return false;
|
||||||
|
|
@ -657,8 +659,10 @@
|
||||||
const bText = buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause });
|
const bText = buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause });
|
||||||
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
|
setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b });
|
||||||
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a });
|
setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a });
|
||||||
a.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
const championPair = Boolean(a.isTarinaiChampion && b.isTarinaiChampion);
|
||||||
b.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10);
|
const reproductionCooldown = championPair ? Math.max(8, CONFIG.reproductionCooldown * 0.45) + rand(1, 4) : CONFIG.reproductionCooldown + rand(2, 10);
|
||||||
|
a.reproductionTimer = reproductionCooldown;
|
||||||
|
b.reproductionTimer = reproductionCooldown;
|
||||||
a.surpriseTimer = Math.max(a.surpriseTimer, 0.35);
|
a.surpriseTimer = Math.max(a.surpriseTimer, 0.35);
|
||||||
b.surpriseTimer = Math.max(b.surpriseTimer, 0.35);
|
b.surpriseTimer = Math.max(b.surpriseTimer, 0.35);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -816,6 +820,7 @@
|
||||||
sizeItemMode: "",
|
sizeItemMode: "",
|
||||||
lifeItemMode: "",
|
lifeItemMode: "",
|
||||||
});
|
});
|
||||||
|
if (!child) return null;
|
||||||
if (zunchiSlaveChild) child.becomeZunchiSlave?.({ birth: true });
|
if (zunchiSlaveChild) child.becomeZunchiSlave?.({ birth: true });
|
||||||
this.maxGeneration = Math.max(this.maxGeneration, child.generation);
|
this.maxGeneration = Math.max(this.maxGeneration, child.generation);
|
||||||
a.hasPaired = true;
|
a.hasPaired = true;
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@
|
||||||
<button id="rotatorClear" type="button">\ud83d\uddd1 \u5168\u6d88\u3057</button>
|
<button id="rotatorClear" type="button">\ud83d\uddd1 \u5168\u6d88\u3057</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="rotatorCanvas" width="520" height="360" aria-label="\u56de\u8ee2\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
<canvas id="rotatorCanvas" width="520" height="360" aria-label="\u56de\u8ee2\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
||||||
<p class="rotator-editor-hint">\u4e2d\u592e\u304c\u56de\u8ee2\u8ef8\u3002\u76f4\u7dda\u30fb\u81ea\u7531\u63cf\u753b\u30fb\u6d88\u3057\u30b4\u30e0\u3092\u9078\u3073\u3001\u30c9\u30e9\u30c3\u30b0\u3067\u8f2a\u90ed\u3092\u7de8\u96c6\u3002</p>
|
<p class="rotator-editor-hint">\u4e2d\u592e\u304c\u56de\u8ee2\u8ef8\u3002\u8f2a\u90ed\u306f20px\u9593\u9694\u306e\u30b0\u30ea\u30c3\u30c9\u683c\u5b50\u70b9\u306b\u30b9\u30ca\u30c3\u30d7\u3057\u307e\u3059\u3002</p>
|
||||||
<div class="rotator-editor-actions">
|
<div class="rotator-editor-actions">
|
||||||
<button id="rotatorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
<button id="rotatorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||||
<button id="rotatorApply" class="btn primary" type="button">\u53cd\u6620</button>
|
<button id="rotatorApply" class="btn primary" type="button">\u53cd\u6620</button>
|
||||||
|
|
@ -353,11 +353,11 @@
|
||||||
function phyScalar(item, key, fallback = 0) { return physicsApi()?.scalar?.(item, key, fallback) ?? fallback; }
|
function phyScalar(item, key, fallback = 0) { return physicsApi()?.scalar?.(item, key, fallback) ?? fallback; }
|
||||||
function phySet(item, key, value, reason = "editor") { return physicsApi()?.setScalar?.(item, key, value, reason) || false; }
|
function phySet(item, key, value, reason = "editor") { return physicsApi()?.setScalar?.(item, key, value, reason) || false; }
|
||||||
function cloneSegmentsForRotator(item) {
|
function cloneSegmentsForRotator(item) {
|
||||||
const fallback = item?.type === "poison_block" ? [[-82, -28, 82, -28], [82, -28, 82, 28], [82, 28, -82, 28], [-82, 28, -82, -28]] : [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
const fallback = item?.type === "poison_block" ? [[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]] : [[-78, 0, 78, 0], [0, -52, 0, 52]];
|
||||||
const src = physicsApi()?.segments?.(item) || fallback;
|
const src = physicsApi()?.segments?.(item) || fallback;
|
||||||
return src.filter(seg => Array.isArray(seg) && seg.length >= 4).map(seg => [Number(seg[0]) || 0, Number(seg[1]) || 0, Number(seg[2]) || 0, Number(seg[3]) || 0]);
|
return src.filter(seg => Array.isArray(seg) && seg.length >= 4).map(seg => [Number(seg[0]) || 0, Number(seg[1]) || 0, Number(seg[2]) || 0, Number(seg[3]) || 0]);
|
||||||
}
|
}
|
||||||
function physicsShapeExtent(item, segments = cloneSegmentsForRotator(item), thick = phyScalar(item, "thickness", item?.type === "poison_block" ? 14 : 12)) {
|
function physicsShapeExtent(item, segments = cloneSegmentsForRotator(item), thick = phyScalar(item, "thickness", item?.type === "poison_block" ? 11 : 12)) {
|
||||||
return Math.max(...segments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + Math.max(4, thick) + 8;
|
return Math.max(...segments.flatMap(seg => [Math.hypot(seg[0], seg[1]), Math.hypot(seg[2], seg[3])]), 64) + Math.max(4, thick) + 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,7 +400,7 @@
|
||||||
});
|
});
|
||||||
if (!controller) return false;
|
if (!controller) return false;
|
||||||
const deg = Math.round((Number(phyScalar(item, "motorSpeed", 0) || 0) || 0) * 180 / Math.PI);
|
const deg = Math.round((Number(phyScalar(item, "motorSpeed", 0) || 0) || 0) * 180 / Math.PI);
|
||||||
const initialThickness = Math.max(4, Math.min(34, Number(phyScalar(item, "thickness", isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12)));
|
const initialThickness = Math.max(4, Math.min(34, Number(phyScalar(item, "thickness", isPoisonBlock ? 11 : 12)) || (isPoisonBlock ? 11 : 12)));
|
||||||
if (titleEl) titleEl.textContent = isPoisonBlock ? "\u6bd2\u30d6\u30ed\u30c3\u30af" : "\u56de\u8ee2\u4f53";
|
if (titleEl) titleEl.textContent = isPoisonBlock ? "\u6bd2\u30d6\u30ed\u30c3\u30af" : "\u56de\u8ee2\u4f53";
|
||||||
const speedLabel = speed?.closest?.("label");
|
const speedLabel = speed?.closest?.("label");
|
||||||
const poweredLabel = poweredInput?.closest?.("label");
|
const poweredLabel = poweredInput?.closest?.("label");
|
||||||
|
|
@ -424,7 +424,7 @@
|
||||||
controller.cleanSegments();
|
controller.cleanSegments();
|
||||||
const editedSegments = controller.getSegments();
|
const editedSegments = controller.getSegments();
|
||||||
physicsApi()?.setSegments?.(item, editedSegments, isPoisonBlock ? "poison-shape-edit" : "rotator-shape-edit");
|
physicsApi()?.setSegments?.(item, editedSegments, isPoisonBlock ? "poison-shape-edit" : "rotator-shape-edit");
|
||||||
phySet(item, "thickness", Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || (isPoisonBlock ? 14 : 12)) || (isPoisonBlock ? 14 : 12))), "editor-thickness");
|
phySet(item, "thickness", Math.max(4, Math.min(34, Number(thickNum?.value || thick?.value || (isPoisonBlock ? 11 : 12)) || (isPoisonBlock ? 11 : 12))), "editor-thickness");
|
||||||
phySet(item, "solid", poisonCollisionInput ? Boolean(poisonCollisionInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid");
|
phySet(item, "solid", poisonCollisionInput ? Boolean(poisonCollisionInput.checked) : phyScalar(item, "solid", true) !== false, "editor-solid");
|
||||||
if (isPoisonBlock) {
|
if (isPoisonBlock) {
|
||||||
phySet(item, "damage", Math.max(1, Number(phyScalar(item, "damage", 7) || 7) || 7), "editor-damage");
|
phySet(item, "damage", Math.max(1, Number(phyScalar(item, "damage", 7) || 7) || 7), "editor-damage");
|
||||||
|
|
@ -478,7 +478,7 @@
|
||||||
<button id="reciprocatorClear" type="button">\ud83d\uddd1 \u5168\u6d88\u3057</button>
|
<button id="reciprocatorClear" type="button">\ud83d\uddd1 \u5168\u6d88\u3057</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="\u5f80\u5fa9\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
<canvas id="reciprocatorCanvas" width="520" height="360" aria-label="\u5f80\u5fa9\u4f53\u306e\u8f2a\u90ed\u7de8\u96c6"></canvas>
|
||||||
<p class="rotator-editor-hint">\u4e2d\u592e\u306e\u8f2a\u90ed\u5168\u4f53\u304c\u3001\u8a2d\u5b9a\u3057\u305f\u65b9\u5411\u3078\u5f80\u5fa9\u79fb\u52d5\u3057\u307e\u3059\u3002\u52d5\u529bOFF\u3067\u306f\u3001\u305f\u308a\u306a\u3044\u30fb\u305a\u3093\u3061\u30fb\u4ed6\u306e\u6a5f\u69cb\u304b\u3089\u62bc\u3055\u308c\u305f\u3068\u304d\u3060\u3051\u52d5\u304d\u307e\u3059\u3002</p>
|
<p class="rotator-editor-hint">\u8f2a\u90ed\u306f20px\u9593\u9694\u306e\u30b0\u30ea\u30c3\u30c9\u683c\u5b50\u70b9\u306b\u30b9\u30ca\u30c3\u30d7\u3057\u307e\u3059\u3002\u52d5\u529bOFF\u3067\u306f\u3001\u305f\u308a\u306a\u3044\u30fb\u305a\u3093\u3061\u30fb\u4ed6\u306e\u6a5f\u69cb\u304b\u3089\u62bc\u3055\u308c\u305f\u3068\u304d\u3060\u3051\u52d5\u304d\u307e\u3059\u3002</p>
|
||||||
<div class="rotator-editor-actions">
|
<div class="rotator-editor-actions">
|
||||||
<button id="reciprocatorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
<button id="reciprocatorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||||
<button id="reciprocatorApply" class="btn primary" type="button">\u53cd\u6620</button>
|
<button id="reciprocatorApply" class="btn primary" type="button">\u53cd\u6620</button>
|
||||||
|
|
@ -672,7 +672,7 @@
|
||||||
data.physics = { solid: phyScalar(item, "solid", true) !== false, motorOn: phyScalar(item, "motorOn", true) !== false, motorSpeed: Number(phyScalar(item, "motorSpeed", 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
|
data.physics = { solid: phyScalar(item, "solid", true) !== false, motorOn: phyScalar(item, "motorOn", true) !== false, motorSpeed: Number(phyScalar(item, "motorSpeed", 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
|
||||||
}
|
}
|
||||||
if (item.type === "poison_block") {
|
if (item.type === "poison_block") {
|
||||||
data.physics = { solid: phyScalar(item, "solid", true) !== false, damage: Number(phyScalar(item, "damage", 7) || 7), thickness: Number(phyScalar(item, "thickness", 14) || 14), segments: cloneSegmentsForRotator(item) };
|
data.physics = { solid: phyScalar(item, "solid", true) !== false, damage: Number(phyScalar(item, "damage", 7) || 7), thickness: Number(phyScalar(item, "thickness", 11) || 11), segments: cloneSegmentsForRotator(item) };
|
||||||
}
|
}
|
||||||
if (item.type === "reciprocator") {
|
if (item.type === "reciprocator") {
|
||||||
data.physics = { solid: phyScalar(item, "solid", true) !== false, railOn: phyScalar(item, "railOn", true) !== false, railMotorSpeed: Number(phyScalar(item, "railMotorSpeed", 92) || 92), railTravel: Number(phyScalar(item, "railTravel", 150) || 150), railPhase: Number(phyScalar(item, "railPhase", 0) || 0), railDir: Number(phyScalar(item, "railDir", 1) || 1), railAxis: Number(phyScalar(item, "railAxis", item.angle || 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
|
data.physics = { solid: phyScalar(item, "solid", true) !== false, railOn: phyScalar(item, "railOn", true) !== false, railMotorSpeed: Number(phyScalar(item, "railMotorSpeed", 92) || 92), railTravel: Number(phyScalar(item, "railTravel", 150) || 150), railPhase: Number(phyScalar(item, "railPhase", 0) || 0), railDir: Number(phyScalar(item, "railDir", 1) || 1), railAxis: Number(phyScalar(item, "railAxis", item.angle || 0) || 0), thickness: Number(phyScalar(item, "thickness", 12) || 12), segments: cloneSegmentsForRotator(item) };
|
||||||
|
|
@ -743,7 +743,7 @@
|
||||||
phySet(item, "solid", ph.solid !== false, "copy");
|
phySet(item, "solid", ph.solid !== false, "copy");
|
||||||
phySet(item, "damage", Math.max(1, Number(ph.damage || 7) || 7), "copy");
|
phySet(item, "damage", Math.max(1, Number(ph.damage || 7) || 7), "copy");
|
||||||
phySet(item, "spin", 0, "copy");
|
phySet(item, "spin", 0, "copy");
|
||||||
phySet(item, "thickness", Math.max(4, Math.min(34, Number(ph.thickness || 14) || 14)), "copy");
|
phySet(item, "thickness", Math.max(4, Math.min(34, Number(ph.thickness || 11) || 11)), "copy");
|
||||||
physicsApi()?.setSegments?.(item, Array.isArray(ph.segments) ? ph.segments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item), "copy");
|
physicsApi()?.setSegments?.(item, Array.isArray(ph.segments) ? ph.segments.map(seg => seg.slice(0, 4)) : cloneSegmentsForRotator(item), "copy");
|
||||||
const extent = physicsShapeExtent(item);
|
const extent = physicsShapeExtent(item);
|
||||||
item.r = Math.max(item.r || 64, Math.min(460, extent));
|
item.r = Math.max(item.r || 64, Math.min(460, extent));
|
||||||
|
|
@ -1199,6 +1199,10 @@
|
||||||
|
|
||||||
placeItem(item, dropped = true) {
|
placeItem(item, dropped = true) {
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
if (!(this.canAddObjects?.(1) ?? true)) {
|
||||||
|
showToast(`オブジェクト総数は${this.objectLimit}個までです。`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (item.type === "grass" && !this.canAddGrass?.(1)) {
|
if (item.type === "grass" && !this.canAddGrass?.(1)) {
|
||||||
showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -1220,7 +1224,8 @@
|
||||||
if (removedSoft) this.updateItemCounts?.("placement-soft-overlap-clear");
|
if (removedSoft) this.updateItemCounts?.("placement-soft-overlap-clear");
|
||||||
const placed = this.addItem?.(item, `place:${item.type}`);
|
const placed = this.addItem?.(item, `place:${item.type}`);
|
||||||
if (!placed) {
|
if (!placed) {
|
||||||
if (item.type === "grass") showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
if (!(this.canAddObjects?.(1) ?? true)) showToast(`オブジェクト総数は${this.objectLimit}個までです。`);
|
||||||
|
else if (item.type === "grass") showToast(`\u8349\u306f\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3067\u306f${this.grassLimit?.() ?? CONFIG.grassLimit ?? 99}\u672c\u307e\u3067\u3067\u3059\u3002`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
this.emit("tool:placed", { item, type: item.type, dropped, tool: item.type });
|
||||||
|
|
@ -1357,6 +1362,7 @@
|
||||||
if (tool === "new") {
|
if (tool === "new") {
|
||||||
const spot = this.edgeSpawnPoint(x, y, 42, true);
|
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 });
|
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, generation: 1, entryTimer: 2.0 });
|
||||||
|
if (!t) { showToast(`たりない個体数は${this.tarinaiPopulationLimit}匹までです。`); return; }
|
||||||
t.wanderAngle = spot.angle;
|
t.wanderAngle = spot.angle;
|
||||||
this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
|
this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -215,9 +215,7 @@ const RESET_PRESETS = Object.freeze({
|
||||||
item.y = spot.y;
|
item.y = spot.y;
|
||||||
syncPresetMechanicalPose(item);
|
syncPresetMechanicalPose(item);
|
||||||
}
|
}
|
||||||
if (worldRef.addItem) worldRef.addItem(item, opts.reason || `preset-${type}`);
|
return worldRef.addItem?.(item, opts.reason || `preset-${type}`) || null;
|
||||||
else worldRef.items.push(item);
|
|
||||||
return item;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPresetGrass(worldRef, count, zone = null) {
|
function addPresetGrass(worldRef, count, zone = null) {
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,13 @@
|
||||||
|
|
||||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||||
beginFramePerformanceBudgets() {
|
beginFramePerformanceBudgets() {
|
||||||
|
const count = Math.max(1, (this.tarinai || []).length || 1);
|
||||||
|
const mobile = Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||||
|
const aiCap = mobile ? 8 : 12;
|
||||||
|
const envCap = mobile ? 4 : 6;
|
||||||
this.workBudget = {
|
this.workBudget = {
|
||||||
ai: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 2)),
|
ai: Math.max(1, Math.min(aiCap, Math.ceil(count / 2))),
|
||||||
env: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 3)),
|
env: Math.max(1, Math.min(envCap, Math.ceil(count / 3))),
|
||||||
aiUsed: 0,
|
aiUsed: 0,
|
||||||
envUsed: 0,
|
envUsed: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -286,8 +290,46 @@
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
normalizeColonyLimit(value = 0) {
|
||||||
|
const n = Math.floor(Number(value) || 0);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
activeTarinaiCount() {
|
||||||
|
let count = 0;
|
||||||
|
for (const t of this.tarinai || []) if (t && !t.dead) count += 1;
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
|
||||||
|
activeObjectCount() {
|
||||||
|
let count = 0;
|
||||||
|
for (const item of this.items || []) if (item && !item.dead) count += 1;
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
|
||||||
|
canAddTarinai(count = 1) {
|
||||||
|
const limit = this.normalizeColonyLimit(this.tarinaiPopulationLimit);
|
||||||
|
return limit <= 0 || this.activeTarinaiCount() + Math.max(0, Math.floor(Number(count) || 0)) <= limit;
|
||||||
|
},
|
||||||
|
|
||||||
|
canAddObjects(count = 1) {
|
||||||
|
const limit = this.normalizeColonyLimit(this.objectLimit);
|
||||||
|
return limit <= 0 || this.activeObjectCount() + Math.max(0, Math.floor(Number(count) || 0)) <= limit;
|
||||||
|
},
|
||||||
|
|
||||||
|
setTarinaiPopulationLimit(value = 0) {
|
||||||
|
this.tarinaiPopulationLimit = this.normalizeColonyLimit(value);
|
||||||
|
return this.tarinaiPopulationLimit;
|
||||||
|
},
|
||||||
|
|
||||||
|
setObjectLimit(value = 0) {
|
||||||
|
this.objectLimit = this.normalizeColonyLimit(value);
|
||||||
|
return this.objectLimit;
|
||||||
|
},
|
||||||
|
|
||||||
addItem(item, reason = "add-item", opts = {}) {
|
addItem(item, reason = "add-item", opts = {}) {
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
if (!this.canAddObjects(1)) return null;
|
||||||
if (item.type === "grass" && !this.canAddGrass?.(1)) return null;
|
if (item.type === "grass" && !this.canAddGrass?.(1)) return null;
|
||||||
item.world = this;
|
item.world = this;
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,42 @@ if (context.currentTarinaiBehavior(t) !== null) throw new Error('behavior not cl
|
||||||
ok("behavior state smoke test passed")
|
ok("behavior state smoke test passed")
|
||||||
|
|
||||||
|
|
||||||
|
def need_action_runtime_bridge_smoke() -> None:
|
||||||
|
items_source = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||||
|
core_source = read_text(ROOT / "js/tarinai_needs_core.js")
|
||||||
|
forced_source = read_text(ROOT / "js/tarinai_forced_behavior.js")
|
||||||
|
js = f"""
|
||||||
|
const vm = require('vm');
|
||||||
|
const context = {{ console, globalThis: null }};
|
||||||
|
context.globalThis = context;
|
||||||
|
context.window = context;
|
||||||
|
context.Tarinai = class Tarinai {{}};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext({items_source!r}, context, {{ filename: 'tarinai_needs_items.js' }});
|
||||||
|
if (!context.TarinaiNeedsRuntime) throw new Error('TarinaiNeedsRuntime missing');
|
||||||
|
if (typeof context.TarinaiNeedsRuntime.startNeedAction !== 'function') throw new Error('startNeedAction bridge missing');
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||||
|
f.write(js)
|
||||||
|
tmp = Path(f.name)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||||
|
finally:
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.stderr.write(result.stdout)
|
||||||
|
sys.stderr.write(result.stderr)
|
||||||
|
fail("need-action runtime bridge smoke test failed")
|
||||||
|
qualified_call = "globalThis.TarinaiNeedsRuntime?.startNeedAction?.("
|
||||||
|
if core_source.count(qualified_call) != 2:
|
||||||
|
fail("tarinai_needs_core.js must route both emergency starts through TarinaiNeedsRuntime")
|
||||||
|
if forced_source.count(qualified_call) != 1:
|
||||||
|
fail("tarinai_forced_behavior.js must route forced starts through TarinaiNeedsRuntime")
|
||||||
|
if re.search(r"(?<![.\w])startNeedAction\s*\(", core_source + "\n" + forced_source):
|
||||||
|
fail("cross-file bare startNeedAction call remains")
|
||||||
|
ok("need-action runtime bridge is exported and all cross-file calls are qualified")
|
||||||
|
|
||||||
|
|
||||||
def check_legacy_behavior_refs() -> None:
|
def check_legacy_behavior_refs() -> None:
|
||||||
tokens = re.compile(r"\b(activeBehavior|currentAction|activeForcedBehavior|intent)\b")
|
tokens = re.compile(r"\b(activeBehavior|currentAction|activeForcedBehavior|intent)\b")
|
||||||
offenders = []
|
offenders = []
|
||||||
|
|
@ -1216,6 +1252,383 @@ if (grass.normalized) throw new Error('grass should not be normalized by per-ite
|
||||||
fail("Item lifecycle pipeline smoke test failed")
|
fail("Item lifecycle pipeline smoke test failed")
|
||||||
ok("Item lifecycle pipeline boundary present")
|
ok("Item lifecycle pipeline boundary present")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def mechanical_collision_and_editor_smoke() -> None:
|
||||||
|
footprints = read_text(ROOT / "js/collision_footprint_system.js")
|
||||||
|
editor_source = read_text(ROOT / "js/physics_shape_editor_system.js")
|
||||||
|
placement_source = read_text(ROOT / "js/world_placement_log.js")
|
||||||
|
for token in ("rectCorners,", "rectAxes,", "projectPoints,", "aabbOverlap,", "rectOverlapInfo,", "rectsOverlap,"):
|
||||||
|
if token not in footprints:
|
||||||
|
fail(f"mechanical SAT primitive is not exported: {token}")
|
||||||
|
if "const DEFAULT_GRID_SIZE = 20;" not in editor_source:
|
||||||
|
fail("mechanical shape editor grid must use coarse 20px snapping")
|
||||||
|
if editor_source.count("snapPoint(") < 2 or "gridSize: DEFAULT_GRID_SIZE" not in editor_source:
|
||||||
|
fail("mechanical shape editor does not route pointer coordinates through grid snapping")
|
||||||
|
if placement_source.count("20px\\u9593\\u9694") < 2:
|
||||||
|
fail("mechanical editor hints do not disclose 20px grid snapping")
|
||||||
|
|
||||||
|
js = f"""
|
||||||
|
const vm = require('vm');
|
||||||
|
const fs = require('fs');
|
||||||
|
const context = {{ console, globalThis: null }};
|
||||||
|
context.globalThis = context;
|
||||||
|
context.window = context;
|
||||||
|
context.TarinaiPerf = {{ begin() {{ return null; }} }};
|
||||||
|
context.TarinaiConstraintSystem = {{ updateWorld() {{ return 0; }} }};
|
||||||
|
context.normalizedItemAngle = angle => angle;
|
||||||
|
vm.createContext(context);
|
||||||
|
for (const path of {repr([str(ROOT / rel) for rel in [
|
||||||
|
'js/math.js', 'js/geometry_helpers.js', 'js/collision_footprint_system.js',
|
||||||
|
'js/physics_world_system.js', 'js/mechanical_system.js', 'js/physics_shape_editor_system.js'
|
||||||
|
]])}) vm.runInContext(fs.readFileSync(path, 'utf8'), context, {{ filename: path }});
|
||||||
|
|
||||||
|
const fp = context.TarinaiCollisionFootprints;
|
||||||
|
for (const key of ['rectCorners', 'rectAxes', 'projectPoints', 'aabbOverlap', 'rectOverlapInfo', 'rectsOverlap']) {{
|
||||||
|
if (typeof fp[key] !== 'function') throw new Error(`missing footprint primitive: ${{key}}`);
|
||||||
|
}}
|
||||||
|
|
||||||
|
const editor = context.TarinaiPhysicsShapeEditorSystem;
|
||||||
|
if (editor.gridSize !== 20) throw new Error('editor grid size mismatch');
|
||||||
|
const snapped = editor.snapPoint({{ x: 13, y: 31 }});
|
||||||
|
if (snapped.x !== 20 || snapped.y !== 40) throw new Error('pointer did not snap to grid');
|
||||||
|
const cleaned = editor.cleanSegments([[13, 31, 91, 44], [-52, -20, 52, -20]], {{ fallback: [-80, 0, 80, 0] }});
|
||||||
|
if (!cleaned.length || cleaned.some(seg => seg.some(value => Math.abs(value % 20) > 1e-9))) throw new Error('segment endpoints are not grid aligned');
|
||||||
|
if (cleaned.some(seg => Math.hypot(seg[2] - seg[0], seg[3] - seg[1]) < 20)) throw new Error('editor retained sub-grid segments');
|
||||||
|
|
||||||
|
let serial = 0;
|
||||||
|
function makeItem(type, x, y) {{
|
||||||
|
return {{ id: `m${{++serial}}`, type, x, y, angle: 0, r: 64, amount: 999, dead: false }};
|
||||||
|
}}
|
||||||
|
function activate(item) {{
|
||||||
|
const api = context.TarinaiPhysicsBodySystem;
|
||||||
|
api.setSegments(item, [[-80, 0, 80, 0]], 'test-shape');
|
||||||
|
api.setScalar(item, 'solid', true, 'test-solid');
|
||||||
|
if (item.type === 'rotator') {{ api.setScalar(item, 'motorOn', true, 'test'); api.setScalar(item, 'motorSpeed', 1.4, 'test'); }}
|
||||||
|
if (item.type === 'reciprocator') {{ api.setScalar(item, 'railOn', true, 'test'); api.setScalar(item, 'railAxis', 0, 'test'); api.setScalar(item, 'railMotorSpeed', 80, 'test'); }}
|
||||||
|
if (item.type === 'poison_block') {{ api.setScalar(item, 'xv', 24, 'test'); api.setScalar(item, 'awakeUntil', 10, 'test'); }}
|
||||||
|
}}
|
||||||
|
function pairResult(typeA, typeB, disableB = false) {{
|
||||||
|
const a = makeItem(typeA, 120, 120);
|
||||||
|
const b = makeItem(typeB, 120, 128);
|
||||||
|
const items = [a, b];
|
||||||
|
const world = {{
|
||||||
|
items, tarinai: [], time: 1, frameCount: 1, w: 900, h: 700, drawListDirty: false,
|
||||||
|
itemCounts: {{ rotator: 0, poison_block: 0, reciprocator: 0 }},
|
||||||
|
ensureItemBuckets() {{}}, markSpatialDirty() {{}}, markItemBucketsDirty() {{}},
|
||||||
|
nearbyObstacles() {{ return items; }}, nearbyItems() {{ return items; }},
|
||||||
|
itemsOfType(type) {{ return items.filter(item => item.type === type && !item.dead); }},
|
||||||
|
}};
|
||||||
|
for (const item of items) {{ item.world = world; world.itemCounts[item.type] += 1; context.TarinaiPhysicsBodySystem.ensureBody(item, world); activate(item); }}
|
||||||
|
if (disableB) context.TarinaiPhysicsBodySystem.setScalar(b, 'solid', false, 'test-disabled');
|
||||||
|
const solved = context.TarinaiMechanicalSystem.resolveMechanicalPairs(world, 0.016, {{ maxPairs: 40 }});
|
||||||
|
return {{ solved, a, b }};
|
||||||
|
}}
|
||||||
|
for (const [a, b] of [['rotator', 'rotator'], ['rotator', 'reciprocator'], ['rotator', 'poison_block'], ['reciprocator', 'reciprocator'], ['reciprocator', 'poison_block'], ['poison_block', 'poison_block']]) {{
|
||||||
|
const result = pairResult(a, b, false);
|
||||||
|
if (!(result.solved > 0)) throw new Error(`${{a}}/${{b}} collision-on pair did not interact`);
|
||||||
|
const disabled = pairResult(a, b, true);
|
||||||
|
if (disabled.solved !== 0) throw new Error(`${{a}}/${{b}} collision-off pair still interacted`);
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||||
|
f.write(js)
|
||||||
|
tmp = Path(f.name)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||||
|
finally:
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.stderr.write(result.stdout)
|
||||||
|
sys.stderr.write(result.stderr)
|
||||||
|
fail("mechanical collision/editor smoke test failed")
|
||||||
|
ok("collision-enabled mechanical pairs interact and editor geometry snaps to the 20px lattice")
|
||||||
|
|
||||||
|
def colony_limits_and_defaults_smoke() -> None:
|
||||||
|
tool_defs = read_text(ROOT / "js/item_tool_definitions.js")
|
||||||
|
initializer = read_text(ROOT / "js/item_type_initializers.js")
|
||||||
|
world_budget = read_text(ROOT / "js/world_spatial_budget.js")
|
||||||
|
world_family = read_text(ROOT / "js/world_family_social.js")
|
||||||
|
needs = read_text(ROOT / "js/tarinai_needs_items.js")
|
||||||
|
social = read_text(ROOT / "js/tarinai_social_action_runtime.js")
|
||||||
|
snapshot = read_text(ROOT / "js/snapshot_system.js")
|
||||||
|
codec = read_text(ROOT / "js/save_codec.js")
|
||||||
|
index = read_text(ROOT / "index.html")
|
||||||
|
if 't === "reciprocator") return Math.PI / 2' not in tool_defs:
|
||||||
|
fail("reciprocator default angle is not 90 degrees")
|
||||||
|
small = "[[-52, -20, 52, -20], [52, -20, 52, 20], [52, 20, -52, 20], [-52, 20, -52, -20]]"
|
||||||
|
poison_files = [
|
||||||
|
"js/item_type_initializers.js", "js/physics_world_system.js", "js/item_render_runtime.js",
|
||||||
|
"js/mechanical_system.js", "js/world_placement_log.js",
|
||||||
|
]
|
||||||
|
for rel in poison_files:
|
||||||
|
body = read_text(ROOT / rel)
|
||||||
|
if not all(token in body for token in ("-52", "-20", "52", "20")):
|
||||||
|
fail(f"small poison-block shape missing from {rel}")
|
||||||
|
if "[-82, -28, 82, -28]" in body:
|
||||||
|
fail(f"legacy large poison-block shape remains in {rel}")
|
||||||
|
if 'radius: 44' not in tool_defs or 'thickness: 11' not in initializer:
|
||||||
|
fail("small poison-block radius/thickness defaults are not synchronized")
|
||||||
|
for token in ("canAddTarinai(count = 1)", "canAddObjects(count = 1)", "setTarinaiPopulationLimit", "setObjectLimit"):
|
||||||
|
if token not in world_budget:
|
||||||
|
fail(f"colony limit runtime missing: {token}")
|
||||||
|
if "if (!(this.canAddTarinai?.(1) ?? true)) return null;" not in world_family:
|
||||||
|
fail("tarinai creation/birth is not unconditionally guarded by population limit")
|
||||||
|
if "if (!this.canAddObjects(1)) return null;" not in world_budget:
|
||||||
|
fail("object creation is not unconditionally guarded by object limit")
|
||||||
|
if "ignoreColonyLimit" in world_family or "ignoreColonyLimit" in world_budget:
|
||||||
|
fail("colony limit bypass option remains in central creation APIs")
|
||||||
|
unsafe_fallback = re.compile(r"addItem\?\.[^\n;]+\|\|[^\n;]*items\.push")
|
||||||
|
for rel in (ROOT / "js").glob("*.js"):
|
||||||
|
if unsafe_fallback.search(read_text(rel)):
|
||||||
|
fail(f"object-limit bypass fallback remains in {rel.name}")
|
||||||
|
direct_item_pushes = []
|
||||||
|
direct_tarinai_pushes = []
|
||||||
|
for rel in (ROOT / "js").glob("*.js"):
|
||||||
|
body = read_text(rel)
|
||||||
|
if re.search(r"\b(?:this|worldRef|world)\.items\.push\(", body):
|
||||||
|
direct_item_pushes.append(rel.name)
|
||||||
|
if re.search(r"\b(?:this|worldRef|world)\.tarinai\.push\(", body):
|
||||||
|
direct_tarinai_pushes.append(rel.name)
|
||||||
|
if sorted(direct_item_pushes) != ["snapshot_system.js", "world_spatial_budget.js"]:
|
||||||
|
fail("unexpected direct world item insertion can bypass object limit: " + ", ".join(sorted(direct_item_pushes)))
|
||||||
|
if sorted(direct_tarinai_pushes) != ["snapshot_system.js", "world_family_social.js"]:
|
||||||
|
fail("unexpected direct tarinai insertion can bypass population limit: " + ", ".join(sorted(direct_tarinai_pushes)))
|
||||||
|
guarded_sources = {
|
||||||
|
"js/item_dynamic_ball_system.js": "if (!addedDrop) continue;",
|
||||||
|
"js/fire_runtime_system.js": "if (!addedFire) return null;",
|
||||||
|
"js/tarinai_building_behavior.js": "canAddObjects?.(1)",
|
||||||
|
"js/world_combat_effects.js": "if (!spawned) return 0;",
|
||||||
|
}
|
||||||
|
for rel, token in guarded_sources.items():
|
||||||
|
if token not in read_text(ROOT / rel):
|
||||||
|
fail(f"object-limit guard missing from {rel}: {token}")
|
||||||
|
if "championMate" not in needs or "championPair" not in social or "mateWeight *= 5.0" not in social:
|
||||||
|
fail("champion-pair breeding preference is incomplete")
|
||||||
|
for token in ("tarinaiPopulationLimit", "objectLimit"):
|
||||||
|
if token not in snapshot or token not in codec:
|
||||||
|
fail(f"colony limit persistence missing: {token}")
|
||||||
|
for element_id in ("tarinaiPopulationLimitInput", "objectLimitInput", "colonyLimitStatus"):
|
||||||
|
if f'id="{element_id}"' not in index:
|
||||||
|
fail(f"colony limit UI missing: {element_id}")
|
||||||
|
|
||||||
|
js = f"""
|
||||||
|
const vm = require('vm');
|
||||||
|
const fs = require('fs');
|
||||||
|
class World {{
|
||||||
|
constructor() {{
|
||||||
|
this.items = [];
|
||||||
|
this.tarinai = [];
|
||||||
|
this.itemCounts = {{}};
|
||||||
|
this.tarinaiPopulationLimit = 0;
|
||||||
|
this.objectLimit = 0;
|
||||||
|
}}
|
||||||
|
markItemBucketsDirty() {{}}
|
||||||
|
markSpatialDirty() {{}}
|
||||||
|
markTerrainDirty() {{}}
|
||||||
|
markTerrainDirtyAt() {{}}
|
||||||
|
}}
|
||||||
|
const context = {{ console, World, CONFIG: {{ grassLimit: Infinity }}, globalThis: null }};
|
||||||
|
context.globalThis = context;
|
||||||
|
context.window = context;
|
||||||
|
context.TarinaiGround = {{ grassLimit() {{ return Infinity; }} }};
|
||||||
|
context.TarinaiPerf = {{ snapshot() {{ return null; }}, begin() {{ return null; }} }};
|
||||||
|
vm.createContext(context);
|
||||||
|
vm.runInContext(fs.readFileSync({str(ROOT / 'js/world_spatial_budget.js')!r}, 'utf8'), context);
|
||||||
|
const world = new context.World();
|
||||||
|
if (world.tarinaiPopulationLimit !== 0 || world.objectLimit !== 0) throw new Error('limits must default to unlimited');
|
||||||
|
world.setObjectLimit(2.9);
|
||||||
|
if (world.objectLimit !== 2) throw new Error('object limit must normalize to a positive integer');
|
||||||
|
if (!world.canAddObjects(2) || world.canAddObjects(3)) throw new Error('object capacity query is incorrect');
|
||||||
|
if (!world.addItem({{ type: 'stone' }}, 'test-1')) throw new Error('first object rejected');
|
||||||
|
if (!world.addItem({{ type: 'stone' }}, 'test-2')) throw new Error('second object rejected');
|
||||||
|
if (world.addItem({{ type: 'stone' }}, 'test-3') !== null) throw new Error('object limit not enforced');
|
||||||
|
world.items[0].dead = true;
|
||||||
|
if (!world.addItem({{ type: 'stone' }}, 'test-4')) throw new Error('dead object should free capacity');
|
||||||
|
world.setObjectLimit(1);
|
||||||
|
if (world.addItem({{ type: 'stone' }}, 'over-limit-existing') !== null) throw new Error('existing over-limit world must reject new objects');
|
||||||
|
for (const item of world.items) item.dead = true;
|
||||||
|
if (!world.addItem({{ type: 'stone' }}, 'capacity-restored')) throw new Error('capacity was not restored after all live objects died');
|
||||||
|
world.setTarinaiPopulationLimit(1.8);
|
||||||
|
if (world.tarinaiPopulationLimit !== 1) throw new Error('tarinai limit must normalize to a positive integer');
|
||||||
|
world.tarinai.push({{ dead: false }});
|
||||||
|
if (world.canAddTarinai(1)) throw new Error('tarinai limit not enforced');
|
||||||
|
world.tarinai[0].dead = true;
|
||||||
|
if (!world.canAddTarinai(1)) throw new Error('dead tarinai should free capacity');
|
||||||
|
world.setObjectLimit('');
|
||||||
|
if (world.objectLimit !== 0 || !world.canAddObjects(1000)) throw new Error('blank/zero must mean unlimited');
|
||||||
|
context.TarinaiSnapshot = {{ version: 23 }};
|
||||||
|
context.TarinaiSaveSchema = {{ BINARY_SCHEMA_VERSION: 28, FIELD_IDS: ['garden'] }};
|
||||||
|
context.CONFIG.dayLength = 120;
|
||||||
|
context.TextEncoder = TextEncoder;
|
||||||
|
context.TextDecoder = TextDecoder;
|
||||||
|
vm.runInContext(fs.readFileSync({str(ROOT / 'js/save_codec.js')!r}, 'utf8'), context);
|
||||||
|
(async () => {{
|
||||||
|
const source = {{ v: 23, a: 'tj1', m: [1, 0, 'garden'], w: [0, 0, 0, 120, 0, -9990, 1, 0, 'seed', 0, 12, 345], t: [], i: [] }};
|
||||||
|
const encoded = await context.TarinaiSaveCodec.encodeSnapshot(source);
|
||||||
|
const decoded = await context.TarinaiSaveCodec.decodeSnapshot(encoded);
|
||||||
|
if (decoded.w[10] !== 12 || decoded.w[11] !== 345) throw new Error('colony limits did not survive save roundtrip');
|
||||||
|
}})().catch(error => {{ console.error(error); process.exitCode = 1; }});
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||||
|
f.write(js)
|
||||||
|
tmp = Path(f.name)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||||
|
finally:
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.stderr.write(result.stdout)
|
||||||
|
sys.stderr.write(result.stderr)
|
||||||
|
fail("colony limit runtime smoke test failed")
|
||||||
|
ok("champion breeding, mechanical defaults, and colony limits are wired")
|
||||||
|
|
||||||
|
|
||||||
|
def physics_tuning_smoke() -> None:
|
||||||
|
editor = read_text(ROOT / "js/physics_shape_editor_system.js")
|
||||||
|
environment = read_text(ROOT / "js/world_environment.js")
|
||||||
|
helpers = read_text(ROOT / "js/physics_helpers.js")
|
||||||
|
constraints = read_text(ROOT / "js/constraint_system.js")
|
||||||
|
dynamic_tools = read_text(ROOT / "js/item_dynamic_tool_system.js")
|
||||||
|
initializer = read_text(ROOT / "js/item_type_initializers.js")
|
||||||
|
|
||||||
|
for token in ("BOUNCE_FENCE_RESTITUTION = 2.40", "BOUNCE_FENCE_MIN_SPEED = 520", "BOUNCE_FENCE_MAX_SPEED = 1180"):
|
||||||
|
if token not in environment:
|
||||||
|
fail(f"strong bounce-fence tuning missing: {token}")
|
||||||
|
if "const iterations = 8" not in constraints or "const compliance = mode === \"rod\" ? 0.085 / (step * 60) : 0" not in constraints:
|
||||||
|
fail("rope endpoint constraint is still compliant/stretchable")
|
||||||
|
if "STICKY_BOMB_BLAST_SCALE = 0.68" not in dynamic_tools or "item.blastScale = 0.68" not in initializer:
|
||||||
|
fail("sticky bomb is not synchronized with small-firecracker blast scale")
|
||||||
|
|
||||||
|
js = f"""
|
||||||
|
const vm = require('vm');
|
||||||
|
const editorSource = {editor!r};
|
||||||
|
const helperSource = {helpers!r};
|
||||||
|
const constraintSource = {constraints!r};
|
||||||
|
const dynamicSource = {dynamic_tools!r};
|
||||||
|
|
||||||
|
const context = {{ console, globalThis: null }};
|
||||||
|
context.globalThis = context;
|
||||||
|
context.window = context;
|
||||||
|
context.clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
|
context.distXY = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
|
||||||
|
context.CONFIG = {{ worldPadding: 0 }};
|
||||||
|
context.Effect = function Effect() {{}};
|
||||||
|
context.TarinaiGeometry = {{
|
||||||
|
pointSegmentDistance(px, py, x1, y1, x2, y2) {{
|
||||||
|
const dx = x2 - x1, dy = y2 - y1;
|
||||||
|
const den = dx * dx + dy * dy || 1;
|
||||||
|
const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / den));
|
||||||
|
return Math.hypot(px - (x1 + dx * t), py - (y1 + dy * t));
|
||||||
|
}},
|
||||||
|
rectLocalPoint(rect, x, y) {{ return {{ x: x - (rect.cx || 0), y: y - (rect.cy || 0) }}; }},
|
||||||
|
rectWorldVector(_rect, x, y) {{ return {{ x, y }}; }},
|
||||||
|
rectWorldNormal(_rect, x, y) {{ return {{ x, y }}; }},
|
||||||
|
}};
|
||||||
|
context.TarinaiMechanicalSystem = {{
|
||||||
|
isMechanicalType() {{ return false; }},
|
||||||
|
isPowered() {{ return false; }},
|
||||||
|
applySurfaceVelocityToCircle() {{}},
|
||||||
|
wakeItem() {{}},
|
||||||
|
}};
|
||||||
|
context.TarinaiPhysicsProjectionSystem = {{ markSpatialDirty() {{ return false; }} }};
|
||||||
|
context.TarinaiMovementUpdateStep = {{ markSleepExternalMotion() {{}} }};
|
||||||
|
context.itemAngleFor = item => Number(item?.angle || 0) || 0;
|
||||||
|
context.normalizedItemAngle = angle => Number(angle || 0) || 0;
|
||||||
|
vm.createContext(context);
|
||||||
|
|
||||||
|
vm.runInContext(editorSource, context, {{ filename: 'physics_shape_editor_system.js' }});
|
||||||
|
const circle = context.TarinaiPhysicsShapeEditorSystem.templateSegments('circle');
|
||||||
|
if (!Array.isArray(circle) || circle.length !== 24) throw new Error('circle template is not a 24-sided contour');
|
||||||
|
for (const segment of circle) {{
|
||||||
|
if (segment.length !== 4 || segment.some(value => value % 20 !== 0)) throw new Error('circle template left the 20px grid');
|
||||||
|
}}
|
||||||
|
for (let i = 0; i < circle.length; i += 1) {{
|
||||||
|
const next = circle[(i + 1) % circle.length];
|
||||||
|
if (circle[i][2] !== next[0] || circle[i][3] !== next[1]) throw new Error('circle contour is not closed');
|
||||||
|
}}
|
||||||
|
|
||||||
|
vm.runInContext(helperSource, context, {{ filename: 'physics_helpers.js' }});
|
||||||
|
const ball = {{ x: -4, y: 5, prevX: -14, prevY: 5, vx: 100, vy: 0, r: 5, spinVelocity: 0 }};
|
||||||
|
const fence = {{ left: 0, right: 10, top: 0, bottom: 10, bounce: true, restitution: 2.40, minBounceSpeed: 520 }};
|
||||||
|
if (!context.TarinaiPhysics.bounceCircleOffRect(ball, fence, 5, fence.restitution, null, {{ minBounceSpeed: fence.minBounceSpeed }})) throw new Error('bounce-fence collision did not resolve');
|
||||||
|
if (ball.vx > -519) throw new Error('bounce-fence launch is not substantially stronger');
|
||||||
|
|
||||||
|
context.TarinaiPhysicsBodySystem = {{
|
||||||
|
scalar(item, key, fallback) {{ return Number(item.physicsBody?.[key] ?? fallback); }},
|
||||||
|
setScalar(item, key, value) {{ item.physicsBody ||= {{}}; item.physicsBody[key] = value; return true; }},
|
||||||
|
linkScalar(item, key, fallback) {{
|
||||||
|
const c = item.physicsConstraint || {{}};
|
||||||
|
const map = {{ len: 'length', midX: 'midX', midY: 'midY', midVx: 'midVx', midVy: 'midVy', awakeUntil: 'awakeUntil' }};
|
||||||
|
return Number(c[map[key] || key] ?? fallback);
|
||||||
|
}},
|
||||||
|
setLinkScalar(item, key, value) {{
|
||||||
|
const c = item.physicsConstraint ||= {{}};
|
||||||
|
const map = {{ len: 'length', midX: 'midX', midY: 'midY', midVx: 'midVx', midVy: 'midVy', awakeUntil: 'awakeUntil' }};
|
||||||
|
c[map[key] || key] = value;
|
||||||
|
return true;
|
||||||
|
}},
|
||||||
|
endpoint(item, index) {{ return item.physicsConstraint?.endpoints?.[index] || null; }},
|
||||||
|
particles(item) {{ return item.physicsConstraint?.particles || null; }},
|
||||||
|
setParticles(item, list) {{ item.physicsConstraint.particles = list; return true; }},
|
||||||
|
ensureConstraint(item) {{ return item.physicsConstraint; }},
|
||||||
|
applyConstraintState() {{ return true; }},
|
||||||
|
normalizeConstraintState(_item, state) {{ return state; }},
|
||||||
|
syncPoseFromItem() {{ return true; }},
|
||||||
|
normalizeBodyState() {{ return true; }},
|
||||||
|
markBodyChanged() {{ return true; }},
|
||||||
|
}};
|
||||||
|
vm.runInContext(constraintSource, context, {{ filename: 'constraint_system.js' }});
|
||||||
|
const a = {{ id: 'a', type: 'ball', x: 0, y: 0, r: 10, dead: false }};
|
||||||
|
const b = {{ id: 'b', type: 'ball', x: 200, y: 0, r: 10, dead: false }};
|
||||||
|
const objects = new Map([[a.id, a], [b.id, b]]);
|
||||||
|
const world = {{
|
||||||
|
time: 1,
|
||||||
|
items: [a, b],
|
||||||
|
tarinai: [],
|
||||||
|
itemCounts: {{ rope: 1 }},
|
||||||
|
itemById(id) {{ return objects.get(id) || null; }},
|
||||||
|
nearbyTarinai() {{ return []; }},
|
||||||
|
nearbySolidObstacleRects() {{ return []; }},
|
||||||
|
markSpatialDirty() {{}},
|
||||||
|
drawListDirty: false,
|
||||||
|
}};
|
||||||
|
const rope = {{
|
||||||
|
id: 'rope-1', type: 'rope', dead: false, amount: 999, world,
|
||||||
|
physicsConstraint: {{ endpoints: [{{ kind: 'item', id: 'a', x: 0, y: 0 }}, {{ kind: 'item', id: 'b', x: 200, y: 0 }}], length: 100, particles: null }},
|
||||||
|
}};
|
||||||
|
context.TarinaiConstraintSystem.updateFlexibleLink(rope, 1 / 60, world);
|
||||||
|
const tautDistance = Math.hypot(b.x - a.x, b.y - a.y);
|
||||||
|
if (tautDistance > 100.26) throw new Error(`rope stretched to ${{tautDistance}}px`);
|
||||||
|
const beforeA = {{ x: a.x, y: a.y }}, beforeB = {{ x: b.x, y: b.y }};
|
||||||
|
b.x = a.x + 60;
|
||||||
|
context.TarinaiConstraintSystem.updateFlexibleLink(rope, 1 / 60, world);
|
||||||
|
if (Math.hypot(a.x - beforeA.x, a.y - beforeA.y) > 0.01) throw new Error('slack rope pushed endpoint A');
|
||||||
|
if (Math.abs((b.x - a.x) - 60) > 0.01) throw new Error('slack rope pushed endpoints apart');
|
||||||
|
|
||||||
|
context.deterministicChance = () => false;
|
||||||
|
context.Effect = function Effect() {{}};
|
||||||
|
context.audio = {{ explode() {{}} }};
|
||||||
|
vm.runInContext(dynamicSource, context, {{ filename: 'item_dynamic_tool_system.js' }});
|
||||||
|
let capturedScale = null;
|
||||||
|
const sticky = {{ type: 'sticky_bomb', dead: false, fuseTimer: 0, stickyBombCarrierId: '', stickyBombTransferCooldown: 0, spin: 0, spinVelocity: 0 }};
|
||||||
|
const stickyWorld = {{ tarinai: [], explodeFirecracker(item) {{ capturedScale = item.blastScale; return true; }} }};
|
||||||
|
context.TarinaiItemDynamicToolSystem.update(sticky, 1 / 60, stickyWorld);
|
||||||
|
if (Math.abs(capturedScale - 0.68) > 1e-9) throw new Error('sticky bomb does not match small firecracker blast scale');
|
||||||
|
"""
|
||||||
|
with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".js", delete=False) as f:
|
||||||
|
f.write(js)
|
||||||
|
tmp = Path(f.name)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["node", str(tmp)], cwd=ROOT, text=True, capture_output=True)
|
||||||
|
finally:
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
sys.stderr.write(result.stdout)
|
||||||
|
sys.stderr.write(result.stderr)
|
||||||
|
fail("physics tuning smoke test failed")
|
||||||
|
ok("round circle template, strong bounce fence, inextensible rope, and small sticky-bomb blast verified")
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
data = manifest()
|
data = manifest()
|
||||||
check_files_exist(data)
|
check_files_exist(data)
|
||||||
|
|
@ -1227,6 +1640,10 @@ def main() -> None:
|
||||||
node_check([*(ROOT / "js").glob("*.js"), ROOT / "service-worker.js"])
|
node_check([*(ROOT / "js").glob("*.js"), ROOT / "service-worker.js"])
|
||||||
action_spec_smoke()
|
action_spec_smoke()
|
||||||
behavior_state_smoke()
|
behavior_state_smoke()
|
||||||
|
need_action_runtime_bridge_smoke()
|
||||||
|
mechanical_collision_and_editor_smoke()
|
||||||
|
physics_tuning_smoke()
|
||||||
|
colony_limits_and_defaults_smoke()
|
||||||
check_basic_action_spec_runtime()
|
check_basic_action_spec_runtime()
|
||||||
check_social_action_spec_runtime()
|
check_social_action_spec_runtime()
|
||||||
check_action_spec_no_legacy_hooks()
|
check_action_spec_no_legacy_hooks()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const APP_VERSION = "39.15.164";
|
const APP_VERSION = "39.15.167";
|
||||||
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||||
const v = `v=${APP_VERSION}`;
|
const v = `v=${APP_VERSION}`;
|
||||||
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue