rr
This commit is contained in:
parent
2636d580a8
commit
d14550dad6
78 changed files with 2827 additions and 563 deletions
Binary file not shown.
|
|
@ -1 +0,0 @@
|
|||
level=none expiry=0
|
||||
15
AI_MAP.md
15
AI_MAP.md
|
|
@ -6,7 +6,7 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th
|
|||
|
||||
- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order.
|
||||
- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader.
|
||||
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.16.18`.
|
||||
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.16.29`.
|
||||
- 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.
|
||||
- Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`.
|
||||
|
|
@ -81,3 +81,16 @@ This section records current boundaries only. Version-by-version cleanup history
|
|||
- Effect-specific spatial indexing, fuller delta-history storage, and high-speed collision partitioning. These need new indexes or semantics beyond cleanup.
|
||||
- UI/text-side meaning consolidation, including labels, wrapping/measurement helpers, and duplicated scalar helpers such as `num`, `finiteOr`, `isObject`, and `clonePlain`.
|
||||
- Further `World.prototype` reduction is possible, but only for helpers proven not to be runtime-facing canvas/UI/tool APIs.
|
||||
|
||||
## v39.16.30 routing notes
|
||||
|
||||
- War preset composition: `js/world_reset_presets.js`.
|
||||
- Fence restitution: `js/world_environment.js`; ball response: `js/item_dynamic_ball_system.js`.
|
||||
- Ball-to-Tarinai contact, including sleepers: `js/collision_response_system.js`.
|
||||
- Movable placement stacking: `js/item_tool_metadata.js`, `js/collision_footprint_system.js`, `js/world_placement_log.js`.
|
||||
- Delete-tool target lookup: `js/world_tool_actions.js`.
|
||||
- Nest-box tooltip offset: `js/render.js`.
|
||||
|
||||
## v39.16.31 routing notes
|
||||
|
||||
- Hair Trigger preset: `js/world_reset_presets.js`; physics tool icon renderer: `js/ui_tools.js`; world tooltip offset: `js/render.js`.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ tarinai_compact_save_stage4
|
|||
- Deflate state is folded into the low bit of the frame-length varuint, eliminating the separate compression byte.
|
||||
- 15-bit text conversion uses arithmetic Unicode range mapping instead of a 32768-character string and reverse Map.
|
||||
- Previous binary schemas are intentionally unsupported.
|
||||
- Version: 39.16.18
|
||||
- Version: 39.16.31
|
||||
- CompressionStream and DecompressionStream are drained concurrently to avoid browser backpressure stalls.
|
||||
|
||||
- Load confirmation uses the in-game dialog; fan wind is realtime; zunchi tool preview opacity, carried plushie depth, and blanket grass rules were corrected.
|
||||
|
|
@ -27,3 +27,22 @@ tarinai_compact_save_stage4
|
|||
- v39.16.16: colony population chart records births/deaths, rotators accept negative motor speed, champion personality boosts inherit through descendants, champion-pair mating is slightly reduced, and one-way fence previews/pathfinding honor the passable direction.
|
||||
|
||||
- v39.16.16: one-way fences allow Tarinai forward crossing in swept collision; chart legend dots toggle series; wire tools attach to Tarinai/ants; energized living endpoints panic and take damage; selected data reports electrocution.
|
||||
|
||||
- v39.16.19: added direct food/medicine giving by placement or pinched drop, with a give overlay, stronger food-gift personality change, and heart bursts; removed unused collision reasons and duplicate constraint projection dirty checks.
|
||||
|
||||
- v39.16.20: localized the direct-feeding personality reason, normalized electrocution deaths to 「感電」, added detector-controlled gate fences, made chart legend labels fully clickable, and escaped non-ASCII runtime source.
|
||||
|
||||
- v39.16.21: hid pinch overlays for rope/rod/spring/wire connection items and added detector time schedules with a dual-thumb range control, overnight ranges, and persistent copy/save state.
|
||||
|
||||
- v39.16.22: shortened detector copy, removed the time-range explanatory hint, cascaded connection-item removal when any endpoint disappears, and repaired detached plushies so they return to a living owner or disappear with a missing owner.
|
||||
|
||||
- v39.16.23: excluded transient splat effects from all connection endpoint selection and renamed the user-facing feeding need from 「摂餌」 to 「食欲」.
|
||||
- v39.16.25: added toilet-seeking before normal defecation; renamed rain shelter to parasol; added sunny +3.5C and unsheltered rain -3.5C felt-temperature corrections; parasols neutralize both and block sunbathing. Pet food, first aid, and sedative remain included.
|
||||
|
||||
- v39.16.29: replaced toilet-seeking with walkable toilet sand that immediately removes poop spawned on its surface; added a parasol ground shadow and explicit hot-sun/cold-rain temperature-seeking targets; fixed null candidate temperatures being interpreted as 0C.
|
||||
|
||||
- v39.16.29: aligned parasol placement overlay with its shade, made toilet sand absorb rain water, expanded Happy/Athletic/War presets, simplified Athletic rotators, and reserved their full rotation envelope from fences.
|
||||
|
||||
- v39.16.30: added a few bounce fences to War, reduced ordinary-fence restitution, restored ball collisions for visible sleeping Tarinai, moved nest-box tooltips closer, allowed movable-item placement overlap, and fixed delete-tool target lookup.
|
||||
|
||||
- v39.16.31: unified in-world tooltip spacing, added the Hair Trigger preset, and replaced physics-category line-like tool icons with distinct semantic silhouettes.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
|
||||
"version": "39.16.18",
|
||||
"version": "39.16.31",
|
||||
"entrypoints": {
|
||||
"app": "index.html",
|
||||
"runtime": "js/main.js",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
## v39.16.13 detector and fence changes
|
||||
|
||||
- 感圧スイッチを「検知器」へ改名し、道具UI上で通常の柵と位置を入れ替えた。
|
||||
- 検知器は軸平行の矩形範囲で対象を数える。専用UIから横幅・縦幅・対象・必要数を設定する。
|
||||
- 検知器は軸平行の矩形範囲で対象を数えるほか、時刻も検知できる。専用UIから対象・必要数・範囲、または二つまみスライダーによる作動時間帯を設定する。
|
||||
- 柵の配置境界判定を実際の細長い形状に合わせ、フィールド上下端近くへ配置できるようにした。
|
||||
- バウンス柵の反発係数、最低射出速度、最大速度を引き上げ、衝突直後に強く跳ねるようにした。
|
||||
- ワープゲートを道具、描画、更新、信号、保存形式から削除した。
|
||||
|
|
@ -48,11 +48,13 @@
|
|||
|
||||
## v39.16.11 mechanism fixes and passive spring
|
||||
|
||||
- 検知器は矩形範囲内の「たりない」または「アイテム」を数え、設定個数以上で電線へ信号を送る。
|
||||
- 検知器は矩形範囲内の「たりない」または「アイテム」を数えるか、設定した時刻帯に入ると電線へ信号を送る。開始時刻が終了時刻より後なら日付をまたぎ、同時刻なら終日作動する。
|
||||
- 一方通行柵は矢印方向だけ通過でき、逆方向のたりない・アイテム・機構を遮る。
|
||||
- バウンス柵は衝突対象へ強い即時反発を与える。
|
||||
- バネは棒と同じ2点接続方式で設置する。自動伸縮はせず、外力による伸び縮みに対して受動的に反発し、弾性運動する。
|
||||
- 電線と絶縁電線はアイテム同士を接続し、複数本を経由して信号を伝える。通常電線は通電中に接触したたりないへダメージを与え、絶縁電線は与えない。
|
||||
- ヒモ、棒、バネ、電線、絶縁電線は、いずれかの接続先が消滅すると連鎖的に消滅する。
|
||||
- 所有者が生存しているぬいぐるみは、投擲中やプレイヤー所持中を除き頭上へ復帰し、地面に放置されない。
|
||||
- バイナリスキーマ34、スナップショット28。v39.16.10と保存形式は同じ。
|
||||
|
||||
## v39.16.9 load UI and item behavior fixes
|
||||
|
|
@ -298,3 +300,41 @@ Recovery depends on the disease or effect. Water, rest, food, medicine, zunda ju
|
|||
- Current saves are allowed to use compact/hash-style text. Old save text does not need to remain loadable when the save format is intentionally shortened or replaced, so load failure is acceptable after intentional save-format changes as long as the current save/load path stays internally consistent.
|
||||
- Reset opens field selection and starts a new field state.
|
||||
- Service worker caching allows the static app shell to remain available after load, depending on browser support.
|
||||
|
||||
## v39.16.25 additions
|
||||
|
||||
- 雨天時は未遮蔽のたりないの体感気温が3.5℃低下し、晴天時は3.5℃上昇する。
|
||||
- 雨よけをパラソルへ変更。範囲内では雨と日差しを防ぎ、天候による体感気温補正を無効化する。
|
||||
- パラソル内では日光浴を開始・継続しない。
|
||||
- ペットフード、救急薬、鎮静薬の追加内容は維持する。
|
||||
|
||||
|
||||
## v39.16.28 additions
|
||||
|
||||
- トイレの砂を従来より大きな角丸矩形へ変更し、干草寝床と同じ背面レイヤーで描画する。
|
||||
- トイレの砂の外観とずんち吸収判定を共通の矩形フットプリントから生成し、見た目と有効範囲を一致させる。
|
||||
- パラソルをやや大型化し、影の中心を支柱の根元へ合わせる。
|
||||
- パラソルの影の楕円を、そのまま雨・日差し・天候由来の体感気温補正を防ぐ範囲として使用する。
|
||||
- 晴天の暑さ、または雨天の寒さを避ける際、パラソルの影の内側を気温調整行動の候補にする。
|
||||
- 扇風機の体感気温低下が風の描画・物体押し出しより短い範囲で途切れていた問題を修正し、320pxの風領域全体で最大5℃の冷却を減衰適用する。
|
||||
## v39.16.29 additions
|
||||
|
||||
- Parasol placement previews use the same ellipse and center as the rendered shade and weather-protection footprint.
|
||||
- Toilet sand absorbs ambient rain-water drops before they become ground water items.
|
||||
- Happy preset adds parasols and water balloons, and places one toilet-sand item at every duplicator position.
|
||||
- Athletic preset adds water balloons and replaces complex rotators with single-bar rotators positioned using their full rotation envelope so they do not strike fences.
|
||||
- War preset adds fans with active rotating head-swing settings.
|
||||
|
||||
|
||||
## v39.16.30 additions
|
||||
|
||||
- 戦争プリセットに少数のバウンス柵を追加する。
|
||||
- 通常の柵は、ボールなどに対する反発を控えめにする。
|
||||
- 巣箱の外で眠っているたりないも、ボールや水風船と物理衝突する。
|
||||
- 巣箱ツールチップを巣箱へやや近づける。
|
||||
- ボール、画鋲、ずんちなどの移動可能アイテム同士は、同じ設置位置へ重ねて配置できる。
|
||||
- 削除ツールの対象探索で発生していた例外を修正する。
|
||||
|
||||
## v39.16.31 additions
|
||||
|
||||
- 一触即発プリセット、ツールチップ距離の統一、物理カテゴリ専用アイコンを追加。
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
|
||||
## Runtime Shape
|
||||
|
||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.16.18`.
|
||||
- v39.16.18 changes: redundant physics normalization, dirty-marking, compaction, pickup, and collision bookkeeping were removed; one-way-fence and bounce corrections now use shared canonical helpers.
|
||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.16.31`.
|
||||
- v39.16.31 changes: all in-world tooltips use the closer offset; Hair Trigger adds a central bounce-fence ball enclosure with Tarinai and pet-food duplicators outside; physics tools use distinct icons.
|
||||
- v39.16.29 changes: parasol placement preview now uses the exact shade center and footprint; toilet sand absorbs rain-created water drops. Happy adds parasols, water balloons, and toilet sand beneath each duplicator; Athletic uses fence-safe single-bar rotators and water balloons; War adds rotating fans.
|
||||
- v39.16.19 changes: food and medicine can be given directly by placement or pinched drop, with a dedicated overlay, food heart bursts, and stronger personality effects; unused collision reasons and duplicate constraint projection dirty checks were removed.
|
||||
- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots.
|
||||
- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases.
|
||||
- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`.
|
||||
|
|
@ -56,3 +58,4 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
|
||||
### v39.16.16
|
||||
Colony birth/death chart lines, reverse rotators, champion-trait inheritance, reduced champion-pair mating, and directional one-way-fence routing.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.16.18",
|
||||
"version": "39.16.31",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -91,6 +91,7 @@
|
|||
"js/tarinai_action_definitions.js",
|
||||
"js/tarinai_needs_items.js",
|
||||
"js/tarinai_food_prototype_mixin.js",
|
||||
"js/tarinai_direct_feeding_system.js",
|
||||
"js/tarinai_need_planner_system.js",
|
||||
"js/tarinai_item_interaction_context.js",
|
||||
"js/tarinai_food_interaction_system.js",
|
||||
|
|
|
|||
97
css/base.css
97
css/base.css
|
|
@ -401,6 +401,7 @@ h1 {
|
|||
.tool[data-tool="giant_drug"] .tool-sprite-preview { width: 39px; height: 39px; left: -4px; }
|
||||
.tool[data-tool="dwarf_drug"] .tool-sprite-preview,
|
||||
.tool[data-tool="oshibyo"] .tool-sprite-preview { width: 22px; height: 22px; left: 5px; }
|
||||
.tool.tool-has-physics-icon .tool-physics-preview { width: 31px; height: 31px; left: 0; filter: drop-shadow(0 1px 2px rgba(52,40,26,0.16)); }
|
||||
.tool[data-tool="poke"], .tool[data-tool="water_hose"] { --tool-icon-zoom: 0.92; }
|
||||
|
||||
.signboard-editor {
|
||||
|
|
@ -620,6 +621,102 @@ h1 {
|
|||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.pressure-time-control[hidden] { display: none; }
|
||||
.pressure-spatial-control[hidden] { display: none; }
|
||||
.pressure-time-control {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px 10px 8px;
|
||||
border-radius: 11px;
|
||||
border: 1px solid rgba(84,68,48,0.13);
|
||||
background: rgba(255,255,255,0.58);
|
||||
}
|
||||
.pressure-time-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pressure-time-summary output {
|
||||
display: inline-block;
|
||||
min-width: 42px;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.pressure-time-range {
|
||||
position: relative;
|
||||
height: 34px;
|
||||
margin: 0 7px;
|
||||
}
|
||||
.pressure-time-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 14px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(94,112,128,0.22);
|
||||
box-shadow: inset 0 1px 2px rgba(43,55,66,0.16);
|
||||
}
|
||||
.pressure-time-thumb {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pressure-time-thumb::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.pressure-time-thumb::-webkit-slider-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -7px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(42,121,172,0.88);
|
||||
background: #fafdff;
|
||||
box-shadow: 0 2px 6px rgba(42,78,104,0.24);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
}
|
||||
.pressure-time-thumb::-moz-range-track {
|
||||
height: 6px;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.pressure-time-thumb::-moz-range-thumb {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(42,121,172,0.88);
|
||||
background: #fafdff;
|
||||
box-shadow: 0 2px 6px rgba(42,78,104,0.24);
|
||||
pointer-events: auto;
|
||||
cursor: grab;
|
||||
}
|
||||
.pressure-time-thumb:focus-visible::-webkit-slider-thumb {
|
||||
outline: 3px solid rgba(73,157,216,0.30);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.pressure-time-thumb:focus-visible::-moz-range-thumb {
|
||||
outline: 3px solid rgba(73,157,216,0.30);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.pressure-time-thumb-start { z-index: 2; }
|
||||
.pressure-time-thumb-end { z-index: 3; }
|
||||
.pressure-time-range[data-wraps="1"] .pressure-time-thumb-start { z-index: 4; }
|
||||
.signal-channel-label {
|
||||
font-size: 13px;
|
||||
color: var(--ink);
|
||||
|
|
|
|||
|
|
@ -344,23 +344,35 @@ body::before {
|
|||
color: var(--muted);
|
||||
line-height: 1.25;
|
||||
}
|
||||
.colony-chart-legend span {
|
||||
.colony-chart-legend .chart-series-toggle {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 2px 4px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: opacity 120ms ease, background-color 120ms ease, transform 120ms ease;
|
||||
}
|
||||
.colony-chart-legend i {
|
||||
.colony-chart-legend .chart-series-toggle:hover { background: rgba(255,255,255,0.56); }
|
||||
.colony-chart-legend .chart-series-toggle:focus-visible { outline: 2px solid rgba(103,178,44,0.58); outline-offset: 1px; }
|
||||
.colony-chart-legend .chart-series-toggle:active { transform: translateY(1px); }
|
||||
.colony-chart-legend .chart-series-toggle i {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex: 0 0 10px;
|
||||
border-radius: 999px;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 0 1px rgba(70, 52, 36, 0.10);
|
||||
cursor: pointer;
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
}
|
||||
.colony-chart-legend i:hover,
|
||||
.colony-chart-legend i:focus-visible { transform: scale(1.18); outline: none; }
|
||||
.colony-chart-legend .series-hidden { opacity: 0.42; text-decoration: line-through; }
|
||||
.colony-chart-legend .series-hidden i { background: transparent !important; box-shadow: inset 0 0 0 2px currentColor; }
|
||||
|
||||
|
|
|
|||
356
index.html
356
index.html
|
|
@ -4,14 +4,14 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>たりない観察</title>
|
||||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.16.18" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.16.18" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=39.16.18" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.16.18" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.16.18" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.16.18" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.16.18" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.16.18" />
|
||||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.16.31" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.16.31" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=39.16.31" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.16.31" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.16.31" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.16.31" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.16.31" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.16.31" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -107,15 +107,15 @@
|
|||
<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">
|
||||
</div>
|
||||
<div class="colony-limit-control" aria-label="コロニー上限設定">
|
||||
<div class="colony-limit-head"><span>上限設定</span><small>0は上限なし</small></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 class="colony-limit-row"><span>たりない個体数</span><input id="tarinaiPopulationLimitSlider" class="colony-limit-slider" type="range" min="0" max="300" step="1" value="0"><input id="tarinaiPopulationLimitInput" class="colony-limit-number" type="number" min="0" max="999" step="1" inputmode="numeric" placeholder="0"></label>
|
||||
<label class="colony-limit-row"><span>オブジェクト総数</span><input id="objectLimitSlider" class="colony-limit-slider" type="range" min="0" max="1200" step="1" value="0"><input id="objectLimitInput" class="colony-limit-number" type="number" min="0" max="9999" step="1" inputmode="numeric" placeholder="0"></label>
|
||||
<label class="colony-limit-row"><span>たりない個体数</span><input id="tarinaiPopulationLimitSlider" class="colony-limit-slider" type="range" min="0" max="300" step="1" value="0"><input id="tarinaiPopulationLimitInput" class="colony-limit-number" type="number" min="0" max="999" step="1" inputmode="numeric" placeholder="0"></label>
|
||||
<label class="colony-limit-row"><span>オブジェクト総数</span><input id="objectLimitSlider" class="colony-limit-slider" type="range" min="0" max="1200" step="1" value="0"><input id="objectLimitInput" class="colony-limit-number" type="number" min="0" max="9999" step="1" inputmode="numeric" placeholder="0"></label>
|
||||
</div>
|
||||
<div id="colonyLimitStatus" class="colony-limit-status">たりない 0 / ∞ オブジェクト 0 / ∞</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="colonyChart" class="colony-chart" aria-label="コロニーの推移グラフ" role="img"></div>
|
||||
</div>
|
||||
|
|
@ -181,6 +181,7 @@
|
|||
<button class="field-choice" data-reset-preset="family" type="button" aria-pressed="false"><strong>家族</strong><span>2匹とへこ餅</span></button>
|
||||
<button class="field-choice" data-reset-preset="war" type="button" aria-pressed="false"><strong>戦争</strong><span>けんかと柵</span></button>
|
||||
<button class="field-choice" data-reset-preset="happy" type="button" aria-pressed="false"><strong>幸福</strong><span>掃除機と巣箱</span></button>
|
||||
<button class="field-choice" data-reset-preset="hair_trigger" type="button" aria-pressed="false"><strong>一触即発</strong><span>中央のボール柵</span></button>
|
||||
</div>
|
||||
<div class="field-dialog-actions">
|
||||
<button id="fieldCancelBtn" class="btn" type="button">キャンセル</button>
|
||||
|
|
@ -261,168 +262,169 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.16.18" defer></script>
|
||||
<script src="js/event_bus.js?v=39.16.18" defer></script>
|
||||
<script src="js/domain_ids.js?v=39.16.18" defer></script>
|
||||
<script src="js/registry_base.js?v=39.16.18" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.16.18" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.16.18" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_tool_metadata.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_registry.js?v=39.16.18" defer></script>
|
||||
<script src="js/data.js?v=39.16.18" defer></script>
|
||||
<script src="js/ground_types.js?v=39.16.18" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.16.18" defer></script>
|
||||
<script src="js/math.js?v=39.16.18" defer></script>
|
||||
<script src="js/geometry_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.16.18" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/signal_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/assets.js?v=39.16.18" defer></script>
|
||||
<script src="js/audio.js?v=39.16.18" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.16.18" defer></script>
|
||||
<script src="js/display_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/render.js?v=39.16.18" defer></script>
|
||||
<script src="js/sim_core.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.16.18" defer></script>
|
||||
<script src="js/structures.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_type_catalog.js?v=39.16.18" defer></script>
|
||||
<script src="js/items.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.16.18" defer></script>
|
||||
<script src="js/fire_runtime_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/robot_cleaner_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.16.18" defer></script>
|
||||
<script src="js/update_step_pipeline_runner.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.16.18" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_render_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.16.18" defer></script>
|
||||
<script src="js/burn_motion_util.js?v=39.16.18" defer></script>
|
||||
<script src="js/ants.js?v=39.16.18" defer></script>
|
||||
<script src="js/health.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.16.18" defer></script>
|
||||
<script src="js/world.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_view.js?v=39.16.18" defer></script>
|
||||
<script src="js/family_graph.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_reset_presets.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.16.18" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_environment.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_temperature_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_pathfinding_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_grass_placement_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/weather_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.16.18" defer></script>
|
||||
<script src="js/system_order.js?v=39.16.18" defer></script>
|
||||
<script src="js/simulation.js?v=39.16.18" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_update.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.16.18" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.16.18" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.16.18" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_log.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.16.18" defer></script>
|
||||
<script src="js/save_schema.js?v=39.16.18" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.16.18" defer></script>
|
||||
<script src="js/history_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/save_codec.js?v=39.16.18" defer></script>
|
||||
<script src="js/save_storage.js?v=39.16.18" defer></script>
|
||||
<script src="js/save_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.16.18" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.16.18" defer></script>
|
||||
<script src="js/main.js?v=39.16.18" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.16.18" defer></script>
|
||||
<script src="js/version.js?v=39.16.31" defer></script>
|
||||
<script src="js/event_bus.js?v=39.16.31" defer></script>
|
||||
<script src="js/domain_ids.js?v=39.16.31" defer></script>
|
||||
<script src="js/registry_base.js?v=39.16.31" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.16.31" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.16.31" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_tool_metadata.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_registry.js?v=39.16.31" defer></script>
|
||||
<script src="js/data.js?v=39.16.31" defer></script>
|
||||
<script src="js/ground_types.js?v=39.16.31" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.16.31" defer></script>
|
||||
<script src="js/math.js?v=39.16.31" defer></script>
|
||||
<script src="js/geometry_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.16.31" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/signal_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/assets.js?v=39.16.31" defer></script>
|
||||
<script src="js/audio.js?v=39.16.31" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.16.31" defer></script>
|
||||
<script src="js/display_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/render.js?v=39.16.31" defer></script>
|
||||
<script src="js/sim_core.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.16.31" defer></script>
|
||||
<script src="js/structures.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_type_catalog.js?v=39.16.31" defer></script>
|
||||
<script src="js/items.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.16.31" defer></script>
|
||||
<script src="js/fire_runtime_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/robot_cleaner_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.16.31" defer></script>
|
||||
<script src="js/update_step_pipeline_runner.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.16.31" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_render_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.16.31" defer></script>
|
||||
<script src="js/burn_motion_util.js?v=39.16.31" defer></script>
|
||||
<script src="js/ants.js?v=39.16.31" defer></script>
|
||||
<script src="js/health.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_direct_feeding_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.16.31" defer></script>
|
||||
<script src="js/world.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_view.js?v=39.16.31" defer></script>
|
||||
<script src="js/family_graph.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_reset_presets.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.16.31" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_environment.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_temperature_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_pathfinding_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_grass_placement_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/weather_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.16.31" defer></script>
|
||||
<script src="js/system_order.js?v=39.16.31" defer></script>
|
||||
<script src="js/simulation.js?v=39.16.31" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_update.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.16.31" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.16.31" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.16.31" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_log.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.16.31" defer></script>
|
||||
<script src="js/save_schema.js?v=39.16.31" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.16.31" defer></script>
|
||||
<script src="js/history_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/save_codec.js?v=39.16.31" defer></script>
|
||||
<script src="js/save_storage.js?v=39.16.31" defer></script>
|
||||
<script src="js/save_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.16.31" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.16.31" defer></script>
|
||||
<script src="js/main.js?v=39.16.31" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.16.31" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -100,6 +100,8 @@
|
|||
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) {
|
||||
if (!other || other === item || other.dead) continue;
|
||||
if (isSoftPlacementIgnoredType(other.type)) continue;
|
||||
const freeStacking = global.TarinaiItemToolMetadata?.ITEM_TRAITS?.freeStackingMovable;
|
||||
if (freeStacking?.has(item.type) && freeStacking.has(other.type)) continue;
|
||||
if (!important.has(other.type)) continue;
|
||||
const otherRects = placementRectsFor(worldRef, other);
|
||||
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
return (worldRef.items || []).filter(it => it && !it.dead && isBallLikeType(it.type));
|
||||
}
|
||||
function ballLikeLabel(item) { return item?.type === "balloon" ? "水風船" : "ボール"; }
|
||||
function ballLikeLabel(item) { return item?.type === "balloon" ? "\u6c34\u98a8\u8239" : "\u30dc\u30fc\u30eb"; }
|
||||
|
||||
|
||||
function resolveBallBallCollisions(worldRef, dt) {
|
||||
|
|
@ -130,7 +130,8 @@
|
|||
const sy = ball.y - py;
|
||||
const segLenSq = sx * sx + sy * sy;
|
||||
for (const t of worldRef.nearbyTarinai(ball.x, ball.y, range)) {
|
||||
if (!t || t.dead || t.state === "sleep" || worldRef.isTarinaiHiddenInNestBox(t)) continue;
|
||||
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox(t)) continue;
|
||||
const sleepingContact = t.state === "sleep" || t.sleeping;
|
||||
let hitX = ball.x;
|
||||
let hitY = ball.y;
|
||||
if (segLenSq > 1) {
|
||||
|
|
@ -235,6 +236,35 @@
|
|||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 8.5, -38, 38);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sleepingContact) {
|
||||
const toward = (ball.vx || 0) * nx + (ball.vy || 0) * ny;
|
||||
const bodyImpulse = clamp(10 + Math.max(0, incomingSpeed) * 0.34 + overlapPush * 1.8, 10, 96);
|
||||
t.vx = clamp((t.vx || 0) - nx * bodyImpulse, -220, 220);
|
||||
t.vy = clamp((t.vy || 0) - ny * bodyImpulse, -220, 220);
|
||||
if (toward < 0) {
|
||||
const restitution = isBalloon ? 0.34 : 0.56;
|
||||
ball.vx = (ball.vx || 0) - (1 + restitution) * toward * nx;
|
||||
ball.vy = (ball.vy || 0) - (1 + restitution) * toward * ny;
|
||||
} else {
|
||||
ball.vx = (ball.vx || 0) + nx * Math.max(8, bodyImpulse * 0.28);
|
||||
ball.vy = (ball.vy || 0) + ny * Math.max(8, bodyImpulse * 0.28);
|
||||
}
|
||||
const separation = Math.max(0, overlapPush + 1.5);
|
||||
if (separation > 0) {
|
||||
ball.x = clamp((ball.x || 0) + nx * separation * 0.72, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
|
||||
ball.y = clamp((ball.y || 0) + ny * separation * 0.72, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
|
||||
t.x = clamp((t.x || 0) - nx * separation * 0.28, CONFIG.worldPadding, worldRef.w - CONFIG.worldPadding);
|
||||
t.y = clamp((t.y || 0) - ny * separation * 0.28, CONFIG.worldPadding, worldRef.h - CONFIG.worldPadding);
|
||||
}
|
||||
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 4.2, -38, 38);
|
||||
ball.lastKickedAt = now;
|
||||
ball.lastKickerId = t.id;
|
||||
global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(t, frameDt, "ball-contact");
|
||||
worldRef.markSpatialDirty?.("sleeping-ball-contact");
|
||||
worldRef.effects.push(new Effect("ring", hitX, hitY, { size: Math.max(12, ball.r * 0.82), life: 0.18, color: isBalloon ? "rgba(248,122,166,0.38)" : "rgba(180,205,120,0.44)" }));
|
||||
continue;
|
||||
}
|
||||
|
||||
const relVx = (t.vx || 0) - (ball.vx || 0);
|
||||
const relVy = (t.vy || 0) - (ball.vy || 0);
|
||||
|
|
@ -270,7 +300,7 @@
|
|||
t.nextPlayBubbleAt = now + deterministicRange(worldRef, "ball-play-bubble-delay", 2.0, 3.8, ball, t);
|
||||
worldRef.spawnBubble(t.x, t.y - t.radius * 1.15, pick(["!", "\u306f\u3046", "?" ]), "rgba(70,96,50,0.76)");
|
||||
}
|
||||
if (playful && worldRef.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) worldRef.log(`${t.name}は${objectLabel}を追いかけて弾いた。`, "observe", { participants: [t] });
|
||||
if (playful && worldRef.relationNotice(t.id, ball.id || "ball", "ball-kick", 16)) worldRef.log(`${t.name}\u306f${objectLabel}\u3092\u8ffd\u3044\u304b\u3051\u3066\u5f3e\u3044\u305f\u3002`, "observe", { participants: [t] });
|
||||
worldRef.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(12, ball.r * 0.80), life: 0.20, color: isBalloon ? "rgba(248,122,166,0.42)" : "rgba(170,210,95,0.50)" }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -783,14 +783,10 @@
|
|||
return { a, b, x: Number(item.x || 0) || 0, y: Number(item.y || 0) || 0, r: Number(item.r || item.radius || 0) || 0 };
|
||||
}
|
||||
|
||||
function finishLinkSpatialUpdate(item, worldRef, before, reason, threshold, minInterval) {
|
||||
if (!worldRef || !before) return false;
|
||||
function finishLinkSpatialUpdate(item, worldRef, reason, threshold, minInterval) {
|
||||
if (!worldRef || !item) return false;
|
||||
worldRef.drawListDirty = true;
|
||||
const movedForSpatial = Math.hypot((Number(item.x || 0) || 0) - before.x, (Number(item.y || 0) || 0) - before.y) > threshold
|
||||
|| Math.abs((Number(item.r || item.radius || 0) || 0) - before.r) > threshold;
|
||||
if (movedForSpatial) markConstraintSpatialDirty(worldRef, item, reason, threshold, minInterval);
|
||||
else if (worldRef.constraintDirtyStatsThisFrame) worldRef.constraintDirtyStatsThisFrame.coalesced = (worldRef.constraintDirtyStatsThisFrame.coalesced || 0) + 1;
|
||||
return movedForSpatial;
|
||||
return markConstraintSpatialDirty(worldRef, item, reason, threshold, minInterval);
|
||||
}
|
||||
|
||||
function updateFlexibleLink(item, dt, worldRef) {
|
||||
|
|
@ -809,7 +805,7 @@
|
|||
}
|
||||
length = primeLinkState(item, aa, bb);
|
||||
changed = solveRopeParticleChain(item, aa, bb, dt, length, worldRef) || changed;
|
||||
if (changed) finishLinkSpatialUpdate(item, worldRef, before, "rope-tension", 0.38, 0.085);
|
||||
if (changed) finishLinkSpatialUpdate(item, worldRef, "rope-tension", 0.38, 0.085);
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
|
@ -842,7 +838,7 @@
|
|||
ep(item, 0).x = fa.x; ep(item, 0).y = fa.y;
|
||||
ep(item, 1).x = fb.x; ep(item, 1).y = fb.y;
|
||||
}
|
||||
if (changed) finishLinkSpatialUpdate(item, worldRef, before, "rod-constraint", 0.30, 0.070);
|
||||
if (changed) finishLinkSpatialUpdate(item, worldRef, "rod-constraint", 0.30, 0.070);
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
|
@ -955,7 +951,7 @@
|
|||
const residual = Math.abs(distXY(fa.x, fa.y, fb.x, fb.y) - rest);
|
||||
if (changed || residual > 0.8) lset(item, "awakeUntil", Number(worldRef?.time || 0) + 0.36);
|
||||
}
|
||||
if (changed) finishLinkSpatialUpdate(item, worldRef, before, "spring-constraint", 0.22, 0.035);
|
||||
if (changed) finishLinkSpatialUpdate(item, worldRef, "spring-constraint", 0.22, 0.035);
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
box.nestFireEvacuated = false;
|
||||
box.flameWaterCooldown = 0;
|
||||
box.nestFireEffectCooldown = 0;
|
||||
worldRef?.log?.("巣箱に火が燃え移った。", "danger");
|
||||
worldRef?.log?.("\u5de3\u7bb1\u306b\u706b\u304c\u71c3\u3048\u79fb\u3063\u305f\u3002", "danger");
|
||||
worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.35, life: 0.30, color: "rgba(255,122,48,0.56)" }));
|
||||
}
|
||||
worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire");
|
||||
|
|
@ -198,7 +198,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
count += 1;
|
||||
}
|
||||
if (count) {
|
||||
worldRef.log?.(`炎上した巣箱から${count}匹のたりないが追い出された。`, "danger", { participants: occupants });
|
||||
worldRef.log?.(`\u708e\u4e0a\u3057\u305f\u5de3\u7bb1\u304b\u3089${count}\u5339\u306e\u305f\u308a\u306a\u3044\u304c\u8ffd\u3044\u51fa\u3055\u308c\u305f\u3002`, "danger", { participants: occupants });
|
||||
worldRef.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.55, life: 0.32, color: "rgba(255,92,38,0.58)" }));
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
worldRef.drawListDirty = true;
|
||||
|
|
@ -222,7 +222,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
box.hp = 0;
|
||||
box.dead = true;
|
||||
}
|
||||
worldRef?.log?.("巣箱が焼失した。", "danger");
|
||||
worldRef?.log?.("\u5de3\u7bb1\u304c\u713c\u5931\u3057\u305f\u3002", "danger");
|
||||
worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.45, life: 0.34, color: "rgba(90,54,38,0.58)" }));
|
||||
worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-burnout");
|
||||
worldRef?.markItemBucketsDirty?.("nest-box-burnout");
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const GROUND_TYPE_ORDER = window.TarinaiDomainIds.GROUND_IDS;
|
||||
const GROUND_TYPES = Object.freeze({
|
||||
// \u30c7\u30d5\u30a9\u30eb\u30c8\u5730\u9762ID\u3002\u8868\u793a\u540d\u306f\u300c\u5730\u9762\u300d\u3002
|
||||
soil: Object.freeze({ id: "soil", label: "砂", description: "普通の砂地。", stressMultiplier: 1.0, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
soil: Object.freeze({ id: "soil", label: "\u7802", description: "\u666e\u901a\u306e\u7802\u5730\u3002", stressMultiplier: 1.0, grassMultiplier: 1.0, grassGrowthMultiplier: 1.0 }),
|
||||
dirt: Object.freeze({ id: "dirt", label: "\u571f", description: "\u8349\u304c\u3088\u304f\u80b2\u3064\u5730\u9762\u3002", stressMultiplier: 1.0, grassMultiplier: 1.75, grassGrowthMultiplier: 2.45 }),
|
||||
concrete: Object.freeze({ id: "concrete", label: "\u30b3\u30f3\u30af\u30ea\u30fc\u30c8", description: "\u8349\u304c\u751f\u3048\u306a\u3044", stressMultiplier: 1.0, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0 }),
|
||||
blanket: Object.freeze({ id: "blanket", label: "\u3075\u308f\u3075\u308f\u6bdb\u5e03", description: "\u30b9\u30c8\u30ec\u30b9\u304c\u5897\u3048\u306b\u304f\u3044\u3002\u8349\u306f\u751f\u3048\u306a\u3044", stressMultiplier: 0.5, grassMultiplier: 0.0, grassGrowthMultiplier: 0.0, grassLimit: 0 }),
|
||||
|
|
|
|||
|
|
@ -32,14 +32,16 @@ const HEALTH = (() => {
|
|||
dwarfDrug: "\u77ee\u5c0f\u85ac",
|
||||
freeze: "\u51CD\u7D50",
|
||||
heatstroke: "\u71B1\u4E2D\u75C7",
|
||||
cleaning: "掃除",
|
||||
cleaning: "\u6383\u9664",
|
||||
electrocution: "\u611f\u96fb",
|
||||
});
|
||||
const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/;
|
||||
|
||||
function causeLabel(reason = "") {
|
||||
const text = String(reason || "");
|
||||
if (!text) return "";
|
||||
if (/掃除|ロボ掃除機|robot.?cleaner|cleaned/.test(text)) return CAUSES.cleaning;
|
||||
if (/\u6383\u9664|\u30ed\u30dc\u6383\u9664\u6a5f|robot.?cleaner|cleaned/.test(text)) return CAUSES.cleaning;
|
||||
if (/\u611f\u96fb|\u901a\u96fb\u4e2d\u306e\u96fb\u7dda|electrocution|electric.?shock|shocked/i.test(text)) return CAUSES.electrocution;
|
||||
if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
if (/\u51CD\u7D50|\u4F4E\u6E29|\u5BD2\u3055|cold|freeze|freezing/i.test(text)) return CAUSES.freeze;
|
||||
if (/\u71B1\u4E2D\u75C7|\u9AD8\u6E29|\u6691\u3055|heat|hot/i.test(text)) return CAUSES.heatstroke;
|
||||
|
|
@ -273,6 +275,7 @@ const HEALTH = (() => {
|
|||
const jitter = (Math.random() - 0.5) * Math.max(4, (t.radius || 22) * 0.25);
|
||||
const x = t.x - side * (t.radius || 22) * (0.72 + Math.random() * 0.24);
|
||||
const y = t.y + (t.radius || 22) * (0.24 + Math.random() * 0.25) + jitter;
|
||||
// Damage-triggered release is involuntary and must never be redirected to a toilet.
|
||||
t.world.spawnZunchi?.(x, y, t);
|
||||
t.poopCount = (t.poopCount || 0) + 1;
|
||||
t.digest = Math.max(0, Number(t.digest || 0) - 1.0);
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@
|
|||
const searchRadius = Math.max(150, (item.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 120);
|
||||
const items = worldRef.nearbyItems(item.x, item.y, searchRadius) || [];
|
||||
const seen = new Set(items);
|
||||
// 大きな機構は中心が近傍セル外でも当たる可能性がある。
|
||||
// 全item走査ではなく、機構type bucketだけを追加確認する。
|
||||
// \u5927\u304d\u306a\u6a5f\u69cb\u306f\u4e2d\u5fc3\u304c\u8fd1\u508d\u30bb\u30eb\u5916\u3067\u3082\u5f53\u305f\u308b\u53ef\u80fd\u6027\u304c\u3042\u308b\u3002
|
||||
// \u5168item\u8d70\u67fb\u3067\u306f\u306a\u304f\u3001\u6a5f\u69cbtype bucket\u3060\u3051\u3092\u8ffd\u52a0\u78ba\u8a8d\u3059\u308b\u3002
|
||||
const mechanicalTypes = ["rotator", "poison_block", "reciprocator"];
|
||||
for (const type of mechanicalTypes) {
|
||||
const bucket = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType(type) : (worldRef.items || []);
|
||||
|
|
@ -448,7 +448,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
const WATER_BALLOON_SURPRISE_TEXT = "水風船が割れておどろいている。";
|
||||
const WATER_BALLOON_SURPRISE_TEXT = "\u6c34\u98a8\u8239\u304c\u5272\u308c\u3066\u304a\u3069\u308d\u3044\u3066\u3044\u308b\u3002";
|
||||
|
||||
function tarinaiOpennessValue(t) {
|
||||
const v = (typeof global.effectivePersonalityValue === "function")
|
||||
|
|
@ -577,7 +577,7 @@
|
|||
const d = Math.max(1, distXY(balloon.x, balloon.y, t.x, t.y));
|
||||
const strength = clamp(1 - d / 165, 0, 1);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.34 + strength * 0.44);
|
||||
if (typeof t.addStress === "function") t.addStress(0.18 + strength * 0.42, "水風船が割れて驚いた");
|
||||
if (typeof t.addStress === "function") t.addStress(0.18 + strength * 0.42, "\u6c34\u98a8\u8239\u304c\u5272\u308c\u3066\u9a5a\u3044\u305f");
|
||||
if (typeof t.spawnBubble === "function" && strength > 0.35) t.spawnBubble("!", { life: 0.72 });
|
||||
const awayX = (t.x - balloon.x) / d;
|
||||
const awayY = (t.y - balloon.y) / d;
|
||||
|
|
@ -628,13 +628,13 @@
|
|||
worldRef?.effects?.push(new Effect("fight", balloon.x, balloon.y, { size: deterministicRange(worldRef || balloon, "water-balloon-pop-size", 4, 10, balloon, i), life: 0.28, color: "rgba(95,204,246,0.72)", vx: Math.cos(a) * s, vy: Math.sin(a) * s }));
|
||||
}
|
||||
spawnWaterFromBalloon(balloon, worldRef);
|
||||
const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "火"
|
||||
: (cause?.type === "firecracker" ? "爆竹"
|
||||
: (cause?.type === "shoot" ? "射撃"
|
||||
: (cause?.type === "genkotsu" ? "げんこつ"
|
||||
: (cause && typeof isPinType === "function" && isPinType(cause.type) ? "鋲" : "強い衝撃"))));
|
||||
const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "\u706b"
|
||||
: (cause?.type === "firecracker" ? "\u7206\u7af9"
|
||||
: (cause?.type === "shoot" ? "\u5c04\u6483"
|
||||
: (cause?.type === "genkotsu" ? "\u3052\u3093\u3053\u3064"
|
||||
: (cause && typeof isPinType === "function" && isPinType(cause.type) ? "\u92f2" : "\u5f37\u3044\u885d\u6483"))));
|
||||
if (worldRef?.relationNotice?.(balloon.id || "balloon", cause?.id || sourceLabel, "water-balloon-pop", 1.4)) {
|
||||
worldRef.log?.(`水風船が${sourceLabel}に触れて割れた。`, "accident", { participants: [] });
|
||||
worldRef.log?.(`\u6c34\u98a8\u8239\u304c${sourceLabel}\u306b\u89e6\u308c\u3066\u5272\u308c\u305f\u3002`, "accident", { participants: [] });
|
||||
}
|
||||
applyWaterBalloonSurprise(balloon, worldRef);
|
||||
worldRef?.markSpatialDirty?.("water-balloon-pop");
|
||||
|
|
@ -643,6 +643,7 @@
|
|||
global.TarinaiItemDynamicBallSystem = Object.freeze({
|
||||
update,
|
||||
updateFan,
|
||||
fanWindAt,
|
||||
updateWaterDropMotion,
|
||||
resolveObstacleCollisions,
|
||||
popBalloon,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
}
|
||||
|
||||
|
||||
const STICKY_BOMB_PANIC_TEXT = "粘着ボムが付いてパニックになっている。";
|
||||
const STICKY_BOMB_PANIC_TEXT = "\u7c98\u7740\u30dc\u30e0\u304c\u4ed8\u3044\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b\u3002";
|
||||
const STICKY_BOMB_FUSE_SECONDS = 12.0;
|
||||
const STICKY_BOMB_BLAST_SCALE = 0.68; // identical to TOOL_SIZE_SCALES.small
|
||||
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
panicStickyBombCarrier(carrier, item, worldRef, true);
|
||||
if (old && old !== carrier) {
|
||||
old.surpriseTimer = Math.max(old.surpriseTimer || 0, 0.18);
|
||||
old.thought = "粘着ボムが移った";
|
||||
old.thought = "\u7c98\u7740\u30dc\u30e0\u304c\u79fb\u3063\u305f";
|
||||
}
|
||||
if (worldRef?.effects && typeof Effect !== "undefined") {
|
||||
worldRef.effects.push(new Effect("ring", item.x, item.y, { size: Math.max(16, (item.r || 16) * 1.45), life: 0.18, color: reason === "transfer" ? "rgba(255,210,72,0.52)" : "rgba(255,128,78,0.48)" }));
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
item.amount = 0;
|
||||
item.hp = 0;
|
||||
audio.explode?.();
|
||||
worldRef?.log?.("粘着ボムが爆発した。", "accident");
|
||||
worldRef?.log?.("\u7c98\u7740\u30dc\u30e0\u304c\u7206\u767a\u3057\u305f\u3002", "accident");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@
|
|||
sleep_drug: {
|
||||
label: "\u306d\u3080\u308a\u85ac", color: "rgba(104,84,168,0.78)", kind: "disease", tags: ["item", "disease", "behavior"],
|
||||
},
|
||||
first_aid: {
|
||||
label: "\u6551\u6025\u85ac", color: "rgba(210, 72, 82, 0.82)", kind: "instant",
|
||||
tags: ["item", "heal"], display: () => "\u5373\u6642 / HP\u56de\u5fa9\u306e\u307f",
|
||||
},
|
||||
sedative: {
|
||||
label: "\u93ae\u9759\u85ac", color: "rgba(92, 142, 174, 0.82)", kind: "timed", defaultDuration: 42,
|
||||
tags: ["item", "behavior"], display: timer => `${Math.ceil(timer || 0)}\u79d2 / \u6050\u6016\u3068\u3051\u3093\u304b\u6c17\u5206\u3092\u6291\u3048\u308b`,
|
||||
},
|
||||
mystery_drug: {
|
||||
label: "\u602a\u3057\u3044\u85ac", color: "rgba(60, 154, 87, 0.80)", kind: "instant", tags: ["item", "behavior"],
|
||||
},
|
||||
|
|
@ -103,6 +111,14 @@
|
|||
for (const id of ["love_mochi", "fight_mochi", "sleep_drug"]) {
|
||||
Object.assign(rawDefinitions[id], { mysteryEligible: true, zundaJuiceRemovable: id !== "sleep_drug" });
|
||||
}
|
||||
Object.assign(rawDefinitions.first_aid, { mysteryEligible: false, zundaJuiceRemovable: false });
|
||||
Object.assign(rawDefinitions.sedative, {
|
||||
modifiers: { fearMultiplier: 0.45, fightWeightMultiplier: 0.28, speedMultiplier: 0.72 },
|
||||
visualEffectId: "sedative",
|
||||
bubblePolicy: "none",
|
||||
mysteryEligible: true,
|
||||
zundaJuiceRemovable: true,
|
||||
});
|
||||
Object.assign(rawDefinitions.zunda_juice, { removesItemEffects: true, mysteryEligible: false });
|
||||
|
||||
Object.assign(rawDefinitions.love_mochi, {
|
||||
|
|
@ -150,6 +166,31 @@
|
|||
return tarinai.infectSleepDisease?.(context.item || null) || true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.first_aid, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const max = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100);
|
||||
const scale = Math.max(0.35, Number(context.scale ?? 1) || 1);
|
||||
const amount = Math.max(18, Math.round(max * 0.38 * scale));
|
||||
tarinai.recoverHealth?.(amount);
|
||||
tarinai.hpBarTimer = Math.max(tarinai.hpBarTimer || 0, 2.4);
|
||||
tarinai.spawnPowerItemEffect?.("first_aid", 0.42);
|
||||
tarinai.bubble?.("HP+", 2.2, "rgba(210,72,82,0.82)", { force: true });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.sedative, {
|
||||
onApply(tarinai, context = {}) {
|
||||
const duration = Math.round((this.defaultDuration || 42) * Math.max(0.35, Number(context.scale ?? 1) || 1));
|
||||
tarinai.setTimedItemEffect?.("sedative", duration);
|
||||
tarinai.fearTimer = Math.max(0, (tarinai.fearTimer || 0) - 2.4);
|
||||
tarinai.intimidatedTimer = Math.max(0, (tarinai.intimidatedTimer || 0) - 1.8);
|
||||
tarinai.fightMochiTimer = Math.max(0, (tarinai.fightMochiTimer || 0) - 10);
|
||||
tarinai.postConflictPeaceTimer = Math.max(tarinai.postConflictPeaceTimer || 0, 14);
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { safety: -18, social: -2 });
|
||||
tarinai.spawnPowerItemEffect?.("sedative", 0.36);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
Object.assign(rawDefinitions.laxative, {
|
||||
onApply(tarinai, context = {}) {
|
||||
return tarinai.setTimedItemEffect?.("laxative", Math.round((this.defaultDuration || 60) * (context.scale ?? 1)));
|
||||
|
|
@ -249,7 +290,7 @@
|
|||
return this.timerValue(tarinai, id) > 0.04;
|
||||
}
|
||||
activeSummary(tarinai = null) {
|
||||
const order = ["protein", "niteropu", "mercury", "giant_drug", "dwarf_drug", "laxative", "ammo", "love_mochi", "fight_mochi"];
|
||||
const order = ["protein", "niteropu", "mercury", "giant_drug", "dwarf_drug", "laxative", "ammo", "sedative", "love_mochi", "fight_mochi"];
|
||||
return order
|
||||
.filter(id => this.isActive(tarinai, id))
|
||||
.map(id => ({
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
|
||||
(function (global) {
|
||||
const rawDefinitions = {
|
||||
food: { label: "\u98df\u3079\u7269", nutrition: 18.0, hungerRelief: 1.0, interest: "normal", decayRateMultiplier: 1.0 },
|
||||
food: { label: "\u30da\u30c3\u30c8\u30d5\u30fc\u30c9", nutrition: 20.0, hungerRelief: 1.08, interest: "normal", decayRateMultiplier: 1.0 },
|
||||
sweet: { label: "\u305a\u3093\u3060\u9905", nutrition: 16.5, hungerRelief: 0.95, interest: "zunda_high", decayRateMultiplier: 0.82 },
|
||||
love_mochi: { label: "\u3078\u3053\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "love_mochi" },
|
||||
fight_mochi: { label: "\u3051\u3093\u304b\u9905", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "fight_mochi" },
|
||||
sleep_drug: { label: "\u306d\u3080\u308a\u85ac", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "sleep_drug" },
|
||||
first_aid: { label: "\u6551\u6025\u85ac", nutrition: 1.0, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "first_aid" },
|
||||
sedative: { label: "\u93ae\u9759\u85ac", nutrition: 11.0, hungerRelief: 0.12, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "sedative" },
|
||||
laxative: { label: "\u4e0b\u5264", nutrition: 6.8, hungerRelief: 0, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "laxative" },
|
||||
protein: { label: "\u30d7\u30ed\u30c6\u30a4\u30f3", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "protein" },
|
||||
niteropu: { label: "\u30f3\u30a4\u30c6\u30ed\u30d7", nutrition: 15.0, hungerRelief: 0.92, interest: "medicine_low", decayRateMultiplier: 1.0, effectId: "niteropu" },
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
if (!item || item.dead) return { done: true };
|
||||
item.age += dt;
|
||||
if (item.type === "zunchi") item.spawnGrace = Math.max(0, (item.spawnGrace || 0) - dt);
|
||||
if (item.type === "toilet") item.toiletFlash = Math.max(0, (item.toiletFlash || 0) - dt * 1.8);
|
||||
if (item.dropTimer > 0) {
|
||||
const beforeDrop = item.dropTimer;
|
||||
item.dropTimer = Math.max(0, item.dropTimer - dt);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// globals instead of carrying helper implementations inline.
|
||||
(function (global) {
|
||||
const DUPLICATOR_STORABLE_TYPES = new Set([
|
||||
"food", "sweet", "love_mochi", "fight_mochi", "sleep_drug",
|
||||
"food", "sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative",
|
||||
"laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug",
|
||||
"zunda_juice", "water", "pushpin", "oshibyo"
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -225,6 +225,101 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.fillStyle = "rgba(255,238,128,0.94)";
|
||||
ctx.fillRect(-r * 0.78, r * 0.20, r * 1.56, r * 0.24);
|
||||
ctx.restore();
|
||||
} else if (this.type === "toilet") {
|
||||
const r = this.r || 48;
|
||||
const flash = clamp(this.toiletFlash || 0, 0, 1);
|
||||
const footprint = globalThis.TarinaiToiletSandSystem?.footprint?.(this) || {
|
||||
centerX: this.x,
|
||||
centerY: this.y + r * 0.08,
|
||||
halfWidth: r * 1.55,
|
||||
halfHeight: r * 0.72,
|
||||
cornerRadius: r * 0.18,
|
||||
angle: Number(this.angle || 0) || 0,
|
||||
};
|
||||
const localY = footprint.centerY - this.y;
|
||||
ctx.save();
|
||||
ctx.rotate(footprint.angle || 0);
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(54,42,31,0.16)";
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.shadowOffsetY = 3;
|
||||
}
|
||||
ctx.fillStyle = styleNight ? "rgba(91,77,68,0.96)" : "rgba(143,116,91,0.96)";
|
||||
ctx.strokeStyle = styleNight ? "rgba(45,39,36,0.92)" : "rgba(79,62,49,0.90)";
|
||||
ctx.lineWidth = Math.max(1.8, r * 0.060);
|
||||
roundedRect(ctx, -footprint.halfWidth, localY - footprint.halfHeight, footprint.halfWidth * 2, footprint.halfHeight * 2, footprint.cornerRadius);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
const inset = Math.max(4, r * 0.14);
|
||||
ctx.fillStyle = styleNight ? "rgba(146,128,91,0.96)" : "rgba(218,190,128,0.98)";
|
||||
roundedRect(ctx, -footprint.halfWidth + inset, localY - footprint.halfHeight + inset, (footprint.halfWidth - inset) * 2, (footprint.halfHeight - inset) * 2, Math.max(2, footprint.cornerRadius * 0.62));
|
||||
ctx.fill();
|
||||
ctx.fillStyle = styleNight ? "rgba(176,156,111,0.72)" : "rgba(244,220,164,0.78)";
|
||||
for (const grain of [[-1.08,-0.20,0.055],[-0.62,0.21,0.045],[-0.18,-0.15,0.050],[0.28,0.19,0.042],[0.72,-0.18,0.052],[1.12,0.16,0.040]]) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(r * grain[0], localY + r * grain[1], Math.max(1, r * grain[2]), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
if (flash > 0.02) {
|
||||
ctx.globalAlpha *= flash * 0.42;
|
||||
ctx.fillStyle = "rgba(255,244,204,0.96)";
|
||||
roundedRect(ctx, -footprint.halfWidth + inset * 0.55, localY - footprint.halfHeight + inset * 0.55, (footprint.halfWidth - inset * 0.55) * 2, (footprint.halfHeight - inset * 0.55) * 2, Math.max(2, footprint.cornerRadius * 0.78));
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "rain_shelter") {
|
||||
const r = this.r || 50;
|
||||
const footprint = globalThis.TarinaiParasolSystem?.parasolFootprint?.(this) || {
|
||||
centerX: this.x,
|
||||
centerY: this.y + r * 0.84,
|
||||
radiusX: r * 1.58,
|
||||
radiusY: r * 0.68,
|
||||
};
|
||||
ctx.save();
|
||||
ctx.fillStyle = styleNight ? "rgba(18,18,28,0.26)" : "rgba(54,48,54,0.20)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(footprint.centerX - this.x, footprint.centerY - this.y, footprint.radiusX, footprint.radiusY, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (itemShadowsEnabled()) {
|
||||
ctx.shadowColor = "rgba(54,42,31,0.18)";
|
||||
ctx.shadowBlur = 7;
|
||||
ctx.shadowOffsetY = 5;
|
||||
}
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = styleNight ? "rgba(70,70,78,0.90)" : "rgba(92,86,76,0.82)";
|
||||
ctx.lineWidth = Math.max(2.2, r * 0.065);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -r * 0.55);
|
||||
ctx.lineTo(0, r * 0.84);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r * 0.24, r * 0.84);
|
||||
ctx.lineTo(r * 0.24, r * 0.84);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = styleNight ? "rgba(116,76,112,0.96)" : "rgba(225,104,126,0.96)";
|
||||
ctx.strokeStyle = styleNight ? "rgba(62,42,68,0.92)" : "rgba(126,54,72,0.84)";
|
||||
ctx.lineWidth = Math.max(1.8, r * 0.065);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-r * 1.12, -r * 0.24);
|
||||
ctx.quadraticCurveTo(-r * 0.72, -r * 0.98, 0, -r * 1.10);
|
||||
ctx.quadraticCurveTo(r * 0.72, -r * 0.98, r * 1.12, -r * 0.24);
|
||||
ctx.quadraticCurveTo(r * 0.76, -r * 0.06, r * 0.50, -r * 0.24);
|
||||
ctx.quadraticCurveTo(r * 0.24, -r * 0.04, 0, -r * 0.24);
|
||||
ctx.quadraticCurveTo(-r * 0.24, -r * 0.04, -r * 0.50, -r * 0.24);
|
||||
ctx.quadraticCurveTo(-r * 0.76, -r * 0.06, -r * 1.12, -r * 0.24);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.strokeStyle = styleNight ? "rgba(210,184,205,0.44)" : "rgba(255,220,224,0.62)";
|
||||
ctx.lineWidth = Math.max(1, r * 0.025);
|
||||
for (const x of [-0.50, 0, 0.50]) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -r * 1.08);
|
||||
ctx.quadraticCurveTo(r * x * 0.72, -r * 0.68, r * x, -r * 0.24);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
} else if (this.type === "water") {
|
||||
drawOvalWaterDropSprite(ctx, this.r * visualFieldScaleFor("water"), visualPaletteFor("water"));
|
||||
} else if (this.type === "grass") {
|
||||
|
|
@ -1139,7 +1234,8 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.strokeStyle = active ? "#9b6512" : "#69717a"; ctx.lineWidth=3;
|
||||
ctx.beginPath(); ctx.roundRect?.(-27,-18,54,36,8); if (!ctx.roundRect) { ctx.rect(-27,-18,54,36); } ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = active ? "#fff6c9" : "#7b838d"; ctx.beginPath(); ctx.arc(0,-3,9,0,Math.PI*2); ctx.fill();
|
||||
ctx.fillStyle = styleNight ? "#eef2f6" : "#26323e"; ctx.font="bold 10px ui-rounded, sans-serif"; ctx.textAlign="center"; ctx.fillText(`${this.pressureTarget === "item" ? "物" : "た"}${this.pressureThreshold || 1}`,0,14);
|
||||
const detectorLabel = this.pressureTarget === "time" ? "\u6642\u523b" : `${this.pressureTarget === "item" ? "\u7269" : "\u305f"}${this.pressureThreshold || 1}`;
|
||||
ctx.fillStyle = styleNight ? "#eef2f6" : "#26323e"; ctx.font="bold 10px ui-rounded, sans-serif"; ctx.textAlign="center"; ctx.fillText(detectorLabel,0,14);
|
||||
ctx.restore();
|
||||
} else if (this.type === "rotator") {
|
||||
const angle = itemAngleFor(this);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
observe: { id: "observe", label: "\u305f\u308a\u306a\u3044\u30c7\u30fc\u30bf", placeable: false, scalable: false, icon: "assets/ui/tool_observe.webp", tooltip: "\u500b\u4f53\u3092\u9078\u629e\u3057\u3001\u72b6\u614b\u30fb\u6027\u683c\u30fb\u75c5\u6c17\u30fb\u304a\u6c17\u306b\u5165\u308a\u3092\u78ba\u8a8d\u3059\u308b\u3002" },
|
||||
clear_tool: { id: "clear_tool", label: "\u89e3\u9664", placeable: false, scalable: false, iconText: "\u2715", tooltip: "\u9078\u629e\u4e2d\u306e\u9053\u5177\u30dc\u30bf\u30f3\u3092\u89e3\u9664\u3057\u3001\u901a\u5e38\u306e\u89b3\u5bdf\u72b6\u614b\u306b\u623b\u3059\u3002" },
|
||||
delete: { id: "delete", label: "\u524a\u9664", placeable: false, scalable: false, icon: "assets/ui/tool_delete.webp", tooltip: "\u7f6e\u3044\u305f\u9053\u5177\u3084\u6c5a\u308c\u3092\u6d88\u3059\u3002" },
|
||||
area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, iconText: "▦", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" },
|
||||
area_delete: { id: "area_delete", label: "\u7bc4\u56f2\u524a\u9664", placeable: false, scalable: false, iconText: "\u25a6", tooltip: "\u30ab\u30fc\u30bd\u30eb\u5468\u56f2\u306e\u5e83\u3044\u7bc4\u56f2\u306b\u3042\u308b\u7f6e\u304d\u3082\u306e\u3084\u6c5a\u308c\u3092\u307e\u3068\u3081\u3066\u6d88\u3059\u3002" },
|
||||
copy: { id: "copy", label: "\u30b3\u30d4\u30fc", placeable: false, scalable: false, iconText: "\u29c9", tooltip: "\u7f6e\u3044\u305f\u30a2\u30a4\u30c6\u30e0\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u30b3\u30d4\u30fc\u3057\u3001\u6b21\u306e\u30af\u30ea\u30c3\u30af\u3067\u8a2d\u5b9a\u3054\u3068\u8cbc\u308a\u4ed8\u3051\u308b\u3002" },
|
||||
undo: { id: "undo", label: "\u623b\u3059", placeable: false, scalable: false, iconText: "\u21b6", tooltip: "\u76f4\u524d\u306e\u64cd\u4f5c\u3092\u53d6\u308a\u6d88\u3059\u3002" },
|
||||
redo: { id: "redo", label: "\u9032\u3080", placeable: false, scalable: false, iconText: "\u21b7", tooltip: "\u53d6\u308a\u6d88\u3057\u305f\u64cd\u4f5c\u3092\u3084\u308a\u76f4\u3059\u3002" },
|
||||
|
|
@ -16,9 +16,9 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
water_hose: { id: "water_hose", label: "\u6d17\u6d44", placeable: false, scalable: false, iconText: "\u{1F6BF}", tooltip: "\u30c9\u30e9\u30c3\u30b0\u3067\u6c5a\u308c\u3084\u706b\u3092\u304d\u308c\u3044\u306b\u3059\u308b\u3002" },
|
||||
rope: { id: "rope", label: "\u30d2\u30e2", placeable: false, scalable: false, iconText: "\u2570", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3075\u306b\u3083\u3075\u306b\u3083\u3057\u305f\u30d2\u30e2\u3067\u7e4b\u3050\u3002" },
|
||||
rod: { id: "rod", label: "\u68d2", placeable: false, scalable: false, iconText: "\u2501", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u66f2\u304c\u3089\u306a\u3044\u68d2\u3067\u63a5\u7d9a\u3059\u308b\u3002" },
|
||||
spring: { id: "spring", label: "バネ", placeable: false, scalable: false, iconText: "≋", tooltip: "2つの対象を順にクリックして、伸縮するバネでつなぐ。" },
|
||||
wire: { id: "wire", label: "電線", placeable: false, scalable: false, iconText: "⌁", tooltip: "検知器の信号を電気で伝える。触ると感電。" },
|
||||
insulated_wire: { id: "insulated_wire", label: "絶縁電線", placeable: false, scalable: false, iconText: "⌁", tooltip: "検知器の信号を電気で伝える。感電しない。" },
|
||||
spring: { id: "spring", label: "\u30d0\u30cd", placeable: false, scalable: false, iconText: "\u224b", tooltip: "2\u3064\u306e\u5bfe\u8c61\u3092\u9806\u306b\u30af\u30ea\u30c3\u30af\u3057\u3066\u3001\u4f38\u7e2e\u3059\u308b\u30d0\u30cd\u3067\u3064\u306a\u3050\u3002" },
|
||||
wire: { id: "wire", label: "\u96fb\u7dda", placeable: false, scalable: false, iconText: "\u2301", tooltip: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3092\u96fb\u6c17\u3067\u4f1d\u3048\u308b\u3002\u89e6\u308b\u3068\u611f\u96fb\u3002" },
|
||||
insulated_wire: { id: "insulated_wire", label: "\u7d76\u7e01\u96fb\u7dda", placeable: false, scalable: false, iconText: "\u2301", tooltip: "\u691c\u77e5\u5668\u306e\u4fe1\u53f7\u3092\u96fb\u6c17\u3067\u4f1d\u3048\u308b\u3002\u611f\u96fb\u3057\u306a\u3044\u3002" },
|
||||
shoot: { id: "shoot", label: "\u5c04\u6483", placeable: false, scalable: false, icon: "assets/ui/tool_shoot.png", tooltip: "\u9283\u3067\u653b\u6483\u3059\u308b\u3002" },
|
||||
robot_cleaner: { id: "robot_cleaner", itemType: "robot_cleaner", label: "\u30ed\u30dc\u6383\u9664\u6a5f", placeable: true, scalable: false, radius: 22, amount: 999, iconText: "\u25c9", tooltip: "\u30b4\u30df\u3084\u6c5a\u308c\u3092\u7247\u4ed8\u3051\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\u3002" },
|
||||
zunchi: { id: "zunchi", itemType: "zunchi", label: "\u305a\u3093\u3061", placeable: true, scalable: true, radius: 14, amount: 240, icon: "assets/ui/tool_zunchi.webp", tooltip: "\u305f\u308a\u306a\u3044\u306e\u305a\u3093\u3061\u3002\u75c5\u6c17\u3084\u8349\u306e\u80a5\u6599\u306b\u95a2\u308f\u308b\u3002" },
|
||||
|
|
@ -43,19 +43,19 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
genkotsu: { id: "genkotsu", itemType: "genkotsu", label: "\u3052\u3093\u3053\u3064", placeable: true, scalable: false, radius: 88, amount: 100, icon: "assets/ui/tool_genkotsu.webp", tooltip: "\u62f3\u3092\u5730\u9762\u306b\u305f\u305f\u304d\u3064\u3051\u308b\u3002" },
|
||||
bed: { id: "bed", itemType: "bed", label: "\u5e72\u8349\u5bdd\u5e8a", placeable: true, scalable: false, radius: 37, amount: 999, icon: "assets/ui/tool_bed.webp", tooltip: "\u8fd1\u304f\u3067\u7720\u308b\u3002\u4f53\u529b\u304c\u56de\u5fa9\u3059\u308b\u3002" },
|
||||
nest_box: { id: "nest_box", itemType: "nest_box", label: "\u5de3\u7bb1", placeable: true, scalable: false, radius: 42, amount: 999, icon: "assets/ui/tool_nest_box.webp", collisionShape: "nest_box_3x3", tooltip: "\u3044\u308b\u3060\u3051\u3067\u30b9\u30c8\u30ec\u30b9\u4f4e\u6e1b\u3002\u7720\u308b\u3068\u7761\u7720\u306e\u8cea\u304c\u4e0a\u304c\u308b\u3002" },
|
||||
pipe: { id: "pipe", itemType: "pipe", label: "\u571f\u7ba1", placeable: true, scalable: false, radius: 36, amount: 999, iconText: "\u25ce", collisionShape: "circle", tooltip: "移動できる地下空間。たりないが眠ることもある。" },
|
||||
pipe: { id: "pipe", itemType: "pipe", label: "\u571f\u7ba1", placeable: true, scalable: false, radius: 36, amount: 999, iconText: "\u25ce", collisionShape: "circle", tooltip: "\u79fb\u52d5\u3067\u304d\u308b\u5730\u4e0b\u7a7a\u9593\u3002\u305f\u308a\u306a\u3044\u304c\u7720\u308b\u3053\u3068\u3082\u3042\u308b\u3002" },
|
||||
dry_ice: { id: "dry_ice", itemType: "dry_ice", label: "\u30C9\u30E9\u30A4\u30A2\u30A4\u30B9", placeable: true, scalable: false, radius: 21, amount: 999, iconText: "\uD83E\uDDCA", collisionShape: "circle", tooltip: "\u5468\u56F2\u306E\u6C17\u6E29\u309210\u2103\u4E0B\u3052\u308B\u3002\u52B9\u679C\u306F\u91CD\u306D\u3089\u308C\u308B\u3002" },
|
||||
stove: { id: "stove", itemType: "stove", label: "\u30B9\u30C8\u30FC\u30D6", placeable: true, scalable: false, radius: 24, amount: 999, iconText: "\uD83D\uDD25", collisionShape: "circle", tooltip: "\u5468\u56F2\u306E\u6C17\u6E29\u309210\u2103\u4E0A\u3052\u308B\u3002\u52B9\u679C\u306F\u91CD\u306D\u3089\u308C\u308B\u3002" },
|
||||
ant_nest: { id: "ant_nest", itemType: "ant_nest", label: "\u30a2\u30ea\u306e\u5de3", placeable: true, scalable: false, radius: 17, amount: 999, icon: "assets/ui/tool_ant_nest.webp", collisionShape: "circle", tooltip: "\u50cd\u304d\u30a2\u30ea\u304c\u305f\u308a\u306a\u3044\u3092\u63a2\u3057\u3001\u5de3\u3078\u904b\u3076\u3002" },
|
||||
ball: { id: "ball", itemType: "ball", label: "\u30dc\u30fc\u30eb", placeable: true, scalable: false, radius: 18, amount: 999, icon: "assets/ui/tool_ball.webp", iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\u3002" },
|
||||
balloon: { id: "balloon", itemType: "balloon", label: "水風船", placeable: true, scalable: false, radius: 20, amount: 999, iconText: "💧", collisionShape: "circle", tooltip: "たりないのおもちゃ。割れると水滴が飛び散る。" },
|
||||
fan: { id: "fan", itemType: "fan", label: "扇風機", placeable: true, scalable: false, radius: 26, amount: 999, iconText: "🌀", collisionShape: "circle", tooltip: "前方に風を出す。軽いものを押し、少し涼しくする。Q/Eで置く向きを変える。配置後にクリックすると首振り設定を開く。" },
|
||||
balloon: { id: "balloon", itemType: "balloon", label: "\u6c34\u98a8\u8239", placeable: true, scalable: false, radius: 20, amount: 999, iconText: "\ud83d\udca7", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u306e\u304a\u3082\u3061\u3083\u3002\u5272\u308c\u308b\u3068\u6c34\u6ef4\u304c\u98db\u3073\u6563\u308b\u3002" },
|
||||
fan: { id: "fan", itemType: "fan", label: "\u6247\u98a8\u6a5f", placeable: true, scalable: false, radius: 26, amount: 999, iconText: "\ud83c\udf00", collisionShape: "circle", tooltip: "\u524d\u65b9\u306b\u98a8\u3092\u51fa\u3059\u3002\u8efd\u3044\u3082\u306e\u3092\u62bc\u3057\u3001\u5c11\u3057\u6dbc\u3057\u304f\u3059\u308b\u3002Q/E\u3067\u7f6e\u304f\u5411\u304d\u3092\u5909\u3048\u308b\u3002\u914d\u7f6e\u5f8c\u306b\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u9996\u632f\u308a\u8a2d\u5b9a\u3092\u958b\u304f\u3002" },
|
||||
signboard: { id: "signboard", itemType: "signboard", label: "\u770b\u677f", placeable: true, scalable: false, radius: 30, amount: 999, icon: "assets/ui/tool_signboard.webp", collisionShape: "circle", tooltip: "\u8a2d\u7f6e\u5f8c\u306b\u30af\u30ea\u30c3\u30af\u3059\u308b\u3068\u6587\u5b57\u3092\u66f8\u304d\u8fbc\u3081\u308b\u770b\u677f\u30026\u6587\u5b57\u00d73\u884c\u307e\u3067\u3002" },
|
||||
duplicator: { id: "duplicator", itemType: "duplicator", label: "\u8907\u88fd\u6a5f", placeable: true, scalable: false, radius: 24, amount: 999, icon: "assets/ui/tool_duplicator.webp", collisionShape: "circle", tooltip: "\u98df\u3079\u7269\u3001\u85ac\u3001\u92f2\u3092\u30bb\u30c3\u30c8\u3059\u308b\u3068\u7121\u9650\u306b\u4f9b\u7d66\u3067\u304d\u308b\u3002" },
|
||||
firecracker: { id: "firecracker", itemType: "firecracker", label: "\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, icon: "assets/ui/tool_firecracker.webp", tooltip: "\u7206\u767a\u3092\u8d77\u3053\u3059\u3002" },
|
||||
fire: { id: "fire", itemType: "fire", label: "\u708e", placeable: false, scalable: false, radius: 18, amount: 140, iconText: "\uD83D\uDD25", collisionShape: "circle", simulationOnly: true },
|
||||
flame_firecracker: { id: "flame_firecracker", itemType: "flame_firecracker", label: "\u706b\u708e\u7206\u7af9", placeable: true, scalable: true, radius: 15, amount: 999, iconText: "\uD83E\uDDE8", collisionShape: "circle", tooltip: "\u5468\u56f2\u306b\u708e\u3092\u6492\u304d\u6563\u3089\u3059\u3002" },
|
||||
sticky_bomb: { id: "sticky_bomb", itemType: "sticky_bomb", label: "粘着ボム", placeable: true, scalable: false, radius: 16, amount: 999, iconText: "\uD83D\uDCA3", collisionShape: "circle", tooltip: "\u4e00\u5b9a\u6642\u9593\u3067\u7206\u767a\u3059\u308b\u7206\u5f3e\u3002\u4ed8\u7740\u3057\u305f\u500b\u4f53\u306f\u30d1\u30cb\u30c3\u30af\u306b\u306a\u308b\u3002\u4ed6\u500b\u4f53\u306b\u89e6\u308c\u308b\u3068\u30dc\u30e0\u304c\u305d\u3063\u3061\u306b\u79fb\u52d5\u3059\u308b\u3002" },
|
||||
sticky_bomb: { id: "sticky_bomb", itemType: "sticky_bomb", label: "\u7c98\u7740\u30dc\u30e0", placeable: true, scalable: false, radius: 16, amount: 999, iconText: "\uD83D\uDCA3", collisionShape: "circle", tooltip: "\u4e00\u5b9a\u6642\u9593\u3067\u7206\u767a\u3059\u308b\u7206\u5f3e\u3002\u4ed8\u7740\u3057\u305f\u500b\u4f53\u306f\u30d1\u30cb\u30c3\u30af\u306b\u306a\u308b\u3002\u4ed6\u500b\u4f53\u306b\u89e6\u308c\u308b\u3068\u30dc\u30e0\u304c\u305d\u3063\u3061\u306b\u79fb\u52d5\u3059\u308b\u3002" },
|
||||
pushpin: { id: "pushpin", itemType: "pushpin", label: "\u753b\u92f2", placeable: true, scalable: false, radius: 8, amount: 999, icon: "assets/ui/tool_pushpin.webp", tooltip: "\u843d\u3068\u3059\u3068\u3053\u308d\u304c\u308a\u3001\u305f\u308a\u306a\u3044\u306b\u523a\u3055\u308b\u3068\u30d1\u30cb\u30c3\u30af\u3068\u7d99\u7d9a\u30c0\u30e1\u30fc\u30b8\u3092\u4e0e\u3048\u308b\u3002" },
|
||||
oshibyo: { id: "oshibyo", itemType: "oshibyo", label: "\u304a\u3057\u308a\u92f2", placeable: true, scalable: false, radius: 9, amount: 999, icon: "assets/ui/tool_oshibyo.webp", tooltip: "\u523a\u3055\u308b\u3068\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u308b\u3002\u3064\u307e\u3093\u3067\u629c\u304f\u3068\u6e9c\u307e\u3063\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3076\u3002" },
|
||||
fence_v: { id: "fence_v", itemType: "fence_v", label: "\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" },
|
||||
|
|
@ -64,16 +64,20 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
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\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" },
|
||||
one_way_fence: { id: "one_way_fence", itemType: "one_way_fence", label: "一方通行柵", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "⇧", collisionShape: "oriented_rect", tooltip: "矢印方向には通過でき、逆方向からは通れない柵。Q/Eで45度、Shift+Q/Eで5度ずつ向きを変える。" },
|
||||
one_way_fence: { id: "one_way_fence", itemType: "one_way_fence", label: "\u4e00\u65b9\u901a\u884c\u67f5", placeable: true, scalable: true, radius: 42, amount: 999, iconText: "\u21e7", collisionShape: "oriented_rect", tooltip: "\u77e2\u5370\u65b9\u5411\u306b\u306f\u901a\u904e\u3067\u304d\u3001\u9006\u65b9\u5411\u304b\u3089\u306f\u901a\u308c\u306a\u3044\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" },
|
||||
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" },
|
||||
pressure_switch: { id: "pressure_switch", itemType: "pressure_switch", label: "検知器", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "▣", collisionShape: "circle", tooltip: "表示された矩形範囲内にいるたりない、またはアイテム数を検知して電線へ信号を送る。クリックで対象・必要数・範囲を設定。" },
|
||||
pressure_switch: { id: "pressure_switch", itemType: "pressure_switch", label: "\u691c\u77e5\u5668", placeable: true, scalable: false, radius: 34, amount: 999, iconText: "\u25a3", collisionShape: "circle", tooltip: "\u69d8\u3005\u306a\u72b6\u6cc1\u3092\u691c\u77e5\u3057\u3066\u96fb\u7dda\u306b\u4fe1\u53f7\u3092\u9001\u308b\u3002\u30af\u30ea\u30c3\u30af\u3067\u8a73\u7d30\u8a2d\u5b9a\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 },
|
||||
splat: { id: "splat", itemType: "splat", label: "\u3057\u3076\u304d", placeable: false, scalable: false, radius: 26, amount: 260, simulationOnly: true },
|
||||
food: { id: "food", itemType: "food", label: "\u98df\u3079\u7269", placeable: false, scalable: false, radius: 14, amount: 85, simulationOnly: true },
|
||||
food: { id: "food", itemType: "food", label: "\u30da\u30c3\u30c8\u30d5\u30fc\u30c9", placeable: true, scalable: true, radius: 14, amount: 85, iconText: "\ud83e\udd63", tooltip: "\u7279\u6b8a\u52b9\u679c\u306e\u306a\u3044\u666e\u901a\u306e\u3054\u306f\u3093\u3002" },
|
||||
ant_corpse: { id: "ant_corpse", itemType: "ant_corpse", label: "\u30a2\u30ea\u306e\u6b7b\u9ab8", placeable: false, scalable: false, radius: 4, amount: 24, simulationOnly: true },
|
||||
toilet: { id: "toilet", itemType: "toilet", label: "\u30c8\u30a4\u30ec\u306e\u7802", placeable: true, scalable: true, radius: 48, amount: 999, iconText: "\u25a3", tooltip: "\u4e0a\u306b\u6392\u6cc4\u3055\u308c\u305f\u305a\u3093\u3061\u304c\u76f4\u3061\u306b\u6d88\u6ec5\u3059\u308b\u3002" },
|
||||
rain_shelter: { id: "rain_shelter", itemType: "rain_shelter", label: "\u30d1\u30e9\u30bd\u30eb", placeable: true, scalable: true, radius: 50, amount: 999, iconText: "\u26f1", tooltip: "\u96e8\u3068\u65e5\u5dee\u3057\u3092\u9632\u304e\u3001\u4f53\u611f\u6c17\u6e29\u3092\u7a4f\u3084\u304b\u306b\u3059\u308b\u3002" },
|
||||
first_aid: { id: "first_aid", itemType: "first_aid", label: "\u6551\u6025\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, iconText: "\u271a", tooltip: "\u98df\u3079\u308b\u3068HP\u3092\u5373\u6642\u56de\u5fa9\u3059\u308b\u3002" },
|
||||
sedative: { id: "sedative", itemType: "sedative", label: "\u93ae\u9759\u85ac", placeable: true, scalable: true, radius: 13, amount: 58, iconText: "\u25cc", tooltip: "\u98df\u3079\u308b\u3068\u6050\u6016\u3084\u3051\u3093\u304b\u6c17\u5206\u3092\u843d\u3061\u7740\u304b\u305b\u308b\u3002" },
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
// Static item/tool metadata shared by tool definitions and UI helpers.
|
||||
(function (global) {
|
||||
const ITEM_TRAITS = Object.freeze({
|
||||
obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]),
|
||||
food_interest: new Set(["sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]),
|
||||
obstacle: new Set(["stone", "ball", "balloon", "fan", "nest_box", "bed", "duplicator", "dry_ice", "stove", "rain_shelter", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]),
|
||||
food_interest: new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunchi", "water", "ant_corpse", "duplicator", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "slave_chain", "golden_crown", "sleep_drug"]),
|
||||
hazard: new Set(["firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu", "pushpin", "poison_block", "sleep_drug", "zunchi", "splat", "mystery_drug", "laxative", "niteropu", "mercury", "giant_drug", "dwarf_drug"]),
|
||||
pin: new Set(["pushpin", "oshibyo"]),
|
||||
kinematic: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi"]),
|
||||
freeStackingMovable: new Set(["ball", "balloon", "pushpin", "oshibyo", "zunchi", "water", "zunda_juice", "mercury", "genkotsu", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "ant_corpse"]),
|
||||
sleepFurniture: new Set(["bed", "nest_box", "pipe", "grass_bed"]),
|
||||
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]),
|
||||
draggable: new Set(["ball", "balloon", "fan", "stone", "firecracker", "flame_firecracker", "sticky_bomb", "bed", "nest_box", "pipe", "dry_ice", "stove", "rain_shelter", "signboard", "duplicator", "robot_cleaner", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "pressure_switch"]),
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -25,9 +26,9 @@
|
|||
|
||||
const TOOL_CATEGORIES = Object.freeze([
|
||||
{ id: "operate", label: "\u64cd\u4f5c", themeClass: "tool-category-operate", order: 10, toolIds: ["observe", "new", "poke", "pinch", "copy", "undo", "redo", "delete", "area_delete", "water_hose", "clear_tool"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "signboard"] },
|
||||
{ id: "food", label: "\u98df\u3079\u7269", themeClass: "tool-category-food", order: 20, toolIds: ["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice", "duplicator"] },
|
||||
{ id: "medicine", label: "\u85ac", themeClass: "tool-category-medicine", order: 30, toolIds: ["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "slave_chain", "golden_crown"] },
|
||||
{ id: "habitat", label: "\u751f\u6d3b", themeClass: "tool-category-habitat", order: 40, toolIds: ["zunchi", "toilet", "rain_shelter", "robot_cleaner", "bed", "nest_box", "pipe", "dry_ice", "stove", "ball", "balloon", "fan", "signboard"] },
|
||||
{ id: "physics", label: "\u7269\u7406", themeClass: "tool-category-physics", order: 45, toolIds: PHYSICS_TOOL_IDS },
|
||||
{ id: "hazard", label: "\u5371\u967a", themeClass: "tool-category-hazard", order: 50, toolIds: ["stone", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "pushpin", "oshibyo", "shoot", "ant_nest"] },
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,39 @@
|
|||
|
||||
// Type-specific Item constructor state. Kept out of items.js so construction stays focused on shared fields.
|
||||
|
||||
// Shared toilet-sand geometry. Rendering and poop absorption must use the
|
||||
// same rounded rectangle so the visible sand and active area never diverge.
|
||||
function toiletSandFootprint(item = null) {
|
||||
if (!item || item.dead || item.type !== "toilet") return null;
|
||||
const r = Math.max(24, Number(item.r || 48) || 48);
|
||||
return {
|
||||
centerX: Number(item.x || 0),
|
||||
centerY: Number(item.y || 0) + r * 0.08,
|
||||
halfWidth: r * 1.55,
|
||||
halfHeight: r * 0.72,
|
||||
cornerRadius: r * 0.18,
|
||||
angle: Number(item.angle || 0) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
function toiletSandContains(item = null, x = 0, y = 0) {
|
||||
const footprint = toiletSandFootprint(item);
|
||||
if (!footprint) return false;
|
||||
const dx = Number(x || 0) - footprint.centerX;
|
||||
const dy = Number(y || 0) - footprint.centerY;
|
||||
const cos = Math.cos(footprint.angle);
|
||||
const sin = Math.sin(footprint.angle);
|
||||
const lx = dx * cos + dy * sin;
|
||||
const ly = -dx * sin + dy * cos;
|
||||
const innerX = Math.max(0, footprint.halfWidth - footprint.cornerRadius);
|
||||
const innerY = Math.max(0, footprint.halfHeight - footprint.cornerRadius);
|
||||
const qx = Math.max(0, Math.abs(lx) - innerX);
|
||||
const qy = Math.max(0, Math.abs(ly) - innerY);
|
||||
return qx * qx + qy * qy <= footprint.cornerRadius * footprint.cornerRadius;
|
||||
}
|
||||
|
||||
globalThis.TarinaiToiletSandSystem = Object.freeze({ footprint: toiletSandFootprint, contains: toiletSandContains });
|
||||
|
||||
function initializeItemTypeState(item, type, x, y) {
|
||||
if (!item) return item;
|
||||
if (isServingFoodType(type) || ["grass", "ant_corpse", "zunchi"].includes(type)) item.roles.food = true;
|
||||
|
|
@ -22,6 +55,14 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
if (type === "slave_chain" || type === "golden_crown") {
|
||||
item.amount = Math.max(1, Number(item.amount || 0) || 1);
|
||||
}
|
||||
if (type === "toilet") {
|
||||
item.amount = 999;
|
||||
item.toiletFlash = Math.max(0, Number(item.toiletFlash || 0) || 0);
|
||||
}
|
||||
if (type === "rain_shelter") {
|
||||
item.amount = 999;
|
||||
item.rainShelterRange = Math.max(76, (item.r || 50) * 1.58);
|
||||
}
|
||||
if (type === "bed") {
|
||||
item.comfort = rand(0.86, 1.14);
|
||||
item.wear = 0;
|
||||
|
|
@ -220,6 +261,8 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.pressureThreshold = 1;
|
||||
item.pressureWidth = 220;
|
||||
item.pressureHeight = 160;
|
||||
item.pressureTimeStart = 360;
|
||||
item.pressureTimeEnd = 1080;
|
||||
item.signalActive = false;
|
||||
item.deletable = true;
|
||||
item.amount = 999;
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
const SCHEDULED_ITEM_TYPES = Object.freeze([
|
||||
"ball", "balloon", "fan", "genkotsu", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "pushpin", "oshibyo",
|
||||
"plushie", "grass_bed", "zunchi", "robot_cleaner", "duplicator", "water", "trace", "splat", "ant_corpse", "food",
|
||||
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
|
||||
"stone", "bed", "nest_box", "pipe", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch",
|
||||
"sweet", "love_mochi", "fight_mochi", "sleep_drug", "first_aid", "sedative", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
|
||||
"stone", "bed", "nest_box", "pipe", "toilet", "rain_shelter", "ant_nest", "signboard", "fence_v", "fence_h", "bounce_fence", "bounce_fence_v", "gate_fence", "one_way_fence", "rotator", "poison_block", "reciprocator", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch",
|
||||
]);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,24 @@
|
|||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["tablet", "moon", "purple"]
|
||||
},
|
||||
"first_aid": {
|
||||
"renderer": "split_pill",
|
||||
"shape": "cross_tablet",
|
||||
"palette": { "fill": "#fff1f1", "stroke": "rgba(180,55,65,0.78)", "highlight": "rgba(220,64,78,0.82)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["tablet", "cross", "red"]
|
||||
},
|
||||
"sedative": {
|
||||
"renderer": "split_pill",
|
||||
"shape": "calm_tablet",
|
||||
"palette": { "fill": "#edf7ff", "stroke": "rgba(58,104,142,0.78)", "highlight": "rgba(92,142,174,0.82)" },
|
||||
"scale": 1,
|
||||
"uiPreviewScale": 1,
|
||||
"fieldScale": 1,
|
||||
"accessibleShapeTraits": ["tablet", "calm", "blue"]
|
||||
},
|
||||
"laxative": {
|
||||
"renderer": "laxative_tablet",
|
||||
"shape": "rounded_tablet_zunchi_motif",
|
||||
|
|
|
|||
32
js/items.js
32
js/items.js
|
|
@ -281,7 +281,25 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
|
|||
const localR = Math.max(1, Number(r) || 10);
|
||||
const compact = !!opts.compact;
|
||||
ctx.save();
|
||||
if (key === "food" || key === "sweet" || key === "love_mochi" || key === "fight_mochi") {
|
||||
if (key === "food") {
|
||||
ctx.fillStyle = "#b97842";
|
||||
ctx.strokeStyle = "#754725";
|
||||
ctx.lineWidth = Math.max(1.2, localR * 0.14);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, localR * 0.16, localR * 1.18, localR * 0.48, 0, 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = "#d9aa70";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, -localR * 0.02, localR * 0.92, localR * 0.42, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
const dots = compact ? 5 : 7;
|
||||
ctx.fillStyle = "#7d4d2c";
|
||||
for (let i = 0; i < dots; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(randSeed(localSeed + i, -localR * 0.62, localR * 0.62), randSeed(localSeed + i + 9, -localR * 0.25, localR * 0.20), Math.max(1.1, localR * 0.14), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
} else if (key === "sweet" || key === "love_mochi" || key === "fight_mochi") {
|
||||
const sweet = key === "sweet";
|
||||
const loveMochi = key === "love_mochi";
|
||||
const fightMochi = key === "fight_mochi";
|
||||
|
|
@ -319,14 +337,6 @@ function drawConsumableFieldSprite(ctx, type = "", r = 10, seed = 1, opts = {})
|
|||
ctx.moveTo(-localR * 0.44, localR * 0.42);
|
||||
ctx.lineTo(localR * 0.44, -localR * 0.42);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fillStyle = "#fff7dc";
|
||||
const dots = compact ? 4 : 5;
|
||||
for (let i = 0; i < dots; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(randSeed(localSeed + i, -localR * 0.55, localR * 0.55), randSeed(localSeed + i + 7, -localR * 0.34, localR * 0.34), Math.max(1.0, localR * 0.12), 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
} else if (key === "grass") {
|
||||
ctx.strokeStyle = "#5fa53f";
|
||||
|
|
@ -586,6 +596,8 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
|
|||
nest_box: 0.78,
|
||||
pipe: 0.84,
|
||||
signboard: 0.76,
|
||||
toilet: 0.45,
|
||||
rain_shelter: 0.62,
|
||||
duplicator: 0.82,
|
||||
fence_v: 0.70,
|
||||
fence_h: 0.70,
|
||||
|
|
@ -611,7 +623,7 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
|
|||
if (isConsumableSpriteType(type)) scale *= 1.95;
|
||||
else if (["grass", "water", "zunda_juice", "mercury", "fire"].includes(type)) scale *= 1.85;
|
||||
else if (type === "duplicator") scale *= 1.55;
|
||||
else if (["signboard", "bed", "nest_box", "pipe", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu"].includes(type)) scale *= 1.08;
|
||||
else if (["signboard", "toilet", "rain_shelter", "bed", "nest_box", "pipe", "ant_nest", "pushpin", "oshibyo", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "reciprocator", "firecracker", "flame_firecracker", "sticky_bomb", "fire", "genkotsu"].includes(type)) scale *= 1.08;
|
||||
else if (isParamEffectItemType(type)) scale *= 1.35;
|
||||
if (watermark) scale *= 1.42;
|
||||
ctx.scale(scale, scale);
|
||||
|
|
|
|||
|
|
@ -151,6 +151,14 @@
|
|||
const radius = Math.max(14, (num(item.r, itemRadius(item.type, 12)) || itemRadius(item.type, 12)) * (item.type === "bed" ? 1.55 : 1.25));
|
||||
return { x: global.clamp ? global.clamp(x, pad + radius, (worldRef.w || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.w || 0) - pad - radius, x)), y: global.clamp ? global.clamp(y, pad + radius, (worldRef.h || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.h || 0) - pad - radius, y)) };
|
||||
}
|
||||
function overlayForItem(item) {
|
||||
if (!item || item.dead) return null;
|
||||
if (item.type === "rain_shelter") {
|
||||
const footprint = global.TarinaiParasolSystem?.parasolFootprint?.(item) || null;
|
||||
if (footprint) return { shape: "ellipse", centerX: footprint.centerX, centerY: footprint.centerY, radiusX: footprint.radiusX, radiusY: footprint.radiusY };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function geometryFor(worldRef, item) {
|
||||
if (!worldRef || !item) return null;
|
||||
ensureMechanicalPreview(item, worldRef);
|
||||
|
|
@ -174,6 +182,7 @@
|
|||
r: item.r || radius,
|
||||
rect: geometry.rect || null,
|
||||
rects,
|
||||
overlay: overlayForItem(item),
|
||||
boundsBlocked: bounds,
|
||||
blocked: opts.skipBlocked ? bounds : (bounds || Boolean(worldRef.placementBlocked?.(item) || worldRef.fencePlacementBlocked?.(item))),
|
||||
};
|
||||
|
|
@ -203,7 +212,7 @@
|
|||
const rectsForBounds = geometry.rects && geometry.rects.length ? geometry.rects : (geometry.rect ? [geometry.rect] : null);
|
||||
const ownBounds = extra.boundsRadius ? circleOutside(worldRef, x, y, extra.boundsRadius) : boundsBlocked(worldRef, tmp, { rects: rectsForBounds });
|
||||
const blocked = ownBounds || Boolean(worldRef.placementBlocked?.(tmp));
|
||||
return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], influence: extra.influence || null, blocked, item: tmp };
|
||||
return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], overlay: overlayForItem(tmp), influence: extra.influence || null, blocked, item: tmp };
|
||||
}
|
||||
global.TarinaiPlacementPreviewSystem = Object.freeze({
|
||||
applyToolOrientation,
|
||||
|
|
|
|||
50
js/render.js
50
js/render.js
|
|
@ -178,13 +178,16 @@ function strokeWorldObstacleRect(ctx, rect) {
|
|||
function drawToolTargetHoverHighlight(ctx, world) {
|
||||
if (!world) return;
|
||||
const tool = String(world.tool || "");
|
||||
const target = tool === "pinch" ? world.hoverGrabTarget : (tool === "delete" ? world.hoverDeleteTarget : null);
|
||||
const mode = world.hoverGiveTarget ? "give" : (tool === "pinch" ? "pinch" : (tool === "delete" ? "delete" : ""));
|
||||
const target = mode === "give" ? world.hoverGiveTarget : (mode === "pinch" ? world.hoverGrabTarget : (mode === "delete" ? world.hoverDeleteTarget : null));
|
||||
if (!target || target.dead || target.playerHeld || target._heldByPlayer) return;
|
||||
const p = world.pointer || {};
|
||||
if (!p.inside) return;
|
||||
const style = tool === "delete"
|
||||
? { color: "rgba(224,82,68,0.98)", fill: "rgba(224,82,68,0.13)", label: "\u524a\u9664" }
|
||||
: { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "\u3064\u307e\u3080" };
|
||||
const style = mode === "give"
|
||||
? { color: "rgba(224,98,142,0.98)", fill: "rgba(224,98,142,0.14)", label: "\u4e0e\u3048\u308b" }
|
||||
: (mode === "delete"
|
||||
? { color: "rgba(224,82,68,0.98)", fill: "rgba(224,82,68,0.13)", label: "\u524a\u9664" }
|
||||
: { color: "rgba(86,142,232,0.98)", fill: "rgba(86,142,232,0.12)", label: "\u3064\u307e\u3080" });
|
||||
const pulse = 0.62 + Math.sin((world.time || 0) * 9.5) * 0.12;
|
||||
const worldLine = world.screenSizeToWorld ? world.screenSizeToWorld(4.6) : 4.6;
|
||||
const outerLine = world.screenSizeToWorld ? world.screenSizeToWorld(2.1) : 2.1;
|
||||
|
|
@ -489,7 +492,7 @@ function placementPreviewFor(world) {
|
|||
function drawPlacementPreview(ctx, world) {
|
||||
const preview = placementPreviewFor(world);
|
||||
if (!preview) return;
|
||||
const { type, x, y, r, rect, rects, influence, blocked } = preview;
|
||||
const { type, x, y, r, rect, rects, overlay, influence, blocked } = preview;
|
||||
const pulse = 0.55 + Math.sin((world.time || 0) * 7.0) * 0.10;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = blocked ? 0.44 : 0.72;
|
||||
|
|
@ -514,7 +517,12 @@ function drawPlacementPreview(ctx, world) {
|
|||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
if (rect || (rects && rects.length)) {
|
||||
if (overlay?.shape === "ellipse") {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(overlay.centerX, overlay.centerY, Math.max(1, overlay.radiusX), Math.max(1, overlay.radiusY), 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
} else if (rect || (rects && rects.length)) {
|
||||
if (rects && rects.length) for (const rr of rects) drawRectPreview(rr);
|
||||
else drawRectPreview(rect);
|
||||
if (type === "nest_box" && world.nestBoxSolidRects) {
|
||||
|
|
@ -567,12 +575,14 @@ function drawPlacementPreview(ctx, world) {
|
|||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
const markerX = overlay?.shape === "ellipse" ? overlay.centerX : x;
|
||||
const markerY = overlay?.shape === "ellipse" ? overlay.centerY : y;
|
||||
ctx.globalAlpha = blocked ? 0.52 : 0.70;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x - 8, y);
|
||||
ctx.lineTo(x + 8, y);
|
||||
ctx.moveTo(x, y - 8);
|
||||
ctx.lineTo(x, y + 8);
|
||||
ctx.moveTo(markerX - 8, markerY);
|
||||
ctx.lineTo(markerX + 8, markerY);
|
||||
ctx.moveTo(markerX, markerY - 8);
|
||||
ctx.lineTo(markerX, markerY + 8);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
|
@ -587,7 +597,7 @@ function drawPressureSwitchRangeOverlay(ctx, world) {
|
|||
: (world.items || []).filter(item => item && !item.dead && item.type === "pressure_switch");
|
||||
|
||||
for (const item of switches) {
|
||||
if (!item || item.dead) continue;
|
||||
if (!item || item.dead || item.pressureTarget === "time") continue;
|
||||
const isEditing = editing?.item === item;
|
||||
specs.push({
|
||||
x: Number(item.x || 0) || 0,
|
||||
|
|
@ -638,6 +648,7 @@ function drawOperationToolPreview(ctx, world) {
|
|||
if (item) {
|
||||
const placementInfo = TARINAI_RENDER_GLOBAL.TarinaiPlacementPreviewSystem.forItem(world, item) || null;
|
||||
const previewRects = placementInfo?.rects || world.solidObstacleRects?.(item) || [];
|
||||
const previewOverlay = placementInfo?.overlay || null;
|
||||
const blocked = Boolean(placementInfo ? placementInfo.blocked : (world.placementBlocked?.(item) || world.fencePlacementBlocked?.(item)));
|
||||
let copyPreviewDrew = false;
|
||||
if (typeof item.draw === "function") {
|
||||
|
|
@ -666,7 +677,11 @@ function drawOperationToolPreview(ctx, world) {
|
|||
ctx.strokeStyle = blocked ? "rgba(194,70,58,0.88)" : "rgba(110,178,55,0.88)";
|
||||
ctx.fillStyle = blocked ? "rgba(232,84,72,0.14)" : "rgba(190,236,92,0.15)";
|
||||
const rects = previewRects;
|
||||
if (rects.length) {
|
||||
if (previewOverlay?.shape === "ellipse") {
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(previewOverlay.centerX, previewOverlay.centerY, Math.max(1, previewOverlay.radiusX), Math.max(1, previewOverlay.radiusY), 0, 0, Math.PI * 2);
|
||||
ctx.fill(); ctx.stroke();
|
||||
} else if (rects.length) {
|
||||
for (const rr of rects) {
|
||||
if (rr.oriented) {
|
||||
ctx.save(); ctx.translate(rr.cx, rr.cy); ctx.rotate(rr.angle || 0);
|
||||
|
|
@ -679,7 +694,9 @@ function drawOperationToolPreview(ctx, world) {
|
|||
ctx.beginPath(); ctx.ellipse(p.x, p.y, r * 1.45, r * 0.95, 0, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath(); ctx.moveTo(p.x - 9, p.y); ctx.lineTo(p.x + 9, p.y); ctx.moveTo(p.x, p.y - 9); ctx.lineTo(p.x, p.y + 9); ctx.stroke();
|
||||
const markerX = previewOverlay?.shape === "ellipse" ? previewOverlay.centerX : p.x;
|
||||
const markerY = previewOverlay?.shape === "ellipse" ? previewOverlay.centerY : p.y;
|
||||
ctx.beginPath(); ctx.moveTo(markerX - 9, markerY); ctx.lineTo(markerX + 9, markerY); ctx.moveTo(markerX, markerY - 9); ctx.lineTo(markerX, markerY + 9); ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
return;
|
||||
|
|
@ -864,7 +881,7 @@ function linkIntersectsVisibleRect(it, worldRef, rect, margin = 18) {
|
|||
}
|
||||
|
||||
function isBehindSpriteLayerItem(it) {
|
||||
return Boolean(it && !it.dead && (isLinkRenderItem(it) || it.type === "bed" || it.type === "grass_bed"));
|
||||
return Boolean(it && !it.dead && (isLinkRenderItem(it) || it.type === "bed" || it.type === "grass_bed" || it.type === "toilet"));
|
||||
}
|
||||
|
||||
function renderLayerSortY(entity) {
|
||||
|
|
@ -878,7 +895,7 @@ function renderLayerSortY(entity) {
|
|||
if (entity.kind === "queen" || entity.kind === "worker") return y + Math.max(4, Number(entity.size || 10) || 10);
|
||||
const type = String(entity.type || "");
|
||||
const r = Math.max(0, Number(entity.r || entity.radius || entity.size || 0) || 0);
|
||||
if (type === "bed" || type === "grass_bed") return y + Math.max(8, r * 0.72);
|
||||
if (type === "bed" || type === "grass_bed" || type === "toilet") return y + Math.max(8, r * 0.72);
|
||||
if (type === "nest_box") return y + Math.max(10, r * 0.76);
|
||||
if (type === "pipe") return y + Math.max(8, r * 0.64);
|
||||
if (type === "fan") return y + Math.max(10, r * 0.95);
|
||||
|
|
@ -1828,7 +1845,8 @@ function drawPointerItemTooltip(ctx, world) {
|
|||
const w = Math.min(maxBoxW, Math.max(92, textW + 22));
|
||||
const h = Math.max(38, 16 + lines.length * lineH);
|
||||
const x = clamp(pos.x - w / 2, 8, canvasW - w - 8);
|
||||
const y = clamp(pos.y - (target.r || 24) * 1.8 - h, 8, canvasH - h - 8);
|
||||
const targetGapScale = 1.45;
|
||||
const y = clamp(pos.y - (target.r || 24) * targetGapScale - h, 8, canvasH - h - 8);
|
||||
ctx.fillStyle = "rgba(46,38,30,0.66)";
|
||||
ctx.strokeStyle = "rgba(255,248,220,0.74)";
|
||||
ctx.lineWidth = 1.2;
|
||||
|
|
|
|||
|
|
@ -200,8 +200,8 @@ const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains
|
|||
const type = String(typeOverride || item.robotCleanerTargetType || target.type || "");
|
||||
const label = robotTargetLabel(type);
|
||||
let removed = false;
|
||||
const cleanReason = "掃除";
|
||||
const cleanLogReason = "ロボ掃除機に吸い込まれた";
|
||||
const cleanReason = "\u6383\u9664";
|
||||
const cleanLogReason = "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u5438\u3044\u8fbc\u307e\u308c\u305f";
|
||||
if (type === "tarinai" || isRobotTarinaiTarget(target)) {
|
||||
triggerRobotPanic(item, target, worldRef, true);
|
||||
if (typeof target.die === "function") {
|
||||
|
|
@ -216,7 +216,7 @@ const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains
|
|||
} else {
|
||||
removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, target, {
|
||||
reason: "robot-cleaner",
|
||||
userReason: type === "grass_bed" ? "ロボ掃除機に片付けられた" : cleanLogReason,
|
||||
userReason: type === "grass_bed" ? "\u30ed\u30dc\u6383\u9664\u6a5f\u306b\u7247\u4ed8\u3051\u3089\u308c\u305f" : cleanLogReason,
|
||||
wake: true,
|
||||
breaker: item,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@
|
|||
confirmDialog.setAttribute("aria-describedby", "saveLoadConfirmMessage");
|
||||
confirmDialog.innerHTML = `
|
||||
<div class="field-dialog-panel save-confirm-panel">
|
||||
<h2 id="saveLoadConfirmTitle">ロード前の確認</h2>
|
||||
<h2 id="saveLoadConfirmTitle">\u30ed\u30fc\u30c9\u524d\u306e\u78ba\u8a8d</h2>
|
||||
<p id="saveLoadConfirmMessage" class="field-dialog-lead save-confirm-message"></p>
|
||||
<p class="hint save-warning">現在の状態は上書きされます。必要であれば先にセーブしてください。</p>
|
||||
<p class="hint save-warning">\u73fe\u5728\u306e\u72b6\u614b\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002\u5fc5\u8981\u3067\u3042\u308c\u3070\u5148\u306b\u30bb\u30fc\u30d6\u3057\u3066\u304f\u3060\u3055\u3044\u3002</p>
|
||||
<div class="field-dialog-actions">
|
||||
<button class="btn" data-save-load-cancel type="button">キャンセル</button>
|
||||
<button class="btn primary" data-save-load-confirm type="button">ロードする</button>
|
||||
<button class="btn" data-save-load-cancel type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||
<button class="btn primary" data-save-load-confirm type="button">\u30ed\u30fc\u30c9\u3059\u308b</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(confirmDialog);
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
function requestLoadConfirmation(message) {
|
||||
const confirmDialog = ensureLoadConfirmDialog();
|
||||
const messageEl = confirmDialog.querySelector("#saveLoadConfirmMessage");
|
||||
if (messageEl) messageEl.textContent = String(message || "保存した状態をロードします。");
|
||||
if (messageEl) messageEl.textContent = String(message || "\u4fdd\u5b58\u3057\u305f\u72b6\u614b\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002");
|
||||
confirmDialog.classList.remove("hidden");
|
||||
const confirmBtn = confirmDialog.querySelector("[data-save-load-confirm]");
|
||||
const cancelBtn = confirmDialog.querySelector("[data-save-load-cancel]");
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
};
|
||||
confirmDialog.addEventListener("click", onClick);
|
||||
document.addEventListener("keydown", onKeyDown, true);
|
||||
cancelBtn?.setAttribute?.("aria-label", "ロードをキャンセル");
|
||||
cancelBtn?.setAttribute?.("aria-label", "\u30ed\u30fc\u30c9\u3092\u30ad\u30e3\u30f3\u30bb\u30eb");
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
}
|
||||
if (loadBtn) {
|
||||
const slot = Number(loadBtn.dataset.loadSlot);
|
||||
if (!await requestLoadConfirmation(`スロット${slot}をロードします。`)) return;
|
||||
if (!await requestLoadConfirmation(`\u30b9\u30ed\u30c3\u30c8${slot}\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002`)) return;
|
||||
try {
|
||||
await loadSlot(slot);
|
||||
closeDialog();
|
||||
|
|
@ -255,7 +255,7 @@
|
|||
const area = dialog.querySelector("#saveHashText");
|
||||
const text = area?.value || "";
|
||||
if (!text.trim()) { global.showToast?.("\u30ed\u30fc\u30c9\u3059\u308b\u546a\u6587\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u304f\u3060\u3055\u3044\u3002"); return; }
|
||||
if (!await requestLoadConfirmation("貼り付けた呪文をロードします。")) return;
|
||||
if (!await requestLoadConfirmation("\u8cbc\u308a\u4ed8\u3051\u305f\u546a\u6587\u3092\u30ed\u30fc\u30c9\u3057\u307e\u3059\u3002")) return;
|
||||
try {
|
||||
restoreSnapshot(await decodeSnapshot(text), global.world);
|
||||
closeDialog();
|
||||
|
|
|
|||
|
|
@ -4,12 +4,33 @@
|
|||
(function (global) {
|
||||
const WIRE_TYPES = new Set(["wire", "insulated_wire"]);
|
||||
const LINK_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
|
||||
const SIGNAL_SINK_TYPES = new Set(["rotator", "reciprocator", "robot_cleaner", "gate_fence"]);
|
||||
|
||||
function num(value, fallback = 0) {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
function clamp(value, min, max) { return Math.max(min, Math.min(max, num(value, min))); }
|
||||
function normalizeMinute(value, fallback = 0) {
|
||||
return Math.max(0, Math.min(1435, Math.round(num(value, fallback) / 5) * 5));
|
||||
}
|
||||
function currentMinuteOfDay(worldRef) {
|
||||
const dayLength = Math.max(1, num(worldRef?.config?.dayLength ?? global.CONFIG?.dayLength, 120));
|
||||
const progress = typeof worldRef?.dayProgress === "function"
|
||||
? num(worldRef.dayProgress())
|
||||
: ((num(worldRef?.time) % dayLength) + dayLength) % dayLength / dayLength;
|
||||
return Math.max(0, Math.min(1439, Math.floor((((progress % 1) + 1) % 1) * 1440)));
|
||||
}
|
||||
function isMinuteInRange(minute, start, end) {
|
||||
const now = Math.max(0, Math.min(1439, Number(minute) | 0));
|
||||
const from = normalizeMinute(start, 360);
|
||||
const to = normalizeMinute(end, 1080);
|
||||
if (from === to) return true;
|
||||
return from < to ? now >= from && now < to : now >= from || now < to;
|
||||
}
|
||||
function normalizePressureTarget(value) {
|
||||
return value === "time" ? "time" : (value === "item" ? "item" : "tarinai");
|
||||
}
|
||||
function isWire(item) { return Boolean(item && !item.dead && WIRE_TYPES.has(item.type)); }
|
||||
|
||||
function emitEffect(worldRef, type, x, y, options = {}) {
|
||||
|
|
@ -75,16 +96,27 @@
|
|||
}
|
||||
|
||||
function updatePressureSwitches(worldRef) {
|
||||
const minute = currentMinuteOfDay(worldRef);
|
||||
for (const item of worldRef.itemsOfType?.("pressure_switch") || []) {
|
||||
if (!item || item.dead) continue;
|
||||
item.pressureTarget = item.pressureTarget === "item" ? "item" : "tarinai";
|
||||
item.pressureTarget = normalizePressureTarget(item.pressureTarget);
|
||||
item.pressureThreshold = Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0));
|
||||
item.pressureWidth = clamp(item.pressureWidth || 220, 60, 840);
|
||||
item.pressureHeight = clamp(item.pressureHeight || 160, 60, 840);
|
||||
const count = countPressureTargets(item, worldRef);
|
||||
const active = count >= item.pressureThreshold;
|
||||
item.pressureTimeStart = normalizeMinute(item.pressureTimeStart, 360);
|
||||
item.pressureTimeEnd = normalizeMinute(item.pressureTimeEnd, 1080);
|
||||
let count = 0;
|
||||
let active = false;
|
||||
if (item.pressureTarget === "time") {
|
||||
active = isMinuteInRange(minute, item.pressureTimeStart, item.pressureTimeEnd);
|
||||
count = active ? 1 : 0;
|
||||
} else {
|
||||
count = countPressureTargets(item, worldRef);
|
||||
active = count >= item.pressureThreshold;
|
||||
}
|
||||
if (active !== Boolean(item.signalActive)) worldRef.drawListDirty = true;
|
||||
item.pressureCount = count;
|
||||
item.pressureCurrentMinute = minute;
|
||||
item.signalActive = active;
|
||||
}
|
||||
}
|
||||
|
|
@ -138,10 +170,16 @@
|
|||
}
|
||||
const hasSource = component.some(node => node.type === "pressure_switch");
|
||||
for (const node of component) {
|
||||
const sink = node.type === "rotator" || node.type === "reciprocator" || node.type === "robot_cleaner";
|
||||
if (sink) {
|
||||
node._signalDriven = hasSource;
|
||||
node._signalOn = hasSource && active;
|
||||
if (!SIGNAL_SINK_TYPES.has(node.type)) continue;
|
||||
node._signalDriven = hasSource;
|
||||
node._signalOn = hasSource && active;
|
||||
if (node.type === "gate_fence" && hasSource) {
|
||||
const nextOpen = Boolean(active);
|
||||
if (Boolean(node.gateOpen) !== nextOpen) {
|
||||
node.gateOpen = nextOpen;
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markSpatialDirty?.("signal-gate-toggle");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const entry of wires) {
|
||||
|
|
@ -151,7 +189,7 @@
|
|||
|
||||
for (const item of worldRef.items || []) {
|
||||
if (!item || item.dead) continue;
|
||||
if ((item.type === "rotator" || item.type === "reciprocator" || item.type === "robot_cleaner") && !connected.has(item)) {
|
||||
if (SIGNAL_SINK_TYPES.has(item.type) && !connected.has(item)) {
|
||||
item._signalDriven = false;
|
||||
item._signalOn = false;
|
||||
}
|
||||
|
|
@ -186,11 +224,11 @@
|
|||
target.targetRef = null;
|
||||
target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.55);
|
||||
} else {
|
||||
target.damage?.(2.4, connected ? "通電中の電線に接続された" : "通電中の電線に触れた");
|
||||
target.damage?.(2.4, connected ? "\u901a\u96fb\u4e2d\u306e\u96fb\u7dda\u306b\u63a5\u7d9a\u3055\u308c\u305f" : "\u901a\u96fb\u4e2d\u306e\u96fb\u7dda\u306b\u89e6\u308c\u305f");
|
||||
target.panicTimer = Math.max(num(target.panicTimer), 0.8);
|
||||
target.fearTimer = Math.max(num(target.fearTimer), 0.8);
|
||||
target.electricShockUntil = Math.max(num(target.electricShockUntil), now + 0.62);
|
||||
target.setActionState?.("panic", { target: null, reason: "感電している", wake: true, sleeping: false });
|
||||
target.setActionState?.("panic", { target: null, reason: "\u611f\u96fb\u3057\u3066\u3044\u308b", wake: true, sleeping: false });
|
||||
}
|
||||
emitEffect(worldRef, "electric_shock", target.x, target.y - Math.max(3, num(target.radius ?? target.r, 12) * 0.12), {
|
||||
size: Math.max(12, num(target.radius ?? target.r, 12) * 1.15),
|
||||
|
|
@ -247,5 +285,7 @@
|
|||
updateWorld,
|
||||
powerOverride,
|
||||
isWire,
|
||||
isMinuteInRange,
|
||||
currentMinuteOfDay,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,14 @@
|
|||
// horizontal oval water items spawned by rain; it intentionally does not
|
||||
// share the rain overlay renderer.
|
||||
if (global.TarinaiWeatherSystem.shouldDropRainWater(worldRef, dt)) {
|
||||
const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef);
|
||||
const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef, { rainDrop: true });
|
||||
const sand = worldRef.toiletSandAt?.(drop.x, drop.y) || null;
|
||||
if (sand) {
|
||||
sand.toiletFlash = 1;
|
||||
sand.awakeUntil = Math.max(sand.awakeUntil || 0, (worldRef.time || 0) + 0.8);
|
||||
worldRef.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-rain-absorb");
|
||||
return;
|
||||
}
|
||||
worldRef.addItem?.(drop, "rain-water");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// out-of-bounds cleanup, and draw-list invalidation.
|
||||
(function (global) {
|
||||
function runMaintenance(worldRef, dt, itemCountBefore) {
|
||||
global.TarinaiStructureLifecycle?.maintainDependencies?.(worldRef);
|
||||
worldRef.compactTimer += dt;
|
||||
const compactInterval = 2.5;
|
||||
let itemsCompacted = false;
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@
|
|||
const parentIdx = (t.parents || []).map(id => familyIndex.get(id)).filter(Number.isInteger);
|
||||
const timers = [
|
||||
q(t.reproductionTimer, 10), q(t.fightCooldown, 10), q(t.loveMochiTimer, 10), q(t.fightMochiTimer, 10),
|
||||
q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100)
|
||||
q(t.zunchiDiseaseSeverity, 10), q(t.zunchiStain, 10), q(t.itemEffectTimers?.laxative, 10), q(t.itemEffectTimers?.ammo, 10), q(t.mercuryLifeMultiplier, 100, 100), q(t.itemEffectTimers?.sedative, 10)
|
||||
];
|
||||
const modes = [enumIndex(POWER_MODE_IDS, t.powerItemMode || ""), enumIndex(SIZE_MODE_IDS, t.sizeItemMode || ""), enumIndex(LIFE_MODE_IDS, t.lifeItemMode || "")];
|
||||
const pinIdx = itemIndex.get(t.stuckPushpinId) ?? -1;
|
||||
|
|
@ -301,7 +301,7 @@
|
|||
const timers = Array.isArray(row[13]) ? row[13] : [];
|
||||
t.reproductionTimer = u(timers[0], 10); t.fightCooldown = u(timers[1], 10); t.loveMochiTimer = u(timers[2], 10); t.fightMochiTimer = u(timers[3], 10);
|
||||
t.zunchiDiseaseSeverity = u(timers[4], 10); t.zunchiStain = u(timers[5], 10);
|
||||
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10) };
|
||||
t.itemEffectTimers = { ...(t.itemEffectTimers || {}), laxative: u(timers[6], 10), ammo: u(timers[7], 10), sedative: u(timers[9], 10) };
|
||||
t.mercuryLifeMultiplier = u(timers[8], 100, t.mercuryLifeMultiplier || 1);
|
||||
const modes = Array.isArray(row[14]) ? row[14] : [];
|
||||
t.powerItemMode = enumValue(POWER_MODE_IDS, modes[0], "");
|
||||
|
|
@ -346,7 +346,16 @@
|
|||
q(item.fanSwingPhase || 0, 1000),
|
||||
q(item.fanBodyAngle ?? item.angle ?? 0, 1000),
|
||||
];
|
||||
if (type === "pressure_switch") return [item.pressureTarget === "item" ? 1 : 0, Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0)), Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0)), Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0))];
|
||||
if (type === "pressure_switch") {
|
||||
const targetCode = item.pressureTarget === "time" ? 2 : (item.pressureTarget === "item" ? 1 : 0);
|
||||
const width = Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0));
|
||||
const height = Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0));
|
||||
const start = Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0));
|
||||
const end = Math.max(0, Math.min(1435, Number(item.pressureTimeEnd ?? 1080) | 0));
|
||||
const third = targetCode === 2 ? width * 1440 + start : width;
|
||||
const fourth = targetCode === 2 ? height * 1440 + end : height;
|
||||
return [targetCode, Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0)), third, fourth];
|
||||
}
|
||||
if (global.isRotatableItemType(type)) return [q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0];
|
||||
if (global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
|
||||
return [];
|
||||
|
|
@ -435,10 +444,16 @@
|
|||
item.fanSwingPhase = u(extra[5], 1000, item.fanSwingPhase || 0);
|
||||
item.fanBodyAngle = global.normalizedItemAngle(u(extra[6], 1000, item.angle), item.angle);
|
||||
} else if (type === "pressure_switch") {
|
||||
item.pressureTarget = Number(extra[0]) === 1 ? "item" : "tarinai";
|
||||
const targetCode = Number(extra[0]) | 0;
|
||||
const rawThird = Number(extra[2]);
|
||||
const rawFourth = Number(extra[3]);
|
||||
const packedTime = targetCode === 2 && rawThird >= 1440 && rawFourth >= 1440;
|
||||
item.pressureTarget = targetCode === 2 ? "time" : (targetCode === 1 ? "item" : "tarinai");
|
||||
item.pressureThreshold = Math.max(1, Math.min(999, Number(extra[1] || 1) | 0));
|
||||
item.pressureWidth = Math.max(60, Math.min(840, Number(extra[2] || 220) | 0));
|
||||
item.pressureHeight = Math.max(60, Math.min(840, Number(extra[3] || 160) | 0));
|
||||
item.pressureWidth = targetCode === 2 ? (packedTime ? Math.max(60, Math.min(840, Math.floor(rawThird / 1440))) : 220) : Math.max(60, Math.min(840, Number(extra[2] || 220) | 0));
|
||||
item.pressureHeight = targetCode === 2 ? (packedTime ? Math.max(60, Math.min(840, Math.floor(rawFourth / 1440))) : 160) : Math.max(60, Math.min(840, Number(extra[3] || 160) | 0));
|
||||
item.pressureTimeStart = targetCode === 2 ? Math.max(0, Math.min(1435, (packedTime ? rawThird % 1440 : rawThird) | 0)) : 360;
|
||||
item.pressureTimeEnd = targetCode === 2 ? Math.max(0, Math.min(1435, (packedTime ? rawFourth % 1440 : rawFourth) | 0)) : 1080;
|
||||
item.signalActive = false;
|
||||
} else if (global.isRotatableItemType(type)) {
|
||||
const fallback = global.defaultItemAngle(type);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const CONNECTION_ITEM_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
|
||||
|
||||
function isLiveLooseItem(item) {
|
||||
return Boolean(item && !item.dead && !(Number.isFinite(Number(item.amount)) && Number(item.amount) <= 0));
|
||||
}
|
||||
|
||||
function markWorldAfterStructureChange(worldRef, reason = "structure-change", item = null, radius = 40) {
|
||||
if (!worldRef) return;
|
||||
worldRef.drawListDirty = true;
|
||||
|
|
@ -132,36 +138,143 @@
|
|||
}
|
||||
|
||||
|
||||
function removeDependentLinks(worldRef, target, reason = "support-removed") {
|
||||
if (!worldRef || !target || !target.id) return 0;
|
||||
function endpointTarget(worldRef, endpoint) {
|
||||
if (!worldRef || !endpoint) return null;
|
||||
const cached = endpoint._ref;
|
||||
if (endpoint.kind === "tarinai") {
|
||||
const token = endpoint.liveToken ?? null;
|
||||
const found = worldRef.liveTarinaiById?.(endpoint.id, token)
|
||||
|| worldRef.tarinaiById?.get?.(endpoint.id)
|
||||
|| (worldRef.tarinai || []).find(t => t && !t.dead && t.id === endpoint.id)
|
||||
|| null;
|
||||
if (!found || found.dead) return null;
|
||||
if (token != null && Number(found.liveToken ?? token) !== Number(token)) return null;
|
||||
return found;
|
||||
}
|
||||
if (endpoint.kind === "ant") {
|
||||
const found = (worldRef.ants || []).find(ant => ant && !ant.dead && ant.id === endpoint.id) || null;
|
||||
return found && !found.dead ? found : null;
|
||||
}
|
||||
let found = worldRef.itemById?.(endpoint.id) || null;
|
||||
if (found) return isLiveLooseItem(found) ? found : null;
|
||||
found = (worldRef.items || []).find(item => item && item.id === endpoint.id) || null;
|
||||
if (found) return isLiveLooseItem(found) ? found : null;
|
||||
if (cached && (!endpoint.id || cached.id === endpoint.id) && isLiveLooseItem(cached)) return cached;
|
||||
return null;
|
||||
}
|
||||
|
||||
function connectionItems(worldRef) {
|
||||
if (!worldRef) return [];
|
||||
if (typeof worldRef.itemsOfType !== "function") return (worldRef.items || []).filter(item => isLiveLooseItem(item) && CONNECTION_ITEM_TYPES.has(item.type));
|
||||
const out = [];
|
||||
for (const type of CONNECTION_ITEM_TYPES) {
|
||||
for (const item of worldRef.itemsOfType(type) || []) if (isLiveLooseItem(item)) out.push(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function removeOrphanedLinks(worldRef, reason = "link-endpoint-gone") {
|
||||
if (!worldRef?.items) return 0;
|
||||
const pb = global.TarinaiPhysicsBodySystem || null;
|
||||
if (!pb?.endpoint) return 0;
|
||||
let removed = 0;
|
||||
const removedIds = new Set([target.id]);
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const link of worldRef.items || []) {
|
||||
if (!link || link.dead || !["rope", "rod", "spring", "wire", "insulated_wire"].includes(link.type)) continue;
|
||||
const a = pb.endpoint(link, 0);
|
||||
const b = pb.endpoint(link, 1);
|
||||
const hit = (a && a.kind === "item" && removedIds.has(a.id)) || (b && b.kind === "item" && removedIds.has(b.id));
|
||||
if (!hit) continue;
|
||||
link.amount = 0;
|
||||
link.hp = 0;
|
||||
removedIds.add(link.id);
|
||||
removed += 1;
|
||||
changed = true;
|
||||
const links = connectionItems(worldRef);
|
||||
const linksById = new Map(links.map(link => [link.id, link]));
|
||||
const dependents = new Map();
|
||||
const queue = [];
|
||||
const queued = new Set();
|
||||
const enqueue = link => {
|
||||
if (!link || queued.has(link.id)) return;
|
||||
queued.add(link.id);
|
||||
queue.push(link);
|
||||
};
|
||||
for (const link of links) {
|
||||
let orphaned = false;
|
||||
for (const endpoint of [pb.endpoint(link, 0), pb.endpoint(link, 1)]) {
|
||||
const target = endpointTarget(worldRef, endpoint);
|
||||
if (!target) {
|
||||
orphaned = true;
|
||||
continue;
|
||||
}
|
||||
if (endpoint?.kind === "item" && linksById.has(target.id)) {
|
||||
if (!dependents.has(target.id)) dependents.set(target.id, []);
|
||||
dependents.get(target.id).push(link);
|
||||
}
|
||||
}
|
||||
if (orphaned) enqueue(link);
|
||||
}
|
||||
if (removed) {
|
||||
let removed = 0;
|
||||
let queueIndex = 0;
|
||||
while (queueIndex < queue.length) {
|
||||
const link = queue[queueIndex++];
|
||||
if (!isLiveLooseItem(link)) continue;
|
||||
if (!markLooseItemGone(link, reason)) continue;
|
||||
removed += 1;
|
||||
for (const dependent of dependents.get(link.id) || []) enqueue(dependent);
|
||||
}
|
||||
let pendingCleared = false;
|
||||
const pending = worldRef.pendingLinkEndpoint || null;
|
||||
if (pending && !endpointTarget(worldRef, pending)) {
|
||||
worldRef.pendingLinkEndpoint = null;
|
||||
pendingCleared = true;
|
||||
}
|
||||
if (removed || pendingCleared) {
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markItemBucketsDirty?.("dependent-links-removed");
|
||||
worldRef.markSpatialDirty?.("dependent-links-removed");
|
||||
worldRef.markItemBucketsDirty?.(reason);
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
function reconcilePlushies(worldRef, reason = "plushie-attachment-reconcile") {
|
||||
if (!worldRef?.items) return { restored: 0, removed: 0 };
|
||||
let restored = 0;
|
||||
let removed = 0;
|
||||
const source = worldRef.itemsOfType?.("plushie") || worldRef.items;
|
||||
for (const plushie of source || []) {
|
||||
if (!plushie || plushie.dead || !plushie.isStructure || plushie.type !== "plushie") continue;
|
||||
if (plushie.playerHeld || plushie._heldByPlayer || Number(plushie.plushieFlingTimer || 0) > 0) continue;
|
||||
const owner = worldRef.liveTarinaiById?.(plushie.ownerId)
|
||||
|| (worldRef.tarinai || []).find(t => t && !t.dead && t.id === plushie.ownerId)
|
||||
|| null;
|
||||
if (!plushie.ownerId || !owner || owner.dead) {
|
||||
if (markStructureGone(plushie, "plushie-owner-gone")) removed += 1;
|
||||
plushie.carriedById = "";
|
||||
plushie.onHead = false;
|
||||
continue;
|
||||
}
|
||||
if (plushie.carriedById === owner.id && plushie.onHead) continue;
|
||||
const nextX = Number(owner.x || 0) || 0;
|
||||
const nextY = (Number(owner.y || 0) || 0) - Math.max(22, (Number(owner.radius || 24) || 24) * 1.02);
|
||||
plushie.ownerId = owner.id;
|
||||
plushie.carriedById = owner.id;
|
||||
plushie.onHead = true;
|
||||
plushie.x = nextX;
|
||||
plushie.y = nextY;
|
||||
plushie.vx = 0;
|
||||
plushie.vy = 0;
|
||||
plushie.spinVelocity = 0;
|
||||
restored += 1;
|
||||
}
|
||||
if (restored || removed) {
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markItemBucketsDirty?.(reason);
|
||||
worldRef.markSpatialDirty?.(reason);
|
||||
}
|
||||
return { restored, removed };
|
||||
}
|
||||
|
||||
function maintainDependencies(worldRef) {
|
||||
const linksRemoved = removeOrphanedLinks(worldRef, "link-endpoint-gone");
|
||||
const plushies = reconcilePlushies(worldRef, "plushie-attachment-reconcile");
|
||||
return { linksRemoved, plushiesRestored: plushies.restored, plushiesRemoved: plushies.removed };
|
||||
}
|
||||
|
||||
|
||||
function removeDependentLinks(worldRef, target, reason = "support-removed") {
|
||||
if (!worldRef || !target || !target.id) return 0;
|
||||
return removeOrphanedLinks(worldRef, reason);
|
||||
}
|
||||
|
||||
function deleteStructure(worldRef, structure, opts = {}) {
|
||||
if (!structure || structure.dead) return false;
|
||||
const reason = opts.reason || "structure-deleted";
|
||||
|
|
@ -200,6 +313,9 @@
|
|||
const api = Object.freeze({
|
||||
consumeGrassBedOnWake,
|
||||
deleteItem,
|
||||
maintainDependencies,
|
||||
reconcilePlushies,
|
||||
removeOrphanedLinks,
|
||||
triggerOwnedStructureDestroyedPanic,
|
||||
});
|
||||
global.TarinaiStructureLifecycle = api;
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@
|
|||
this.dead = true;
|
||||
this.hp = 0;
|
||||
this.amount = 0;
|
||||
global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, { type: "fling_out", name: "画面外" });
|
||||
global.StructureRegistry?.get?.(this.type)?.onDestroyed?.(this, worldRef, { type: "fling_out", name: "\u753b\u9762\u5916" });
|
||||
global.TarinaiStructureLifecycle?.triggerOwnedStructureDestroyedPanic?.(worldRef, this, null, "plushie-fling-out");
|
||||
worldRef?.markItemBucketsDirty?.("plushie-fling-out");
|
||||
worldRef?.markSpatialDirty?.("plushie-fling-out");
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ class Tarinai { constructor(world, opts = {}) {
|
|||
this.itemEffectTimers = {
|
||||
laxative: Math.max(0, Number(effectTimers.laxative || opts.laxativeTimer || 0) || 0),
|
||||
ammo: Math.max(0, Number(effectTimers.ammo || opts.ammoTimer || 0) || 0),
|
||||
sedative: Math.max(0, Number(effectTimers.sedative || opts.sedativeTimer || 0) || 0),
|
||||
};
|
||||
this.laxativePoopTimer = Number.isFinite(opts.laxativePoopTimer) ? opts.laxativePoopTimer : 3;
|
||||
this.ammoBumpCooldown = opts.ammoBumpCooldown || 0;
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ function createTemperatureComfortActionSpec() {
|
|||
: ((target.type === "nest_box" || target.type === "pipe") ? target : null);
|
||||
if (shelter && (shelter.type === "nest_box" || shelter.type === "pipe")) {
|
||||
if (typeof t.enterNestBox === "function" && t.enterNestBox(shelter, dt, { purpose: "temperature" })) {
|
||||
t.thought = shelter.type === "pipe" ? "土管の中で気温をしのいでいる" : "巣箱の中で気温をしのいでいる";
|
||||
t.thought = shelter.type === "pipe" ? "\u571f\u7ba1\u306e\u4e2d\u3067\u6c17\u6e29\u3092\u3057\u306e\u3044\u3067\u3044\u308b" : "\u5de3\u7bb1\u306e\u4e2d\u3067\u6c17\u6e29\u3092\u3057\u306e\u3044\u3067\u3044\u308b";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function causePhraseForReason(chosenNeed, action, tarinai = null, options = {})
|
|||
|
||||
function buildReasonText(chosenNeed, tiedNeeds, action, tarinai = null, world = null, options = {}) {
|
||||
if (action?.id === "sunbath" && (options?.leisure || tarinai?.sunbathLeisure || currentTarinaiBehavior(tarinai)?.data?.leisure)) {
|
||||
return "気分がいいので、日光浴している。";
|
||||
return "\u6c17\u5206\u304c\u3044\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002";
|
||||
}
|
||||
if (action?.id === "seek_comfort_temperature") {
|
||||
const felt = tarinai?.world?.feltTemperatureFor?.(tarinai) ?? world?.temperatureAt?.(tarinai?.x, tarinai?.y, tarinai) ?? tarinai?.feltTemperature ?? tarinai?.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ function finishBehaviorConsumption(t, item, foodType, eating, dt, label, color =
|
|||
t.eatTimer = Math.max(t.eatTimer || 0, 1.0);
|
||||
t.eatCooldown = rand(0.96, 1.12);
|
||||
t.foodReactTimer = Math.max(t.foodReactTimer || 0, 0.16);
|
||||
if (typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { food: Math.max(18, Math.min(54, (Number(eating) || 0) * 1.1)) }, "meal");
|
||||
if (foodType !== "first_aid" && typeof applyNeedSatisfaction === "function") applyNeedSatisfaction(t, { food: Math.max(18, Math.min(54, (Number(eating) || 0) * 1.1)) }, "meal");
|
||||
t.lastMealColor = color;
|
||||
if (Math.random() < Math.max(0.1, dt || 0.1) * 2.2) {
|
||||
const mouth = t.mouthPosition?.(item) || { x: t.x, y: t.y };
|
||||
|
|
@ -77,13 +77,16 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
|
|||
const eating = t.consumeFoodServing?.(target);
|
||||
if (!(eating > 0)) return false;
|
||||
const isParam = isParamEffectItemType(foodType);
|
||||
const hpOnlyMedicine = foodType === "first_aid";
|
||||
const isSpecial = foodType === "love_mochi" || foodType === "fight_mochi" || foodType === "sleep_drug" || isParam;
|
||||
const color = ({ sweet: "#78b957", love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff", protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b", mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f" })[foodType] || "#f1dfb7";
|
||||
const color = ({ sweet: "#78b957", love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff", first_aid: "#d24852", sedative: "#5c8eae", protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b", mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f" })[foodType] || "#f1dfb7";
|
||||
finishBehaviorConsumption(t, target, foodType, eating, effectiveDt, `${labelFor(foodType)}\u3092\u98df\u3079\u3066\u3044\u308b`, color);
|
||||
const hungerRelief = t.foodHungerReliefFor?.(foodType || target.type, eating, foodType === "sweet" ? 0.75 : (isSpecial ? 0.82 : 0.95)) ?? eating * 0.8;
|
||||
t.applyFoodHungerRelief ? t.applyFoodHungerRelief(hungerRelief) : (t.hunger -= hungerRelief);
|
||||
t.recoverHealth?.(eating * (foodType === "sweet" ? 0.28 : (isSpecial ? 0.24 : 0.46)));
|
||||
t.affection += foodType === "sweet" ? eating * 0.03 : eating * 0.012;
|
||||
if (!hpOnlyMedicine) {
|
||||
const hungerRelief = t.foodHungerReliefFor?.(foodType || target.type, eating, foodType === "sweet" ? 0.75 : (isSpecial ? 0.82 : 0.95)) ?? eating * 0.8;
|
||||
t.applyFoodHungerRelief ? t.applyFoodHungerRelief(hungerRelief) : (t.hunger -= hungerRelief);
|
||||
t.recoverHealth?.(eating * (foodType === "sweet" ? 0.28 : (isSpecial ? 0.24 : 0.46)));
|
||||
t.affection += foodType === "sweet" ? eating * 0.03 : eating * 0.012;
|
||||
}
|
||||
if (isSpecial && t.applyParamItemEffect) t.applyParamItemEffect(foodType || target.type, target, { source: "eating", nutrition: eating, silentLog: foodType === "love_mochi" || foodType === "fight_mochi" || foodType === "sleep_drug" });
|
||||
if (foodType === "sweet") {
|
||||
applyNeedRelief(t, { food: -eating * 0.4, fulfill: -eating * 0.2 });
|
||||
|
|
@ -91,7 +94,7 @@ function consumeBehaviorTarget(t, world, target, role = "food", dt = 0.12) {
|
|||
if (t.fightDisease) t.recoverFightDisease?.("\u305a\u3093\u3060\u9905\u3067\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f");
|
||||
}
|
||||
const fromDuplicator = isDuplicatorFoodSource(target);
|
||||
t.makePoop?.(normalPoopUnitsFor(foodType || target.type, eating));
|
||||
if (!hpOnlyMedicine) t.makePoop?.(normalPoopUnitsFor(foodType || target.type, eating));
|
||||
if (fromDuplicator) {
|
||||
t.mealCooldownUntil = Math.max(t.mealCooldownUntil || 0, (world.time || 0) + 1.2);
|
||||
t.behaviorLockTimer = Math.max(0, Math.min(t.behaviorLockTimer || 0, 0.18));
|
||||
|
|
|
|||
162
js/tarinai_direct_feeding_system.js
Normal file
162
js/tarinai_direct_feeding_system.js
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"use strict";
|
||||
|
||||
// Direct player-to-Tarinai feeding. Placement clicks and pinched-item drops
|
||||
// share this path so item effects, feedback, and consumption stay consistent.
|
||||
(function (global) {
|
||||
const FOOD_TOOL_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "water", "zunda_juice"]);
|
||||
const MEDICINE_TOOL_TYPES = new Set(["first_aid", "sedative", "sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug"]);
|
||||
const HEART_FOOD_TYPES = new Set(["food", "sweet", "love_mochi", "fight_mochi", "grass", "zunda_juice"]);
|
||||
|
||||
function itemType(itemOrType = "") {
|
||||
return String(typeof itemOrType === "object" ? itemOrType?.type || "" : itemOrType || "");
|
||||
}
|
||||
|
||||
function isDirectFeedType(itemOrType = "") {
|
||||
const type = itemType(itemOrType);
|
||||
return FOOD_TOOL_TYPES.has(type) || MEDICINE_TOOL_TYPES.has(type);
|
||||
}
|
||||
|
||||
function isFoodGift(itemOrType = "") {
|
||||
return HEART_FOOD_TYPES.has(itemType(itemOrType));
|
||||
}
|
||||
|
||||
function hasConsumableAmount(item = null) {
|
||||
if (!item || item.dead || !isDirectFeedType(item)) return false;
|
||||
const type = itemType(item);
|
||||
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) {
|
||||
if (Number.isFinite(Number(item.foodServingsRemaining))) return Number(item.foodServingsRemaining) > 0;
|
||||
return Number(item.amount ?? 1) > 0;
|
||||
}
|
||||
return Number(item.amount ?? 1) > 0;
|
||||
}
|
||||
|
||||
function findTargetAt(worldRef, x, y) {
|
||||
if (!worldRef || !Number.isFinite(Number(x)) || !Number.isFinite(Number(y))) return null;
|
||||
const px = Number(x);
|
||||
const py = Number(y);
|
||||
const list = worldRef.tarinai || [];
|
||||
for (let i = list.length - 1; i >= 0; i -= 1) {
|
||||
const t = list[i];
|
||||
if (!t || t.dead || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const hit = typeof t.contains === "function"
|
||||
? t.contains(px, py)
|
||||
: (typeof global.distXY === "function" && global.distXY(px, py, t.x, t.y) <= Math.max(20, Number(t.radius || 22) * 1.35));
|
||||
if (hit) return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function roleFor(item = null) {
|
||||
const type = itemType(item);
|
||||
if (type === "water") return "drink";
|
||||
if (MEDICINE_TOOL_TYPES.has(type) || type === "zunda_juice") return "medicine";
|
||||
return "food";
|
||||
}
|
||||
|
||||
function spawnFoodGiftHearts(tarinai, nutrition = 0) {
|
||||
const worldRef = tarinai?.world;
|
||||
if (!worldRef?.effects || typeof global.TarinaiEffect !== "function") return false;
|
||||
const count = 10 + Math.min(4, Math.max(0, Math.round((Number(nutrition) || 0) / 7)));
|
||||
const radius = Math.max(18, Number(tarinai.radius || 22) || 22);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const side = i % 2 ? 1 : -1;
|
||||
worldRef.effects.push(new global.TarinaiEffect("heart",
|
||||
tarinai.x + side * global.rand(radius * 0.06, radius * 0.82),
|
||||
tarinai.y - global.rand(radius * 0.38, radius * 1.72), {
|
||||
vx: side * global.rand(9, 30) + global.rand(-9, 9),
|
||||
vy: global.rand(-76, -30),
|
||||
size: global.rand(9, 17),
|
||||
life: global.rand(0.82, 1.36),
|
||||
color: "rgba(240,91,135,0.94)",
|
||||
}));
|
||||
}
|
||||
worldRef.effects.push(new global.TarinaiEffect("ring", tarinai.x, tarinai.y, {
|
||||
size: Math.max(24, radius * 1.35), life: 0.28, color: "rgba(240,91,135,0.42)",
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyFoodGiftPersonality(tarinai, nutrition = 0) {
|
||||
if (!tarinai || typeof tarinai.adjustPersonality !== "function") return false;
|
||||
const amount = Math.max(0, Number(nutrition) || 0);
|
||||
// A single gift intentionally outweighs ordinary pointer contact, which
|
||||
// changes openness gradually per second while the cursor remains nearby.
|
||||
const openness = 0.018 + Math.min(0.010, amount * 0.0004);
|
||||
const sociability = 0.010 + Math.min(0.006, amount * 0.00024);
|
||||
const changedOpen = tarinai.adjustPersonality("openness", openness, "\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f") || 0;
|
||||
const changedSocial = tarinai.adjustPersonality("sociability", sociability, "\u30ab\u30fc\u30bd\u30eb\u304b\u3089\u76f4\u63a5\u98df\u3079\u7269\u3092\u98df\u3079\u305f") || 0;
|
||||
return Math.abs(changedOpen) + Math.abs(changedSocial) > 0;
|
||||
}
|
||||
|
||||
function removeGivenWorldItem(worldRef, item, label) {
|
||||
if (!worldRef || !item || !(worldRef.items || []).includes(item)) return false;
|
||||
if (global.TarinaiStructureLifecycle?.deleteItem) {
|
||||
return Boolean(global.TarinaiStructureLifecycle.deleteItem(worldRef, item, {
|
||||
reason: "direct-feed-consumed",
|
||||
userReason: `${label}\u304c\u76f4\u63a5\u4e0e\u3048\u3089\u308c\u305f`,
|
||||
wake: false,
|
||||
panicOwner: false,
|
||||
}));
|
||||
}
|
||||
item.amount = 0;
|
||||
item.dead = true;
|
||||
worldRef.markItemBucketsDirty?.("direct-feed-consumed");
|
||||
worldRef.markSpatialDirty?.("direct-feed-consumed");
|
||||
return true;
|
||||
}
|
||||
|
||||
function give(worldRef, tarinai, item, options = {}) {
|
||||
if (!worldRef || !tarinai || tarinai.dead || !hasConsumableAmount(item)) return null;
|
||||
if (typeof global.consumeBehaviorTarget !== "function") return null;
|
||||
const type = itemType(item);
|
||||
const label = typeof global.toolLabel === "function" ? global.toolLabel(type) : type;
|
||||
const consumed = global.consumeBehaviorTarget(tarinai, worldRef, item, roleFor(item), 0.18);
|
||||
if (!consumed) return null;
|
||||
const nutrition = global.TarinaiItemRegistry?.food?.nutrition?.(type, 0) ?? 0;
|
||||
|
||||
tarinai.target = null;
|
||||
tarinai.targetKey = "";
|
||||
tarinai.sleeping = false;
|
||||
tarinai.goodMode = "smile";
|
||||
tarinai.moodLiftTimer = Math.max(Number(tarinai.moodLiftTimer || 0) || 0, 1.2);
|
||||
tarinai.foodReactTimer = Math.max(Number(tarinai.foodReactTimer || 0) || 0, 0.55);
|
||||
tarinai.surpriseTimer = Math.max(Number(tarinai.surpriseTimer || 0) || 0, 0.28);
|
||||
tarinai.thought = `${label}\u3092\u3082\u3089\u3063\u305f`;
|
||||
|
||||
const foodGift = isFoodGift(type);
|
||||
if (foodGift) {
|
||||
tarinai.affection = Number(tarinai.affection || 0) + 1.8 + Math.max(0, Number(nutrition) || 0) * 0.035;
|
||||
applyFoodGiftPersonality(tarinai, nutrition);
|
||||
spawnFoodGiftHearts(tarinai, nutrition);
|
||||
tarinai.bubble?.("\u2661", 2.2, "rgba(190,82,132,0.80)");
|
||||
} else {
|
||||
tarinai.focusPulseTimer = Math.max(Number(tarinai.focusPulseTimer || 0) || 0, 0.72);
|
||||
}
|
||||
|
||||
const sourceLabel = options.source === "pinch" ? "\u624b\u6e21\u3057\u3067" : "\u76f4\u63a5";
|
||||
worldRef.log?.(`${tarinai.name}\u306b${label}\u3092${sourceLabel}\u4e0e\u3048\u305f\u3002`, "food", {
|
||||
participants: [tarinai], eventType: "direct_feed", sound: false,
|
||||
});
|
||||
|
||||
const removed = options.removeItem !== false ? removeGivenWorldItem(worldRef, item, label) : false;
|
||||
if (!removed && (worldRef.items || []).includes(item)) {
|
||||
item.vx = 0;
|
||||
item.vy = 0;
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
}
|
||||
worldRef.hoverGiveTarget = null;
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.recordFamily?.(tarinai);
|
||||
if (global.uiCache) global.uiCache.selectedSnapshot = "";
|
||||
return { tarinai, item, type, label, foodGift, removed };
|
||||
}
|
||||
|
||||
global.TarinaiDirectFeedingSystem = Object.freeze({
|
||||
isDirectFeedType,
|
||||
isFoodGift,
|
||||
hasConsumableAmount,
|
||||
findTargetAt,
|
||||
give,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -83,7 +83,8 @@
|
|||
updateZunchiDisease(dt) {
|
||||
this.zunchiStain = clamp(this.zunchiStain || 0, 0, 100);
|
||||
this.zunchiDiseaseCooldown = Math.max(0, (this.zunchiDiseaseCooldown || 0) - dt);
|
||||
if (this.world?.weather === "light_rain") {
|
||||
const shelteredFromRain = globalThis.TarinaiRainShelterSystem?.isSheltered?.(this.world, this.x, this.y) || false;
|
||||
if (this.world?.weather === "light_rain" && !shelteredFromRain) {
|
||||
this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100);
|
||||
if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006);
|
||||
}
|
||||
|
|
@ -283,7 +284,7 @@
|
|||
if (!isNestContainerItem(box) || this.dead) return false;
|
||||
const alreadyInside = this.insideNestBoxId === box.id;
|
||||
if (!alreadyInside && box?.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(box)) {
|
||||
return this.abortNestBoxEntry?.(box, "巣箱が燃えていて入れない") || false;
|
||||
return this.abortNestBoxEntry?.(box, "\u5de3\u7bb1\u304c\u71c3\u3048\u3066\u3044\u3066\u5165\u308c\u306a\u3044") || false;
|
||||
}
|
||||
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
||||
let occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||
|
|
@ -629,19 +630,19 @@
|
|||
const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, currentNest.x, currentNest.y, Math.max(this.radius || 18, (currentNest.r || 42) * 1.15));
|
||||
if (water) {
|
||||
globalThis.TarinaiItemDynamicToolSystem?.consumeWaterForFire?.(this.world, water, Math.max(10, (this.radius || 18) * 0.55));
|
||||
this.extinguishFire("水で消火された");
|
||||
this.extinguishFire("\u6c34\u3067\u6d88\u706b\u3055\u308c\u305f");
|
||||
return;
|
||||
}
|
||||
const mul = globalThis.TarinaiItemDynamicToolSystem?.fireTempMultiplier?.(this.world, currentNest.x, currentNest.y, this) || 1;
|
||||
if (deterministicChance(this.world, "tarinai-nest-fire-extinguish", dt * 0.014 * mul, this, currentNest)) {
|
||||
this.extinguishFire("自然に火が消えた");
|
||||
this.extinguishFire("\u81ea\u7136\u306b\u706b\u304c\u6d88\u3048\u305f");
|
||||
return;
|
||||
}
|
||||
if (this.burnTimer <= 0.01) this.extinguishFire("自然に火が消えた");
|
||||
if (this.burnTimer <= 0.01) this.extinguishFire("\u81ea\u7136\u306b\u706b\u304c\u6d88\u3048\u305f");
|
||||
return;
|
||||
}
|
||||
if (typeof applyNeedShock === "function") applyNeedShock(this, { health: dt * 6.2, safety: dt * 3.0 });
|
||||
if (typeof this.damage === "function") this.damage(dt * 3.8, "炎上");
|
||||
if (typeof this.damage === "function") this.damage(dt * 3.8, "\u708e\u4e0a");
|
||||
else this.energy = clamp((this.energy || 0) - dt * 0.12, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100));
|
||||
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.22);
|
||||
const water = globalThis.TarinaiItemDynamicToolSystem?.contactWaterDrop?.(this.world, this.x, this.y, Math.max(this.radius || 18, 16));
|
||||
|
|
|
|||
|
|
@ -58,12 +58,16 @@ if (canSweetBite && (foodType === "love_mochi" || foodType === "fight_mochi" ||
|
|||
tarinai.beginEating(it, `${eatLabel}\u3092\u98df\u3079\u3066\u3044\u308b`);
|
||||
tarinai.vx *= Math.pow(0.25, dt * 60);
|
||||
tarinai.vy *= Math.pow(0.25, dt * 60);
|
||||
const hungerRelief = tarinai.foodHungerReliefFor(foodType || it.type, eating, 0.82);
|
||||
tarinai.applyFoodHungerRelief ? tarinai.applyFoodHungerRelief(hungerRelief) : (tarinai.hunger -= hungerRelief);
|
||||
tarinai.recoverHealth(eating * 0.24);
|
||||
tarinai.affection += eating * 0.04;
|
||||
const hpOnlyMedicine = foodType === "first_aid";
|
||||
if (!hpOnlyMedicine) {
|
||||
const hungerRelief = tarinai.foodHungerReliefFor(foodType || it.type, eating, 0.82);
|
||||
tarinai.applyFoodHungerRelief ? tarinai.applyFoodHungerRelief(hungerRelief) : (tarinai.hunger -= hungerRelief);
|
||||
tarinai.recoverHealth(eating * 0.24);
|
||||
tarinai.affection += eating * 0.04;
|
||||
}
|
||||
const mealColors = {
|
||||
love_mochi: "#ff7bab", fight_mochi: "#e07c43", sleep_drug: "#b89cff",
|
||||
first_aid: "#d24852", sedative: "#5c8eae",
|
||||
protein: "#f28d3d", niteropu: "#6255a8", ammo: "#d0942f", laxative: "#788c3b",
|
||||
mercury: "#6e95a8", giant_drug: "#e47b3a", dwarf_drug: "#7773c8", zunda_juice: "#61b94f",
|
||||
};
|
||||
|
|
@ -87,7 +91,7 @@ if (canSweetBite && (foodType === "love_mochi" || foodType === "fight_mochi" ||
|
|||
tarinai.world.spawnEatEffect(mouth.x, mouth.y, foodType === "love_mochi" ? "#ff7bab" : (foodType === "fight_mochi" ? "#e07c43" : (foodType === "sleep_drug" ? "#b89cff" : (tarinai.lastMealColor || "#41aa66"))));
|
||||
}
|
||||
const fromDuplicator = isDuplicatorFoodSource(it);
|
||||
tarinai.makePoop(normalPoopUnitsFor(foodType || it.type, eating));
|
||||
if (!hpOnlyMedicine) tarinai.makePoop(normalPoopUnitsFor(foodType || it.type, eating));
|
||||
if (fromDuplicator) tarinai.mealCooldownUntil = Math.max(tarinai.mealCooldownUntil || 0, tarinai.world.time + 1.2);
|
||||
if (tarinai.world.time > tarinai.nextEatSound) { audio.eat(); tarinai.nextEatSound = tarinai.world.time + 0.42; }
|
||||
if (tarinai.world.time - tarinai.lastLog > 9 && Math.random() < 0.02) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
const max = foodServingsForSize(it.toolSize || "medium");
|
||||
if (!Number.isFinite(it.foodServingsMax) || it.foodServingsMax <= 0) it.foodServingsMax = max;
|
||||
if (!Number.isFinite(it.foodServingsRemaining)) {
|
||||
const original = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, laxative: 58, protein: 58, niteropu: 58, ammo: 48, mystery_drug: 52, mercury: 44, giant_drug: 52, dwarf_drug: 52, zunda_juice: 70 }[it.type] || 62;
|
||||
const original = { food: 85, sweet: 62, love_mochi: 62, fight_mochi: 62, sleep_drug: 58, first_aid: 58, sedative: 58, laxative: 58, protein: 58, niteropu: 58, ammo: 48, mystery_drug: 52, mercury: 44, giant_drug: 52, dwarf_drug: 52, zunda_juice: 70 }[it.type] || 62;
|
||||
const ratio = clamp((it.amount ?? original) / original, 0.05, 1);
|
||||
it.foodServingsRemaining = Math.max(1, Math.ceil(it.foodServingsMax * ratio));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,10 @@
|
|||
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
ensureItemEffectState() {
|
||||
if (!this.itemEffectTimers || typeof this.itemEffectTimers !== "object") this.itemEffectTimers = { laxative: 0, ammo: 0 };
|
||||
if (!this.itemEffectTimers || typeof this.itemEffectTimers !== "object") this.itemEffectTimers = { laxative: 0, ammo: 0, sedative: 0 };
|
||||
if (!Number.isFinite(this.itemEffectTimers.laxative)) this.itemEffectTimers.laxative = 0;
|
||||
if (!Number.isFinite(this.itemEffectTimers.ammo)) this.itemEffectTimers.ammo = 0;
|
||||
if (!Number.isFinite(this.itemEffectTimers.sedative)) this.itemEffectTimers.sedative = 0;
|
||||
if (!Number.isFinite(this.laxativePoopTimer)) this.laxativePoopTimer = 3;
|
||||
if (!Number.isFinite(this.ammoBumpCooldown)) this.ammoBumpCooldown = 0;
|
||||
if (this.powerItemMode !== "protein" && this.powerItemMode !== "niteropu") this.powerItemMode = "";
|
||||
|
|
@ -223,6 +224,7 @@
|
|||
let value = this.hasItemEffect("ammo") ? (EFFECT_REGISTRY?.modifier?.("ammo", "speedMultiplier", 10) ?? 10) : 1;
|
||||
if (this.sizeItemMode === "giant_drug") value *= EFFECT_REGISTRY?.modifier?.("giant_drug", "speedMultiplier", 1.5) ?? 1.5;
|
||||
if (this.sizeItemMode === "dwarf_drug") value *= EFFECT_REGISTRY?.modifier?.("dwarf_drug", "speedMultiplier", 0.5) ?? 0.5;
|
||||
if (this.hasItemEffect("sedative")) value *= EFFECT_REGISTRY?.modifier?.("sedative", "speedMultiplier", 0.72) ?? 0.72;
|
||||
return value;
|
||||
},
|
||||
|
||||
|
|
@ -278,6 +280,7 @@
|
|||
if (Math.random() < step * 0.72) this.spawnPowerItemEffect(this.powerItemMode, this.powerItemMode === "protein" ? 0.32 : 0.28);
|
||||
}
|
||||
if (this.hasItemEffect("ammo") && Math.random() < step * 3.0) this.spawnPowerItemEffect("ammo", 0.32);
|
||||
if (this.hasItemEffect("sedative") && Math.random() < step * 0.9) this.spawnPowerItemEffect("sedative", 0.22);
|
||||
if ((this.sizeItemMode === "giant_drug" || this.sizeItemMode === "dwarf_drug") && Math.random() < step * 1.4) this.spawnPowerItemEffect(this.sizeItemMode, 0.24);
|
||||
},
|
||||
|
||||
|
|
@ -291,8 +294,8 @@
|
|||
const side = this.facingDir ? this.facingDir() : (this.facing || 1);
|
||||
const x = this.x - side * this.radius * rand(0.72, 0.96);
|
||||
const y = this.y + this.radius * rand(0.25, 0.48);
|
||||
this.world.spawnZunchi?.(x, y);
|
||||
this.poopCount = (this.poopCount || 0) + 1;
|
||||
this.world.spawnZunchi?.(x, y);
|
||||
this.digest = Math.max(0, (this.digest || 0) - 3.0);
|
||||
const now = this.world?.time || 0;
|
||||
this.bubble("\u3076\u308a\u3085\u3063", 1.2, effectColor("laxative"), { force: true });
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const NORMAL_POOP_MEAL_THRESHOLD = 4;
|
|||
|
||||
function normalPoopUnitsFor(foodType = "", amount = 0, options = {}) {
|
||||
const type = String(foodType || "");
|
||||
if (type === "laxative" || options.laxative === true) return 0;
|
||||
if (type === "laxative" || type === "first_aid" || options.laxative === true) return 0;
|
||||
if (isServingFoodType(type)) return 1;
|
||||
const value = Math.max(0, Number(amount) || 0);
|
||||
return value / 18;
|
||||
|
|
@ -171,9 +171,9 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
|||
};
|
||||
if (Array.isArray(primary)) for (const it of primary) addCandidate(it);
|
||||
|
||||
// 食料・水・薬は、生成直後/移動直後の spatial cache 漏れにも耐える。
|
||||
// item type buckets が使える場合は role ごとの型だけを見ることで、
|
||||
// crowded world で毎回 world.items 全体を足す経路を避ける。
|
||||
// \u98df\u6599\u30fb\u6c34\u30fb\u85ac\u306f\u3001\u751f\u6210\u76f4\u5f8c/\u79fb\u52d5\u76f4\u5f8c\u306e spatial cache \u6f0f\u308c\u306b\u3082\u8010\u3048\u308b\u3002
|
||||
// item type buckets \u304c\u4f7f\u3048\u308b\u5834\u5408\u306f role \u3054\u3068\u306e\u578b\u3060\u3051\u3092\u898b\u308b\u3053\u3068\u3067\u3001
|
||||
// crowded world \u3067\u6bce\u56de world.items \u5168\u4f53\u3092\u8db3\u3059\u7d4c\u8def\u3092\u907f\u3051\u308b\u3002
|
||||
if (foodLikeRole) {
|
||||
if (world?.itemsOfType) {
|
||||
for (const type of foodRoleSearchTypes(role)) for (const it of world.itemsOfType(type) || []) addCandidate(it);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,53 @@
|
|||
// Layer: entity-runtime/tarinai/update-system
|
||||
// Owns local family/friend/grass/nest comfort environment effects.
|
||||
(function (global) {
|
||||
function parasolFootprint(item = null) {
|
||||
if (!item || item.dead || item.type !== "rain_shelter") return null;
|
||||
const r = Math.max(30, Number(item.r || 50) || 50);
|
||||
// The pole foot is the visual and physical center of the shade.
|
||||
return {
|
||||
centerX: Number(item.x || 0),
|
||||
centerY: Number(item.y || 0) + r * 0.84,
|
||||
radiusX: r * 1.58,
|
||||
radiusY: r * 0.68,
|
||||
};
|
||||
}
|
||||
|
||||
function parasolRange(item = null) {
|
||||
return parasolFootprint(item)?.radiusX || 0;
|
||||
}
|
||||
|
||||
function parasolContains(item = null, x = 0, y = 0) {
|
||||
const footprint = parasolFootprint(item);
|
||||
if (!footprint) return false;
|
||||
const nx = (Number(x || 0) - footprint.centerX) / Math.max(1, footprint.radiusX);
|
||||
const ny = (Number(y || 0) - footprint.centerY) / Math.max(1, footprint.radiusY);
|
||||
return nx * nx + ny * ny <= 1;
|
||||
}
|
||||
|
||||
function parasolAt(worldRef, x, y) {
|
||||
if (!worldRef) return null;
|
||||
const candidates = typeof worldRef.itemsOfType === "function"
|
||||
? worldRef.itemsOfType("rain_shelter")
|
||||
: (worldRef.nearbyItems?.(x, y, 240) || []);
|
||||
let best = null;
|
||||
let bestRatio = Infinity;
|
||||
for (const item of candidates || []) {
|
||||
const footprint = parasolFootprint(item);
|
||||
if (!footprint) continue;
|
||||
const nx = (Number(x || 0) - footprint.centerX) / Math.max(1, footprint.radiusX);
|
||||
const ny = (Number(y || 0) - footprint.centerY) / Math.max(1, footprint.radiusY);
|
||||
const ratio = Math.sqrt(nx * nx + ny * ny);
|
||||
if (ratio <= 1 && ratio < bestRatio) { best = item; bestRatio = ratio; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function shelterRange(item = null) { return parasolRange(item); }
|
||||
function shelterAt(worldRef, x, y) { return parasolAt(worldRef, x, y); }
|
||||
function isSheltered(worldRef, x, y) { return Boolean(parasolAt(worldRef, x, y)); }
|
||||
function blocksWeatherAt(worldRef, x, y) { return Boolean(parasolAt(worldRef, x, y)); }
|
||||
|
||||
function updateBody(dt) {
|
||||
const parent = this.parentToFollow ? this.parentToFollow() : null;
|
||||
if (parent && dist(this, parent) < 96) {
|
||||
|
|
@ -30,7 +77,17 @@
|
|||
if (!it || it.dead || it.type !== "nest_box") continue;
|
||||
nestComfort += clamp(1 - distXY(this.x, this.y, it.x, it.y) / 120, 0, 1);
|
||||
}
|
||||
let rainShelterComfort = 0;
|
||||
const rainShelter = parasolAt(this.world, this.x, this.y);
|
||||
if (rainShelter && (this.world?.weather || "") === "light_rain") {
|
||||
const range = parasolRange(rainShelter);
|
||||
rainShelterComfort = clamp(1 - distXY(this.x, this.y, rainShelter.x, rainShelter.y) / range, 0.25, 1);
|
||||
}
|
||||
if (nestComfort > 0) applyNeedRelief(this, { sleep: -dt * Math.min(4, nestComfort * 3), safety: -dt * Math.min(5, nestComfort * 4) });
|
||||
if (rainShelterComfort > 0) {
|
||||
applyNeedRelief(this, { safety: -dt * Math.min(6, rainShelterComfort * 5), health: -dt * Math.min(3, rainShelterComfort * 2), fulfill: -dt * Math.min(2, rainShelterComfort * 1.2) });
|
||||
if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.35, rainShelterComfort * 0.08));
|
||||
}
|
||||
if (grassComfort > 0) {
|
||||
applyNeedRelief(this, { fulfill: -dt * Math.min(2, grassComfort * 0.5), safety: -dt * Math.min(1.5, grassComfort * 0.4) });
|
||||
if (this.state === "panic") this.fearTimer = Math.max(0, this.fearTimer - dt * Math.min(0.22, grassComfort * 0.06));
|
||||
|
|
@ -45,6 +102,9 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
const parasolSystem = Object.freeze({ parasolFootprint, parasolRange, parasolContains, parasolAt, shelterRange, shelterAt, isSheltered, blocksWeatherAt });
|
||||
global.TarinaiParasolSystem = parasolSystem;
|
||||
global.TarinaiRainShelterSystem = parasolSystem;
|
||||
global.TarinaiLocalEnvironmentSystem = Object.freeze({
|
||||
updateOne,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
const TARINAI_NEED_KEYS = ["food", "sleep", "health", "safety", "social", "fulfill"];
|
||||
const TARINAI_NEED_PRIORITY = ["safety", "health", "food", "sleep", "social", "fulfill"];
|
||||
const TARINAI_NEED_LABELS = {
|
||||
food: "\u6442\u990c",
|
||||
food: "\u98df\u6b32",
|
||||
sleep: "\u7761\u7720",
|
||||
health: "\u5065\u5eb7",
|
||||
safety: "\u5b89\u5168",
|
||||
|
|
|
|||
|
|
@ -579,11 +579,11 @@ function shouldWakeFromSleep(tarinai, world, needs) {
|
|||
const elapsed = Math.max(0, now - (Number.isFinite(started) ? started : now));
|
||||
const minSleepDuration = Math.max(2, Number(session.minDuration) || 8);
|
||||
|
||||
// 明確な妨害だけは最低睡眠時間より優先して起床させる。
|
||||
// \u660e\u78ba\u306a\u59a8\u5bb3\u3060\u3051\u306f\u6700\u4f4e\u7761\u7720\u6642\u9593\u3088\u308a\u512a\u5148\u3057\u3066\u8d77\u5e8a\u3055\u305b\u308b\u3002
|
||||
if ((tarinai.hurtTimer || 0) > 0.06 || (tarinai.pokeFlashTimer || 0) > 0.08) return true;
|
||||
if (findNearbyDanger(world, tarinai, 160)) return true;
|
||||
|
||||
// 空腹・睡眠欲の回復・体力回復などの通常要因では、最低2時間は寝続ける。
|
||||
// \u7a7a\u8179\u30fb\u7761\u7720\u6b32\u306e\u56de\u5fa9\u30fb\u4f53\u529b\u56de\u5fa9\u306a\u3069\u306e\u901a\u5e38\u8981\u56e0\u3067\u306f\u3001\u6700\u4f4e2\u6642\u9593\u306f\u5bdd\u7d9a\u3051\u308b\u3002
|
||||
if (elapsed < minSleepDuration) return false;
|
||||
|
||||
const foodNeed = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
|
||||
|
|
@ -642,6 +642,16 @@ function resolveNeedsCore(dt) {
|
|||
const beforeNeeds = this.needs ? { ...this.needs } : createDefaultNeeds();
|
||||
const needs = updateNeedsCached(this, this.world, dt);
|
||||
synchronizeActionTextFromState(this);
|
||||
if (this.state === "seek_toilet" || this.toiletUrgeTargetId) {
|
||||
this.toiletUrgeTargetId = "";
|
||||
this.toiletUrgeStartedAt = 0;
|
||||
this.toiletUrgeDeadline = 0;
|
||||
if (this.state === "seek_toilet") {
|
||||
this.target = null;
|
||||
this.behaviorLockTimer = 0;
|
||||
this.goIdle?.("\u5f85\u3063\u3066\u3044\u308b");
|
||||
}
|
||||
}
|
||||
if (processForcedBehaviorQueue(this, this.world, needs)) return;
|
||||
if (protectSleepSession(this, this.world, needs)) return;
|
||||
if (continueBuildPlan(this, this.world, dt)) return;
|
||||
|
|
@ -1007,6 +1017,8 @@ function resolveNeedsCore(dt) {
|
|||
if (!Number.isFinite(this.world?.w) || !Number.isFinite(this.world?.h)) return false;
|
||||
if ((this.entryTimer || 0) > 0.04) return false;
|
||||
if (!this.isSunbathTime()) return false;
|
||||
const parasolSystem = globalThis.TarinaiParasolSystem || globalThis.TarinaiRainShelterSystem;
|
||||
if (parasolSystem?.blocksWeatherAt?.(this.world, this.x, this.y)) return false;
|
||||
const currentFelt = this.world?.feltTemperatureFor?.(this) ?? this.world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
if (Number.isFinite(Number(currentFelt)) && Number(currentFelt) >= 22) return false;
|
||||
if ((this.sunbathCooldown || 0) > 0) return false;
|
||||
|
|
@ -1159,28 +1171,34 @@ function resolveNeedsCore(dt) {
|
|||
return global.TarinaiNeedPlannerSystem.updateOne(this, dt);
|
||||
},
|
||||
|
||||
dropPoopNow(source = "normal") {
|
||||
const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3;
|
||||
if ((Number(this.digest) || 0) + 1e-6 < threshold) return false;
|
||||
this.digest = Math.max(0, (Number(this.digest) || 0) - threshold);
|
||||
this.poopCount = (this.poopCount || 0) + 1;
|
||||
const side = this.facingDir();
|
||||
const backX = this.x - side * this.radius * rand(0.72, 0.96);
|
||||
const backY = this.y + this.radius * rand(0.25, 0.48);
|
||||
this.world.spawnZunchi(backX, backY, this);
|
||||
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true });
|
||||
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] });
|
||||
return true;
|
||||
},
|
||||
|
||||
makePoop(force = 1) {
|
||||
const add = Math.max(0, Number(force) || 0);
|
||||
if (add <= 0) return false;
|
||||
const threshold = NORMAL_POOP_MEAL_THRESHOLD || 3;
|
||||
this.digest = Math.max(0, Number(this.digest) || 0) + add;
|
||||
if (this.digest + 1e-6 < threshold) return false;
|
||||
this.digest = Math.max(0, this.digest - threshold);
|
||||
if (this.hasLodgedPinEffect?.("blocksZunchi")) {
|
||||
this.digest = Math.max(0, this.digest - threshold);
|
||||
this.oshiriByoZunchiStock = Math.min(18, Math.max(0, this.oshiriByoZunchiStock || 0) + 1);
|
||||
this.thought = (this.oshiriByoZunchiStock || 0) >= 6 ? "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044" : "\u304a\u3057\u308a\u75c5\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b";
|
||||
if ((this.oshiriByoZunchiStock || 0) >= 6) applyNeedShock(this, { health: 8 });
|
||||
return false;
|
||||
}
|
||||
this.poopCount = (this.poopCount || 0) + 1;
|
||||
const side = this.facingDir();
|
||||
const backX = this.x - side * this.radius * rand(0.72, 0.96);
|
||||
const backY = this.y + this.radius * rand(0.25, 0.48);
|
||||
this.world.spawnZunchi(backX, backY, this);
|
||||
const now = this.world?.time || 0;
|
||||
this.bubble("\u3076\u308a\u3085\u3063", 3.2, "rgba(62,84,45,0.78)", { force: true });
|
||||
if (Math.random() < 0.35) this.world.log(`${this.name}\u304c\u305a\u3093\u3061\u3092\u843d\u3068\u3057\u305f\u3002`, null, { participants: [this] });
|
||||
return true;
|
||||
return this.dropPoopNow?.("normal") || false;
|
||||
},
|
||||
|
||||
interactWithItems(dt) {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ function socialFightMateWeights(t, other, world, options = {}) {
|
|||
if (t.type === "angry" || other.type === "angry") fightWeight *= 1.42;
|
||||
if (t.isZunchiSlave || other.isZunchiSlave) fightWeight *= 1.22;
|
||||
if (fightMochi) fightWeight *= 3.4;
|
||||
if (t.hasItemEffect?.("sedative") || other.hasItemEffect?.("sedative")) fightWeight *= 0.28;
|
||||
if (loveMochi) fightWeight *= 0.58;
|
||||
fightWeight *= clamp(1 - affinity / 150, 0.34, 1.08);
|
||||
fightWeight *= clamp(1 - fear / 170, fightMochi ? 0.72 : 0.42, 1.05);
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@
|
|||
const step = Math.min(maxRestore, dt * restore);
|
||||
const sx = dx / d * step;
|
||||
const sy = dy / d * step;
|
||||
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, sx, sy, { dt, reason: externalActive ? "tarinai-sleep-restore-weak" : "tarinai-sleep-move" });
|
||||
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, sx, sy, { dt });
|
||||
else { this.x += sx; this.y += sy; }
|
||||
} else if (!externalActive && Math.hypot((this.vx || 0) + (this.impulseVx || 0), (this.vy || 0) + (this.impulseVy || 0)) < 0.35) {
|
||||
this.x = spot.x;
|
||||
|
|
@ -363,13 +363,13 @@
|
|||
const moveDx = ((Number.isFinite(this.vx) ? this.vx : 0) + impulseVx) * dt;
|
||||
const moveDy = ((Number.isFinite(this.vy) ? this.vy : 0) + impulseVy) * dt;
|
||||
if (Math.hypot(moveDx, moveDy) > 0.001) {
|
||||
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, moveDx, moveDy, { dt, reason: "tarinai-sleep-physical-full", maxChecks: 18, maxStep: 10 });
|
||||
if (this.world?.moveTarinaiWithCollision) this.world.moveTarinaiWithCollision(this, moveDx, moveDy, { dt, maxChecks: 18, maxStep: 10 });
|
||||
else { this.x += moveDx; this.y += moveDy; }
|
||||
}
|
||||
if (this.world?.resolveSolidObstacleCollision) {
|
||||
const beforeContactX = Number(this.x || 0) || 0;
|
||||
const beforeContactY = Number(this.y || 0) || 0;
|
||||
const pushed = this.world.resolveSolidObstacleCollision(this, { maxPasses: 2, searchRadius: Math.max(72, (Number(this.radius) || 22) + 120), maxChecks: 18, slop: 0.28, reason: "tarinai-sleep-full-contact" });
|
||||
const pushed = this.world.resolveSolidObstacleCollision(this, { maxPasses: 2, searchRadius: Math.max(72, (Number(this.radius) || 22) + 120), maxChecks: 18, slop: 0.28 });
|
||||
if (pushed) {
|
||||
this.lastSolidObstacleCollisionAt = this.world.time || 0;
|
||||
marker?.markSleepExternalMotion?.(this, dt, "sleep-full-contact");
|
||||
|
|
@ -615,8 +615,8 @@
|
|||
if (this.world?.moveTarinaiWithCollision) {
|
||||
const motionOnlyPass = Boolean(this._motionOnlyPhysicsPass);
|
||||
this.world.moveTarinaiWithCollision(this, moveDx, moveDy, motionOnlyPass
|
||||
? { dt, reason: "tarinai-move-smooth", light: true, maxChecks: this.state === "panic" ? 18 : 14, maxStep: this.state === "panic" ? 12 : 14 }
|
||||
: { dt, reason: "tarinai-move" });
|
||||
? { dt, light: true, maxChecks: this.state === "panic" ? 18 : 14, maxStep: this.state === "panic" ? 12 : 14 }
|
||||
: { dt });
|
||||
}
|
||||
else {
|
||||
this.x += moveDx;
|
||||
|
|
@ -675,19 +675,19 @@
|
|||
checkLife(dt = 0.016) {
|
||||
let reason = "";
|
||||
const activeDiseaseDeathReason = () => {
|
||||
if (this.zunchiDisease) return "ずんち病";
|
||||
if (this.sleepDisease) return "ねむり病";
|
||||
if (this.explosionDisease) return "爆発病";
|
||||
if (this.fightDisease) return "きずつき病";
|
||||
if (this.zunchiDisease) return "\u305a\u3093\u3061\u75c5";
|
||||
if (this.sleepDisease) return "\u306d\u3080\u308a\u75c5";
|
||||
if (this.explosionDisease) return "\u7206\u767a\u75c5";
|
||||
if (this.fightDisease) return "\u304d\u305a\u3064\u304d\u75c5";
|
||||
return "";
|
||||
};
|
||||
if (this.energy <= 0) {
|
||||
reason = this.temperatureDeathReason?.() || activeDiseaseDeathReason() || this.dominantDeathCause("", { lowHealth: true }) || "事故";
|
||||
reason = this.temperatureDeathReason?.() || activeDiseaseDeathReason() || this.dominantDeathCause("", { lowHealth: true }) || "\u4e8b\u6545";
|
||||
}
|
||||
else if (this.hunger >= 108) reason = "飢餓";
|
||||
else if (this.age >= this.lifeSpan) reason = "天寿を全うした";
|
||||
else if (this.hunger >= 108) reason = "\u98e2\u9913";
|
||||
else if (this.age >= this.lifeSpan) reason = "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";
|
||||
else if (this.stress >= 124) {
|
||||
reason = activeDiseaseDeathReason() || this.dominantDeathCause("ストレス過多", { stressDeath: true }) || "ストレス過多";
|
||||
reason = activeDiseaseDeathReason() || this.dominantDeathCause("\u30b9\u30c8\u30ec\u30b9\u904e\u591a", { stressDeath: true }) || "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
|
||||
}
|
||||
if (reason) this.die(reason);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@
|
|||
}
|
||||
this.energy = clamp((this.energy || 0) + dt * 0.03, 0, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(this) : (Number(this.maxEnergy) || 100));
|
||||
const felt = this.world?.feltTemperatureFor?.(this) ?? this.world?.temperatureAt?.(this.x, this.y, this) ?? this.feltTemperature ?? this.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
if (!this.isSunbathTime() || Number(felt) >= 22 || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.fightDisease || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0;
|
||||
const parasolSystem = globalThis.TarinaiParasolSystem || globalThis.TarinaiRainShelterSystem;
|
||||
const sunlightBlocked = parasolSystem?.blocksWeatherAt?.(this.world, this.x, this.y) || false;
|
||||
if (!this.isSunbathTime() || sunlightBlocked || Number(felt) >= 22 || this.hunger > 82 || this.energy < 8 || this.state === "panic" || this.fightDisease || this.stuckPushpinId || this.currentLodgedPin?.()) this.sunbathTimer = 0;
|
||||
if ((this.sunbathTimer || 0) <= 0 && this.state === "sunbath") {
|
||||
this.finishSunbath?.();
|
||||
this._activeSunbathSpriteId = "";
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@
|
|||
t.updateSpecialDiseases(dt);
|
||||
if (t.updateBurning) t.updateBurning(dt);
|
||||
if (t.updateItemEffects) t.updateItemEffects(dt);
|
||||
if (t.hasItemEffect?.("sedative")) {
|
||||
t.fearTimer = Math.max(0, (t.fearTimer || 0) - dt * 1.8);
|
||||
t.intimidatedTimer = Math.max(0, (t.intimidatedTimer || 0) - dt * 1.4);
|
||||
t.fightMochiTimer = Math.max(0, (t.fightMochiTimer || 0) - dt * 2.6);
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(t, { safety: -dt * 2.6, social: -dt * 0.5 });
|
||||
}
|
||||
if (t.dead) return { done: true };
|
||||
t.loveMochiTimer = Math.max(0, (t.loveMochiTimer || 0) - dt);
|
||||
t.fightMochiTimer = Math.max(0, (t.fightMochiTimer || 0) - dt);
|
||||
|
|
@ -56,7 +62,7 @@
|
|||
t.intimidateTargetId = null;
|
||||
if (t.state === "intimidate" || t.state === "ant_intimidate") {
|
||||
t.spriteLockUntil = 0;
|
||||
t.setActionState?.("idle", { target: null, reason: "威嚇を終えた", clearTarget: true, sleeping: false });
|
||||
t.setActionState?.("idle", { target: null, reason: "\u5a01\u5687\u3092\u7d42\u3048\u305f", clearTarget: true, sleeping: false });
|
||||
}
|
||||
}
|
||||
if (t.birthRitualTimer > 0) {
|
||||
|
|
@ -194,8 +200,11 @@
|
|||
t.vx *= Math.pow(0.996, dt * 60);
|
||||
t.vy *= Math.pow(0.996, dt * 60);
|
||||
} else if (t.world.weather === "light_rain") {
|
||||
t.energy += dt * 0.018;
|
||||
applyNeedRelief(t, { health: -dt * 1, safety: -dt * 1 });
|
||||
const shelteredFromRain = global.TarinaiRainShelterSystem?.isSheltered?.(t.world, t.x, t.y) || false;
|
||||
if (!shelteredFromRain) {
|
||||
t.energy += dt * 0.018;
|
||||
applyNeedRelief(t, { health: -dt * 1, safety: -dt * 1 });
|
||||
}
|
||||
} else if (t.world.weather === "sunny") {
|
||||
t.vx *= Math.pow(1.001, dt * 60);
|
||||
t.vy *= Math.pow(1.001, dt * 60);
|
||||
|
|
|
|||
|
|
@ -158,7 +158,6 @@
|
|||
if (world?.moveTarinaiWithCollision) {
|
||||
world.moveTarinaiWithCollision(t, moveX, moveY, {
|
||||
dt,
|
||||
reason: state === "sleep" ? "tarinai-sleep-physical" : "tarinai-passive-physical",
|
||||
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
||||
maxStep: 10,
|
||||
});
|
||||
|
|
@ -192,7 +191,6 @@
|
|||
searchRadius: Math.max(72, (numeric(t.radius, 22) || 22) + 120),
|
||||
maxChecks: context.cadence?.lane === "sleep" ? 18 : 22,
|
||||
slop: 0.28,
|
||||
reason: state === "eat" ? "tarinai-eat-passive-contact" : (state === "sleep" ? "tarinai-sleep-passive-contact" : "tarinai-locked-passive-contact"),
|
||||
});
|
||||
if (pushed) {
|
||||
t.lastSolidObstacleCollisionAt = world.time || 0;
|
||||
|
|
|
|||
|
|
@ -357,9 +357,9 @@ function bindUI() {
|
|||
renderStats();
|
||||
});
|
||||
const toggleChartSeries = (target) => {
|
||||
const icon = target?.closest?.("i[data-series-key]");
|
||||
if (!icon) return false;
|
||||
const key = String(icon.dataset.seriesKey || "");
|
||||
const control = target?.closest?.("[data-series-key]");
|
||||
if (!control) return false;
|
||||
const key = String(control.dataset.seriesKey || "");
|
||||
if (!key) return false;
|
||||
if (!(uiCache.chartHiddenSeries instanceof Set)) uiCache.chartHiddenSeries = new Set();
|
||||
if (uiCache.chartHiddenSeries.has(key)) uiCache.chartHiddenSeries.delete(key);
|
||||
|
|
@ -370,10 +370,6 @@ function bindUI() {
|
|||
return true;
|
||||
};
|
||||
ui.colonyChartLegend?.addEventListener("click", (e) => toggleChartSeries(e.target));
|
||||
ui.colonyChartLegend?.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") return;
|
||||
if (toggleChartSeries(e.target)) e.preventDefault();
|
||||
});
|
||||
ui.logPushEnabled?.addEventListener("change", () => {
|
||||
uiCache.logPushEnabled = Boolean(ui.logPushEnabled.checked);
|
||||
audio.notify?.();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
function colonyMoodTip() {
|
||||
const mood = world?.evaluateColonyMood?.(true) || world?.colonyMood || null;
|
||||
return String(mood?.description || "大きな問題が少なく、落ち着いた状態です。");
|
||||
return String(mood?.description || "\u5927\u304d\u306a\u554f\u984c\u304c\u5c11\u306a\u304f\u3001\u843d\u3061\u7740\u3044\u305f\u72b6\u614b\u3067\u3059\u3002");
|
||||
}
|
||||
|
||||
function bindColonyMoodTooltip() {
|
||||
|
|
@ -47,9 +47,9 @@ function renderStats() {
|
|||
setTextIfChanged(ui.statJuvenile, "juvenile", values.juvenile);
|
||||
setTextIfChanged(ui.statElder, "elder", values.elder);
|
||||
setTextIfChanged(ui.statDead, "dead", values.dead);
|
||||
const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "安定" };
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "安定");
|
||||
const tempLabel = `${Math.round(temp)}℃`;
|
||||
const colonyMood = world.evaluateColonyMood?.() || world.colonyMood || { label: "\u5b89\u5b9a" };
|
||||
setTextIfChanged(ui.statColonyMood, "colonyMood", colonyMood?.label || "\u5b89\u5b9a");
|
||||
const tempLabel = `${Math.round(temp)}\u2103`;
|
||||
setTextIfChanged(ui.temperatureValue, "temperature", tempLabel);
|
||||
if (ui.temperatureSlider) {
|
||||
ui.temperatureSlider.disabled = world.temperatureAuto !== false;
|
||||
|
|
@ -67,7 +67,7 @@ function renderStats() {
|
|||
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 || "∞"}`);
|
||||
setTextIfChanged(ui.colonyLimitStatus, "colonyLimitStatus", `\u305f\u308a\u306a\u3044 ${values.pop} / ${tarinaiLimit || "\u221e"}\u3000\u30aa\u30d6\u30b8\u30a7\u30af\u30c8 ${objectCount} / ${objectLimit || "\u221e"}`);
|
||||
bindColonyMoodTooltip();
|
||||
window.TarinaiTooltips.refresh(ui.statColonyMood?.closest?.(".stat") || ui.statColonyMood);
|
||||
window.TarinaiGroundUI.update(world);
|
||||
|
|
@ -188,9 +188,9 @@ function chartSeasonDayLabelForTime(t = 0) {
|
|||
const dayLength = Math.max(1, Number(CONFIG.dayLength || 120) || 120);
|
||||
const dayNumber = Math.floor(Math.max(0, Number(t || 0) || 0) / dayLength) + 1;
|
||||
const seasonIndex = Math.floor(((dayNumber - 1) % 20) / 5);
|
||||
const season = ["春", "夏", "秋", "冬"][seasonIndex] || "春";
|
||||
const season = ["\u6625", "\u590f", "\u79cb", "\u51ac"][seasonIndex] || "\u6625";
|
||||
const seasonDay = ((dayNumber - 1) % 5) + 1;
|
||||
return `${season}${seasonDay}日`;
|
||||
return `${season}${seasonDay}\u65e5`;
|
||||
}
|
||||
|
||||
function niceChartMax(raw = 0) {
|
||||
|
|
@ -284,43 +284,43 @@ function drawColonyChart(values) {
|
|||
const cy = yFor(last, s).toFixed(1);
|
||||
return `<polyline points="${pts}" fill="none" stroke="${s.color}" stroke-width="${s.key === "pop" ? 2.4 : 1.8}" stroke-linecap="round" stroke-linejoin="round"/><circle cx="${cx}" cy="${cy}" r="2.7" fill="${s.color}"/>`;
|
||||
}).join("");
|
||||
chart.innerHTML = `<svg viewBox="0 0 ${cssW} ${cssH}" preserveAspectRatio="none" aria-hidden="true"><rect x="0" y="0" width="${cssW}" height="${cssH}" rx="14" fill="rgba(255,255,255,0.18)"/>${grid}${dayMarkers.join("")}${lines}<text x="${pad.l}" y="${cssH - 8}" font-size="10" fill="rgba(117,106,94,0.76)">2時間ごと / 1週間</text></svg>`;
|
||||
chart.innerHTML = `<svg viewBox="0 0 ${cssW} ${cssH}" preserveAspectRatio="none" aria-hidden="true"><rect x="0" y="0" width="${cssW}" height="${cssH}" rx="14" fill="rgba(255,255,255,0.18)"/>${grid}${dayMarkers.join("")}${lines}<text x="${pad.l}" y="${cssH - 8}" font-size="10" fill="rgba(117,106,94,0.76)">2\u6642\u9593\u3054\u3068 / 1\u9031\u9593</text></svg>`;
|
||||
if (ui.colonyChartLegend) ui.colonyChartLegend.innerHTML = allSeries.map(s => {
|
||||
const hidden = hiddenSeries.has(s.key);
|
||||
return `<span class="${hidden ? "series-hidden" : ""}"><i role="button" tabindex="0" data-series-key="${s.key}" style="background:${s.color}" title="${hidden ? "表示する" : "非表示にする"}"></i>${s.label} ${formatChartValue(s.key, rows[rows.length - 1] || values)}</span>`;
|
||||
return `<button type="button" class="chart-series-toggle ${hidden ? "series-hidden" : ""}" data-series-key="${s.key}" aria-pressed="${hidden ? "false" : "true"}" title="${hidden ? "\u8868\u793a\u3059\u308b" : "\u975e\u8868\u793a\u306b\u3059\u308b"}"><i aria-hidden="true" style="background:${s.color}"></i><span>${s.label} ${formatChartValue(s.key, rows[rows.length - 1] || values)}</span></button>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function chartSeries(mode = "population") {
|
||||
if (mode === "life") return [
|
||||
{ key: "stress", label: "ストレス", color: "#d96262", percent: true },
|
||||
{ key: "need_food", label: "摂餌", color: "#e4a33f", percent: true },
|
||||
{ key: "need_sleep", label: "睡眠", color: "#6f8edc", percent: true },
|
||||
{ key: "need_health", label: "健康", color: "#2e9d68", percent: true },
|
||||
{ key: "need_safety", label: "安全", color: "#d65e46", percent: true },
|
||||
{ key: "need_social", label: "関係", color: "#9b78d4", percent: true },
|
||||
{ key: "need_fulfill", label: "充足", color: "#0072b2", percent: true },
|
||||
{ key: "stress", label: "\u30b9\u30c8\u30ec\u30b9", color: "#d96262", percent: true },
|
||||
{ key: "need_food", label: "\u98df\u6b32", color: "#e4a33f", percent: true },
|
||||
{ key: "need_sleep", label: "\u7761\u7720", color: "#6f8edc", percent: true },
|
||||
{ key: "need_health", label: "\u5065\u5eb7", color: "#2e9d68", percent: true },
|
||||
{ key: "need_safety", label: "\u5b89\u5168", color: "#d65e46", percent: true },
|
||||
{ key: "need_social", label: "\u95a2\u4fc2", color: "#9b78d4", percent: true },
|
||||
{ key: "need_fulfill", label: "\u5145\u8db3", color: "#0072b2", percent: true },
|
||||
];
|
||||
if (mode === "objects") return [
|
||||
{ key: "obj_grass", label: "草", color: "#5b9d61" },
|
||||
{ key: "obj_zunchi", label: "ずんち", color: "#6f5a2f" },
|
||||
{ key: "obj_trace", label: "死骸", color: "#8a7054" },
|
||||
{ key: "obj_grass_bed", label: "かんたんベッド", color: "#78ff45" },
|
||||
{ key: "obj_water", label: "雨滴", color: "#5aa8d6" },
|
||||
{ key: "temperature", label: "気温", color: "#c46f3c" },
|
||||
{ key: "obj_grass", label: "\u8349", color: "#5b9d61" },
|
||||
{ key: "obj_zunchi", label: "\u305a\u3093\u3061", color: "#6f5a2f" },
|
||||
{ key: "obj_trace", label: "\u6b7b\u9ab8", color: "#8a7054" },
|
||||
{ key: "obj_grass_bed", label: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", color: "#78ff45" },
|
||||
{ key: "obj_water", label: "\u96e8\u6ef4", color: "#5aa8d6" },
|
||||
{ key: "temperature", label: "\u6c17\u6e29", color: "#c46f3c" },
|
||||
];
|
||||
return [
|
||||
{ key: "pop", label: "総数", color: "#5d8ee6" },
|
||||
{ key: "sick", label: "病気", color: "#d96262" },
|
||||
{ key: "juvenile", label: "幼体", color: "#78a642" },
|
||||
{ key: "elder", label: "老体", color: "#8a7054" },
|
||||
{ key: "births", label: "誕生数", color: "#d48ac2" },
|
||||
{ key: "deaths", label: "死亡数", color: "#4d4d56" },
|
||||
{ key: "pop", label: "\u7dcf\u6570", color: "#5d8ee6" },
|
||||
{ key: "sick", label: "\u75c5\u6c17", color: "#d96262" },
|
||||
{ key: "juvenile", label: "\u5e7c\u4f53", color: "#78a642" },
|
||||
{ key: "elder", label: "\u8001\u4f53", color: "#8a7054" },
|
||||
{ key: "births", label: "\u8a95\u751f\u6570", color: "#d48ac2" },
|
||||
{ key: "deaths", label: "\u6b7b\u4ea1\u6570", color: "#4d4d56" },
|
||||
];
|
||||
}
|
||||
|
||||
function formatChartValue(key, values) {
|
||||
const v = values[key] ?? 0;
|
||||
if (key === "temperature") return `${Math.round(Number(v) || 0)}℃`;
|
||||
if (key === "temperature") return `${Math.round(Number(v) || 0)}\u2103`;
|
||||
return String(Math.round(v));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@
|
|||
world.pointer.motion = 0;
|
||||
world.hoverGrabTarget = null;
|
||||
world.hoverDeleteTarget = null;
|
||||
world.hoverGiveTarget = null;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,24 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function directFeedTargetAt(p, item = null) {
|
||||
const feeding = global.TarinaiDirectFeedingSystem;
|
||||
if (!p?.inside || !feeding) return null;
|
||||
const type = item?.type || (typeof global.toolItemType === "function" ? global.toolItemType(world.tool || "") : "");
|
||||
if (!feeding.isDirectFeedType?.(type)) return null;
|
||||
if (item && !feeding.hasConsumableAmount?.(item)) return null;
|
||||
return feeding.findTargetAt?.(world, p.x, p.y) || null;
|
||||
}
|
||||
|
||||
function setGiveHover(target = null) {
|
||||
const next = target && !target.dead ? target : null;
|
||||
if (world.hoverGiveTarget !== next) {
|
||||
world.hoverGiveTarget = next;
|
||||
world.drawListDirty = true;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function rememberGrabOrigin(target) {
|
||||
if (!target || uiCache.grabKind !== "tarinai") return;
|
||||
uiCache.grabOrigin = { x: target.x, y: target.y, vx: target.vx || 0, vy: target.vy || 0 };
|
||||
|
|
@ -208,6 +226,9 @@
|
|||
}
|
||||
|
||||
function moveGrab(p, clientX, clientY, options = {}) {
|
||||
world.pointer.x = p?.x ?? world.pointer.x;
|
||||
world.pointer.y = p?.y ?? world.pointer.y;
|
||||
world.pointer.inside = Boolean(p?.inside);
|
||||
const target = uiCache.grabTarget;
|
||||
if (!target || target.dead) {
|
||||
uiCache.grabbing = false;
|
||||
|
|
@ -266,6 +287,7 @@
|
|||
target.prevY = target.y - dy;
|
||||
target.spinVelocity = clamp((target.spinVelocity || 0) + dx * 0.06, -38, 38);
|
||||
}
|
||||
setGiveHover(uiCache.grabKind === "item" ? directFeedTargetAt(p, target) : null);
|
||||
world.drawListDirty = true;
|
||||
const now = performance.now();
|
||||
if (!uiCache.grabLastSpatialAt || now - uiCache.grabLastSpatialAt > 80) {
|
||||
|
|
@ -280,6 +302,7 @@
|
|||
function endGrab(clientX, clientY) {
|
||||
if (!uiCache.grabbing) return false;
|
||||
const target = uiCache.grabTarget;
|
||||
let directlyGiven = false;
|
||||
if (target) {
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
target.target = null;
|
||||
|
|
@ -287,28 +310,36 @@
|
|||
target.surpriseTimer = Math.max(target.surpriseTimer || 0, 0.15);
|
||||
finishTarinaiGrab(target, clientX, clientY);
|
||||
} else if (uiCache.grabKind === "item") {
|
||||
const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900);
|
||||
const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900);
|
||||
const releaseSpeed = Math.hypot(releaseVx, releaseVy);
|
||||
if (target.type === "plushie" && typeof target.fling === "function") {
|
||||
const sourceX = Number(uiCache.grabLastWorldX ?? target.x) || target.x;
|
||||
const sourceY = Number(uiCache.grabLastWorldY ?? target.y) || target.y;
|
||||
const vx = releaseSpeed > 80 ? releaseVx : 0;
|
||||
const vy = releaseSpeed > 80 ? releaseVy : 0;
|
||||
target.fling(world, sourceX, sourceY, { source: "pinch", vx, vy, speed: Math.max(620, releaseSpeed * 1.05), life: 2.8 });
|
||||
} else if (releaseSpeed > 18) {
|
||||
target.vx = releaseVx;
|
||||
target.vy = releaseVy;
|
||||
const frameDt = Math.max(1 / 90, Math.min(1 / 30, Number(world.dt || 1 / 60) || 1 / 60));
|
||||
target.prevX = target.x - releaseVx * frameDt;
|
||||
target.prevY = target.y - releaseVy * frameDt;
|
||||
if ((target.type === "ball" || target.type === "balloon")) target.spinVelocity = clamp((target.spinVelocity || 0) + releaseVx * 0.018, -42, 42);
|
||||
} else if ((target.type === "ball" || target.type === "balloon")) {
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
const releasePoint = screenToWorldClient(clientX, clientY);
|
||||
const giveTarget = directFeedTargetAt(releasePoint, target);
|
||||
const result = giveTarget
|
||||
? global.TarinaiDirectFeedingSystem?.give?.(world, giveTarget, target, { source: "pinch" })
|
||||
: null;
|
||||
directlyGiven = Boolean(result);
|
||||
if (!directlyGiven) {
|
||||
const releaseVx = clamp(Number(uiCache.grabVelocityX || target.vx || 0) || 0, -900, 900);
|
||||
const releaseVy = clamp(Number(uiCache.grabVelocityY || target.vy || 0) || 0, -900, 900);
|
||||
const releaseSpeed = Math.hypot(releaseVx, releaseVy);
|
||||
if (target.type === "plushie" && typeof target.fling === "function") {
|
||||
const sourceX = Number(uiCache.grabLastWorldX ?? target.x) || target.x;
|
||||
const sourceY = Number(uiCache.grabLastWorldY ?? target.y) || target.y;
|
||||
const vx = releaseSpeed > 80 ? releaseVx : 0;
|
||||
const vy = releaseSpeed > 80 ? releaseVy : 0;
|
||||
target.fling(world, sourceX, sourceY, { source: "pinch", vx, vy, speed: Math.max(620, releaseSpeed * 1.05), life: 2.8 });
|
||||
} else if (releaseSpeed > 18) {
|
||||
target.vx = releaseVx;
|
||||
target.vy = releaseVy;
|
||||
const frameDt = Math.max(1 / 90, Math.min(1 / 30, Number(world.dt || 1 / 60) || 1 / 60));
|
||||
target.prevX = target.x - releaseVx * frameDt;
|
||||
target.prevY = target.y - releaseVy * frameDt;
|
||||
if ((target.type === "ball" || target.type === "balloon")) target.spinVelocity = clamp((target.spinVelocity || 0) + releaseVx * 0.018, -42, 42);
|
||||
} else if ((target.type === "ball" || target.type === "balloon")) {
|
||||
target.prevX = target.x;
|
||||
target.prevY = target.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uiCache.grabKind === "item") syncGrabbedPhysicsItem(target, "pinch-release");
|
||||
if (uiCache.grabKind === "item" && !directlyGiven && !target.dead) syncGrabbedPhysicsItem(target, "pinch-release");
|
||||
target.playerHeld = false;
|
||||
target._heldByPlayer = false;
|
||||
if (uiCache.grabKind === "tarinai") {
|
||||
|
|
@ -317,22 +348,23 @@
|
|||
target.impulseVx = 0;
|
||||
target.impulseVy = 0;
|
||||
target.pinchThoughtTimer = 0;
|
||||
if (target.thought === "つままれている") target.thought = "";
|
||||
if (target.thought === "\u3064\u307e\u307e\u308c\u3066\u3044\u308b") target.thought = "";
|
||||
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(target) : null;
|
||||
const reason = String(behavior?.reason || behavior?.text || behavior?.label || "");
|
||||
if (reason === "つままれている" && typeof clearBehavior === "function") clearBehavior(target);
|
||||
if (reason === "\u3064\u307e\u307e\u308c\u3066\u3044\u308b" && typeof clearBehavior === "function") clearBehavior(target);
|
||||
}
|
||||
if (target.type === "duplicator") {
|
||||
target.duplicatorLoadSuppressedUntil = Math.max(target.duplicatorLoadSuppressedUntil || 0, (world.time || 0) + 1.2);
|
||||
}
|
||||
}
|
||||
audio.drop?.();
|
||||
if (!directlyGiven) audio.drop?.();
|
||||
uiCache.grabbing = false;
|
||||
uiCache.grabTarget = null;
|
||||
uiCache.grabKind = "";
|
||||
uiCache.grabVelocityX = 0;
|
||||
uiCache.grabVelocityY = 0;
|
||||
uiCache.grabLastMoveAt = 0;
|
||||
setGiveHover(null);
|
||||
world.rebuildSpatial?.();
|
||||
uiCache.grabLastSpatialAt = 0;
|
||||
canvas.style.cursor = cursorForTool(world.tool);
|
||||
|
|
@ -537,7 +569,10 @@
|
|||
world.pointer.inside = !!p.inside;
|
||||
const delta = Math.hypot((p.x || 0) - (prevX || 0), (p.y || 0) - (prevY || 0));
|
||||
world.pointer.motion = clamp(delta / 18, 0, 2);
|
||||
const inactivePointerAction = !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting;
|
||||
const giveTarget = inactivePointerAction ? directFeedTargetAt(p) : null;
|
||||
if (world.tool === "shoot") {
|
||||
setGiveHover(null);
|
||||
if (p.inside) {
|
||||
if (world.shootCursorActive) {
|
||||
const lastRawX = Number.isFinite(Number(world.shootPointerRawX)) ? Number(world.shootPointerRawX) : prevX;
|
||||
|
|
@ -549,8 +584,14 @@
|
|||
world.shootPointerRawX = p.x;
|
||||
world.shootPointerRawY = p.y;
|
||||
}
|
||||
canvas.style.cursor = "none";
|
||||
} else if (world.tool === "pinch" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) {
|
||||
canvas.style.cursor = "none";
|
||||
} else if (giveTarget) {
|
||||
setGiveHover(giveTarget);
|
||||
if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; }
|
||||
if (world.hoverDeleteTarget) { world.hoverDeleteTarget = null; world.drawListDirty = true; }
|
||||
canvas.style.cursor = "pointer";
|
||||
} else if (world.tool === "pinch" && p.inside && inactivePointerAction) {
|
||||
setGiveHover(null);
|
||||
const target = world.findGrabTargetAt ? world.findGrabTargetAt(p.x, p.y) : null;
|
||||
if (world.hoverGrabTarget !== target) {
|
||||
world.hoverGrabTarget = target || null;
|
||||
|
|
@ -558,7 +599,8 @@
|
|||
}
|
||||
if (world.hoverDeleteTarget) { world.hoverDeleteTarget = null; world.drawListDirty = true; }
|
||||
canvas.style.cursor = target ? "grab" : "default";
|
||||
} else if (world.tool === "delete" && p.inside && !uiCache.grabbing && !uiCache.panning && !uiCache.hosing && !uiCache.areaDeleting && !uiCache.shooting) {
|
||||
} else if (world.tool === "delete" && p.inside && inactivePointerAction) {
|
||||
setGiveHover(null);
|
||||
const target = world.findDeleteToolTargetAt ? world.findDeleteToolTargetAt(p.x, p.y) : null;
|
||||
if (world.hoverDeleteTarget !== target) {
|
||||
world.hoverDeleteTarget = target || null;
|
||||
|
|
@ -567,6 +609,7 @@
|
|||
if (world.hoverGrabTarget) { world.hoverGrabTarget = null; world.drawListDirty = true; }
|
||||
canvas.style.cursor = target ? "pointer" : "default";
|
||||
} else {
|
||||
setGiveHover(null);
|
||||
if (world.hoverGrabTarget) {
|
||||
world.hoverGrabTarget = null;
|
||||
world.drawListDirty = true;
|
||||
|
|
@ -596,6 +639,7 @@
|
|||
if (world) {
|
||||
world.hoverGrabTarget = null;
|
||||
world.hoverDeleteTarget = null;
|
||||
world.hoverGiveTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ function applyFieldLayout(fieldType = world.fieldType || "garden") {
|
|||
|
||||
function normalizeResetPresetId(presetId = "default") {
|
||||
if (window.TarinaiResetPresets?.normalize) return window.TarinaiResetPresets.normalize(presetId);
|
||||
return new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy"]).has(String(presetId || "")) ? String(presetId) : "default";
|
||||
return new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy", "hair_trigger"]).has(String(presetId || "")) ? String(presetId) : "default";
|
||||
}
|
||||
|
||||
function syncFieldDialogChoices() {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function shouldShowInEventLog(entry = {}) {
|
|||
const type = String(entry.eventType || "");
|
||||
const text = String(entry.text || "");
|
||||
if (type === "player_item_placement" || type.includes("player") || type.includes("tool") || type.includes("robot")) return false;
|
||||
if (/ロボ掃除機|掃除機/.test(text)) return false;
|
||||
if (/\u30ed\u30dc\u6383\u9664\u6a5f|\u6383\u9664\u6a5f/.test(text)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ function isIdleDisplayText(value = "") {
|
|||
|
||||
function reasonLabel(t) {
|
||||
if (!t) return "\u306a\u3057";
|
||||
if (Number(t.electricShockUntil || 0) > Number(t.world?.time || world?.time || 0)) return "感電している。";
|
||||
if (Number(t.electricShockUntil || 0) > Number(t.world?.time || world?.time || 0)) return "\u611f\u96fb\u3057\u3066\u3044\u308b\u3002";
|
||||
const behavior = behaviorText(t);
|
||||
if (behavior) {
|
||||
const normalized = window.TEXT_CATALOG?.cleanBehaviorText?.(behavior, t.state || "") || behavior;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,86 @@
|
|||
"use strict";
|
||||
|
||||
const SCALABLE_TOOLS = new Set(scalableToolIds());
|
||||
const TEXT_ICON_TOOL_IDS = new Set(["poke", "pinch", "water_hose", "rope", "rod", "spring", "wire", "insulated_wire", "pressure_switch", "reciprocator", "copy", "undo", "redo", "area_delete", "clear_tool"]);
|
||||
const PHYSICS_ICON_TOOL_IDS = new Set(window.TarinaiItemToolMetadata?.PHYSICS_TOOL_IDS || []);
|
||||
const TEXT_ICON_TOOL_IDS = new Set(["poke", "pinch", "water_hose", "copy", "undo", "redo", "area_delete", "clear_tool"]);
|
||||
|
||||
|
||||
function drawPhysicsToolIcon(canvas, toolId) {
|
||||
const ctx = canvas?.getContext?.("2d");
|
||||
if (!ctx || !PHYSICS_ICON_TOOL_IDS.has(toolId)) return false;
|
||||
const w = canvas.width || 160;
|
||||
const h = canvas.height || 160;
|
||||
const sx = w / 160;
|
||||
const sy = h / 160;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.save();
|
||||
ctx.scale(sx, sy);
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
const line = (x1, y1, x2, y2, width = 10, stroke = "#4f4a42") => {
|
||||
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineWidth = width; ctx.strokeStyle = stroke; ctx.stroke();
|
||||
};
|
||||
const circle = (x, y, r, fill, stroke = "#4f4a42", width = 6) => {
|
||||
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = fill; ctx.fill(); ctx.lineWidth = width; ctx.strokeStyle = stroke; ctx.stroke();
|
||||
};
|
||||
const rounded = (x, y, width, height, radius, fill, stroke = "#4f4a42", lineWidth = 6) => {
|
||||
const r = Math.max(0, Math.min(radius, width / 2, height / 2));
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
||||
ctx.lineTo(x + width, y + height - r); ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
||||
ctx.lineTo(x + r, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
||||
ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath();
|
||||
ctx.fillStyle = fill; ctx.fill(); ctx.lineWidth = lineWidth; ctx.strokeStyle = stroke; ctx.stroke();
|
||||
};
|
||||
if (toolId === "rope") {
|
||||
ctx.beginPath(); ctx.moveTo(22, 112); ctx.bezierCurveTo(48, 22, 92, 144, 138, 48); ctx.lineWidth = 13; ctx.strokeStyle = "#9a6b3f"; ctx.stroke();
|
||||
circle(22, 112, 10, "#f3d3a6", "#6c4b2e", 6); circle(138, 48, 10, "#f3d3a6", "#6c4b2e", 6);
|
||||
} else if (toolId === "rod") {
|
||||
line(28, 126, 132, 34, 18, "#8a633d"); circle(28, 126, 12, "#d7e0e8"); circle(132, 34, 12, "#d7e0e8");
|
||||
} else if (toolId === "spring") {
|
||||
const pts = [[18,80],[36,80],[48,48],[64,112],[80,48],[96,112],[112,48],[124,80],[142,80]];
|
||||
ctx.beginPath(); ctx.moveTo(...pts[0]); for (const p of pts.slice(1)) ctx.lineTo(...p); ctx.lineWidth = 11; ctx.strokeStyle = "#647487"; ctx.stroke();
|
||||
circle(18,80,8,"#e8eef4","#465564",5); circle(142,80,8,"#e8eef4","#465564",5);
|
||||
} else if (toolId === "wire" || toolId === "insulated_wire") {
|
||||
ctx.beginPath(); ctx.moveTo(18,112); ctx.bezierCurveTo(52,22,104,138,142,52); ctx.lineWidth = toolId === "insulated_wire" ? 16 : 10; ctx.strokeStyle = toolId === "insulated_wire" ? "#3474b8" : "#3e3b39"; ctx.stroke();
|
||||
if (toolId === "insulated_wire") { ctx.lineWidth = 5; ctx.strokeStyle = "#b9ddff"; ctx.stroke(); }
|
||||
else { ctx.beginPath(); ctx.moveTo(83,47); ctx.lineTo(70,78); ctx.lineTo(87,76); ctx.lineTo(75,108); ctx.lineWidth = 8; ctx.strokeStyle = "#f0b62b"; ctx.stroke(); }
|
||||
circle(18,112,8,"#e9eef1"); circle(142,52,8,"#e9eef1");
|
||||
} else if (toolId === "pressure_switch") {
|
||||
rounded(27,91,106,36,12,"#6f7881"); rounded(46,50,68,48,15,"#e7b842"); line(58,112,102,112,5,"#c9d2d8");
|
||||
} else if (toolId === "glass_wall") {
|
||||
rounded(28,35,104,90,8,"rgba(111,205,225,0.34)","#4d8795",8); line(42,105,78,50,7,"#d9f7ff"); line(83,105,116,55,5,"#d9f7ff");
|
||||
} else if (toolId === "bounce_fence") {
|
||||
line(22,104,138,104,15,"#e06788"); line(34,72,126,72,10,"#f3a9bb");
|
||||
for (const x of [48,80,112]) { line(x,126,x,42,7,"#6c5960"); ctx.beginPath(); ctx.moveTo(x-12,57); ctx.lineTo(x,40); ctx.lineTo(x+12,57); ctx.lineWidth=7; ctx.strokeStyle="#e06788"; ctx.stroke(); }
|
||||
} else if (toolId === "gate_fence") {
|
||||
line(28,32,28,132,13,"#665c50"); line(132,32,132,132,13,"#665c50"); line(34,92,74,72,12,"#c18c55"); line(126,92,86,72,12,"#c18c55"); circle(80,75,8,"#f2d197","#6d5842",5);
|
||||
} else if (toolId === "rotator") {
|
||||
circle(80,80,20,"#e4aa48","#5f574d",8);
|
||||
for (const a of [0, Math.PI*2/3, Math.PI*4/3]) { const x=80+Math.cos(a)*52, y=80+Math.sin(a)*52; line(80,80,x,y,15,"#6d7c8c"); circle(x,y,9,"#cfd8df","#53606b",5); }
|
||||
} else if (toolId === "poison_block") {
|
||||
rounded(31,31,98,98,18,"#8f6cad","#4f3d60",8); circle(62,68,10,"#f1e6f7","#4f3d60",4); circle(98,68,10,"#f1e6f7","#4f3d60",4); line(60,105,100,105,8,"#f1e6f7"); line(56,118,104,90,6,"#eadff0"); line(56,90,104,118,6,"#eadff0");
|
||||
} else if (toolId === "reciprocator") {
|
||||
line(22,80,138,80,11,"#657381"); rounded(57,51,46,58,10,"#d68c4f","#5d5046",7);
|
||||
ctx.beginPath(); ctx.moveTo(42,48); ctx.lineTo(24,62); ctx.lineTo(42,76); ctx.moveTo(118,48); ctx.lineTo(136,62); ctx.lineTo(118,76); ctx.lineWidth=8; ctx.strokeStyle="#4f7fa8"; ctx.stroke();
|
||||
} else if (toolId === "one_way_fence") {
|
||||
line(24,112,136,112,14,"#756653"); for (const x of [34,64,94,124]) line(x,45,x,124,7,"#9c8a72");
|
||||
ctx.beginPath(); ctx.moveTo(48,67); ctx.lineTo(104,67); ctx.lineTo(88,49); ctx.moveTo(104,67); ctx.lineTo(88,85); ctx.lineWidth=12; ctx.strokeStyle="#4b9d62"; ctx.stroke();
|
||||
} else if (toolId === "fence_h") {
|
||||
line(24,55,24,130,12,"#75512f"); line(136,55,136,130,12,"#75512f"); line(24,72,136,72,14,"#b9854d"); line(24,108,136,108,14,"#b9854d");
|
||||
} else { ctx.restore(); return false; }
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
function attachPhysicsToolIcon(btn, toolId) {
|
||||
if (!btn || !PHYSICS_ICON_TOOL_IDS.has(toolId)) return false;
|
||||
const canvas = createToolPreviewCanvas("tool-sprite-preview tool-physics-preview", 160);
|
||||
if (!drawPhysicsToolIcon(canvas, toolId)) return false;
|
||||
btn.classList.add("tool-has-canvas-icon", "tool-has-physics-icon");
|
||||
btn.prepend(canvas);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyToolTextIcon(btn, def, toolId) {
|
||||
if (!btn || !def?.iconText || !TEXT_ICON_TOOL_IDS.has(toolId)) return false;
|
||||
|
|
@ -56,8 +135,9 @@ function renderToolPalette() {
|
|||
if (!def || def.simulationOnly || toolId === "observe") continue;
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "tool";
|
||||
const hasTextIcon = applyToolTextIcon(btn, def, toolId);
|
||||
if (!hasTextIcon) applyToolImageIcon(btn, def);
|
||||
const hasPhysicsIcon = category.id === "physics" && attachPhysicsToolIcon(btn, toolId);
|
||||
const hasTextIcon = !hasPhysicsIcon && applyToolTextIcon(btn, def, toolId);
|
||||
if (!hasPhysicsIcon && !hasTextIcon) applyToolImageIcon(btn, def);
|
||||
btn.dataset.tool = toolId;
|
||||
btn.dataset.toolCategory = category.id || "";
|
||||
const label = document.createElement("span");
|
||||
|
|
@ -74,7 +154,7 @@ function renderToolPalette() {
|
|||
btn.setAttribute("aria-keyshortcuts", shortcut);
|
||||
btn.appendChild(badge);
|
||||
}
|
||||
attachToolSpritePreview(btn, def, toolId);
|
||||
if (!hasPhysicsIcon) attachToolSpritePreview(btn, def, toolId);
|
||||
if (toolId === world?.tool) btn.classList.add("selected");
|
||||
body.appendChild(btn);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.16.18";
|
||||
const APP_BUILD = "tarinai-fixes-v39-16-18";
|
||||
const APP_VERSION = "39.16.31";
|
||||
const APP_BUILD = "tarinai-fixes-v39-16-30";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
const x = hasX ? px : rand(58, worldRef.w - 58);
|
||||
const y = hasY ? py : rand(58, worldRef.h - 58);
|
||||
const drop = new Item("water", x, y);
|
||||
drop.rainDrop = opts?.rainDrop === true || (!hasX && !hasY);
|
||||
const amount = Number(opts?.amount);
|
||||
drop.amount = Number.isFinite(amount) ? amount : rand(24, 58);
|
||||
// Rain drops, placed water drops, and water-balloon splashes all use the
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class World { constructor() {
|
|||
this.pointer = { x: 0, y: 0, inside: false, motion: 0 };
|
||||
this.hoverGrabTarget = null;
|
||||
this.hoverDeleteTarget = null;
|
||||
this.hoverGiveTarget = null;
|
||||
this.shootCursorActive = false;
|
||||
this.shootPointerRawX = 0;
|
||||
this.shootPointerRawY = 0;
|
||||
|
|
@ -82,6 +83,7 @@ class World { constructor() {
|
|||
this.drawListDirty = true;
|
||||
this.antUpdatePhase = 0;
|
||||
this.selected = null;
|
||||
this.hoverGiveTarget = null;
|
||||
this.deadCount = 0;
|
||||
this.birthEventCount = 0;
|
||||
this.liveIdNext = 1;
|
||||
|
|
|
|||
|
|
@ -374,7 +374,16 @@
|
|||
let spawned = 0;
|
||||
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 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 spawnX = clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40);
|
||||
const spawnY = clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40);
|
||||
const sand = this.toiletSandAt?.(spawnX, spawnY) || null;
|
||||
if (sand) {
|
||||
sand.toiletFlash = 1;
|
||||
sand.awakeUntil = Math.max(sand.awakeUntil || 0, (this.time || 0) + 0.8);
|
||||
this.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-absorb");
|
||||
continue;
|
||||
}
|
||||
const it = new Item("zunchi", spawnX, spawnY);
|
||||
it.amount = 240;
|
||||
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);
|
||||
|
|
@ -389,8 +398,28 @@
|
|||
return spawned;
|
||||
},
|
||||
|
||||
toiletSandAt(x, y) {
|
||||
const candidates = typeof this.itemsOfType === "function"
|
||||
? this.itemsOfType("toilet")
|
||||
: (this.nearbyItems?.(x, y, 96) || []);
|
||||
for (const item of candidates || []) {
|
||||
if (!item || item.dead || item.type !== "toilet") continue;
|
||||
if (globalThis.TarinaiToiletSandSystem?.contains?.(item, x, y)) return item;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
spawnZunchi(x, y, source = null) {
|
||||
const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40));
|
||||
const spawnX = clamp(x, 40, this.w - 40);
|
||||
const spawnY = clamp(y, 40, this.h - 40);
|
||||
const sand = this.toiletSandAt?.(spawnX, spawnY) || null;
|
||||
if (sand) {
|
||||
sand.toiletFlash = 1;
|
||||
sand.awakeUntil = Math.max(sand.awakeUntil || 0, (this.time || 0) + 0.8);
|
||||
this.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-absorb");
|
||||
return null;
|
||||
}
|
||||
const it = new Item("zunchi", spawnX, spawnY);
|
||||
it.amount = 260;
|
||||
it.stage = "fresh";
|
||||
it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0);
|
||||
|
|
@ -661,7 +690,7 @@
|
|||
return dist(a, ball) + ap - (dist(b, ball) + bp);
|
||||
});
|
||||
for (const t of arr.slice(5)) {
|
||||
t.goIdle((ball.type === "balloon" ? "水風船" : "ボール") + "が混み合っているので眺めている");
|
||||
t.goIdle((ball.type === "balloon" ? "\u6c34\u98a8\u8239" : "\u30dc\u30fc\u30eb") + "\u304c\u6df7\u307f\u5408\u3063\u3066\u3044\u308b\u306e\u3067\u773a\u3081\u3066\u3044\u308b");
|
||||
t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + deterministicRange(this, "ball-chaser-release-angle", -0.45, 0.45, t, ball);
|
||||
}
|
||||
}
|
||||
|
|
@ -694,7 +723,7 @@
|
|||
ball.amount = Math.max(ball.amount || 999, 999);
|
||||
audio.ballHit?.();
|
||||
this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: isBalloon ? "rgba(94,188,236,0.56)" : "rgba(255,255,255,0.58)" }));
|
||||
if (!isBalloon) this.log("ボールをつついて転がした。", "observe", { eventType: "ball_poke" });
|
||||
if (!isBalloon) this.log("\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066\u8ee2\u304c\u3057\u305f\u3002", "observe", { eventType: "ball_poke" });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
const Geometry = global.TarinaiGeometry;
|
||||
const BOUNCE_FENCE_RESTITUTION = 4.20;
|
||||
const ORDINARY_FENCE_RESTITUTION = 0.38;
|
||||
const BOUNCE_FENCE_MIN_SPEED = 820;
|
||||
const BOUNCE_FENCE_MAX_SPEED = 1680;
|
||||
|
||||
|
|
@ -330,7 +331,8 @@
|
|||
cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
|
||||
vertical: Math.abs(Math.sin(angle)) > Math.abs(Math.cos(angle)), horizontal: Math.abs(Math.cos(angle)) >= Math.abs(Math.sin(angle)),
|
||||
bounce, gate, glassWall, oneWay, oneWayNx: Math.sin(angle), oneWayNy: -Math.cos(angle), gateOpen: Boolean(it.gateOpen),
|
||||
restitution: bounce ? BOUNCE_FENCE_RESTITUTION : undefined, minBounceSpeed: bounce ? BOUNCE_FENCE_MIN_SPEED : undefined
|
||||
restitution: bounce ? BOUNCE_FENCE_RESTITUTION : ORDINARY_FENCE_RESTITUTION,
|
||||
minBounceSpeed: bounce ? BOUNCE_FENCE_MIN_SPEED : undefined
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -755,7 +757,7 @@
|
|||
t.lastBounceFenceImpulseAt = now;
|
||||
t._sleepExternalMotionUntil = Math.max(Number(t._sleepExternalMotionUntil || 0) || 0, now + 1.15);
|
||||
global.TarinaiMovementUpdateStep?.markSleepExternalMotion?.(t, 0.016, "bounce-fence-launch");
|
||||
if (t.sleeping || t.state === "sleep") global.TarinaiMovementUpdateStep?.wakeSleepingFromExternalMotion?.(t, "バウンス柵で跳ね飛ばされた");
|
||||
if (t.sleeping || t.state === "sleep") global.TarinaiMovementUpdateStep?.wakeSleepingFromExternalMotion?.(t, "\u30d0\u30a6\u30f3\u30b9\u67f5\u3067\u8df3\u306d\u98db\u3070\u3055\u308c\u305f");
|
||||
t.fallTimer = Math.max(Number(t.fallTimer || 0) || 0, 0.62);
|
||||
t.fallMax = Math.max(Number(t.fallMax || 0) || 0, Number(t.fallTimer || 0) || 0);
|
||||
// Clear the body beyond the contact skin so the multi-pass collision
|
||||
|
|
@ -800,7 +802,7 @@
|
|||
const moveX = Number(dx || 0) || 0;
|
||||
const moveY = Number(dy || 0) || 0;
|
||||
const dist = Math.hypot(moveX, moveY);
|
||||
if (dist <= 0.0001) return this.resolveSolidObstacleCollision(t, { maxPasses: 2, reason: opts.reason || "tarinai-stationary" });
|
||||
if (dist <= 0.0001) return this.resolveSolidObstacleCollision(t, { maxPasses: 2 });
|
||||
if (this.isTarinaiHiddenInNestBox?.(t)) {
|
||||
t.x += moveX;
|
||||
t.y += moveY;
|
||||
|
|
@ -842,7 +844,6 @@
|
|||
slop: 0.30,
|
||||
oneWayDx: stepX,
|
||||
oneWayDy: stepY,
|
||||
reason: opts.reason || "tarinai-swept-hit",
|
||||
}) || true;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -856,7 +857,6 @@
|
|||
slop: 0.35,
|
||||
oneWayDx: stepX,
|
||||
oneWayDy: stepY,
|
||||
reason: opts.reason || "tarinai-swept-move",
|
||||
});
|
||||
collided = passHit || collided;
|
||||
}
|
||||
|
|
@ -901,7 +901,7 @@
|
|||
},
|
||||
|
||||
resolveFenceCollision(t) {
|
||||
return this.resolveSolidObstacleCollision(t, { maxPasses: 2, reason: "resolve-fence" });
|
||||
return this.resolveSolidObstacleCollision(t, { maxPasses: 2 });
|
||||
},
|
||||
|
||||
segmentIntersectsRect(x1, y1, x2, y2, r) {
|
||||
|
|
|
|||
|
|
@ -112,14 +112,14 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
|
|||
if (d <= limit && d < bestD) { best = t; bestD = d; }
|
||||
}
|
||||
if (!best) return null;
|
||||
const nameLine = String(best.name || "たりない");
|
||||
const nameLine = String(best.name || "\u305f\u308a\u306a\u3044");
|
||||
const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "")
|
||||
|| window.TEXT_CATALOG?.stateLabel?.(best.state, best)
|
||||
|| best.thought
|
||||
|| "様子を見ている";
|
||||
|| "\u69d8\u5b50\u3092\u898b\u3066\u3044\u308b";
|
||||
const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(best) : (Number(best.maxEnergy) || 100);
|
||||
const statusLine = `体力: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)} 空腹: ${Math.round(best.hunger || 0)} ストレス: ${Math.round(best.stress || 0)}`;
|
||||
const tempLine = worldRef.temperatureTooltipLineFor(best, "体感気温") || "体感気温: --℃";
|
||||
const statusLine = `\u4f53\u529b: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)}\u3000\u7a7a\u8179: ${Math.round(best.hunger || 0)}\u3000\u30b9\u30c8\u30ec\u30b9: ${Math.round(best.stress || 0)}`;
|
||||
const tempLine = worldRef.temperatureTooltipLineFor(best, "\u4f53\u611f\u6c17\u6e29") || "\u4f53\u611f\u6c17\u6e29: --\u2103";
|
||||
return { target: best, lines: [nameLine, String(behavior), statusLine, tempLine], kind: "tarinai" };
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
|
|||
function ownedBedTooltipLinesLocal(worldRef, bed) {
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") return [];
|
||||
const owner = worldRef.liveTarinaiById?.(bed.ownerId) || null;
|
||||
return ["かんたんベッド", `持ち主: ${owner?.name || "不明"}`, worldRef.temperatureTooltipLineFor?.(bed, "周辺気温")].filter(Boolean);
|
||||
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, worldRef.temperatureTooltipLineFor?.(bed, "\u5468\u8fba\u6c17\u6e29")].filter(Boolean);
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
|
|
@ -620,14 +620,14 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
|
|||
occupants = this.nestBoxOccupants(box, Infinity);
|
||||
}
|
||||
const names = occupants.map(t => t?.name).filter(Boolean).slice(0, isPipe ? 8 : 5);
|
||||
const more = occupants.length > names.length ? `、ほか${occupants.length - names.length}匹` : "";
|
||||
const label = isPipe ? "土管" : "巣箱";
|
||||
const insideLabel = isPipe ? "土管に入ってる子" : "入ってる子";
|
||||
return [label, names.length ? `${insideLabel}: ${names.join("、")}${more}` : `${insideLabel}: なし`, this.temperatureTooltipLineFor?.(box, "内部気温")].filter(Boolean);
|
||||
const more = occupants.length > names.length ? `\u3001\u307b\u304b${occupants.length - names.length}\u5339` : "";
|
||||
const label = isPipe ? "\u571f\u7ba1" : "\u5de3\u7bb1";
|
||||
const insideLabel = isPipe ? "\u571f\u7ba1\u306b\u5165\u3063\u3066\u308b\u5b50" : "\u5165\u3063\u3066\u308b\u5b50";
|
||||
return [label, names.length ? `${insideLabel}: ${names.join("\u3001")}${more}` : `${insideLabel}: \u306a\u3057`, this.temperatureTooltipLineFor?.(box, "\u5185\u90e8\u6c17\u6e29")].filter(Boolean);
|
||||
},
|
||||
|
||||
|
||||
temperatureTooltipLineFor(target, label = "体感気温") {
|
||||
temperatureTooltipLineFor(target, label = "\u4f53\u611f\u6c17\u6e29") {
|
||||
if (!target || !this.temperatureAt) return "";
|
||||
const isActor = Array.isArray(this.tarinai) && this.tarinai.includes(target);
|
||||
const temp = isActor
|
||||
|
|
@ -637,8 +637,8 @@ function inheritedChampionPersonality(seed = "", parents = [], opts = {}) {
|
|||
: this.temperatureAt(target.x, target.y));
|
||||
if (!Number.isFinite(Number(temp))) return "";
|
||||
const status = this.temperatureStatusFor?.(temp, isActor ? target : null) || null;
|
||||
const suffix = status?.label ? `(${status.label})` : "";
|
||||
return `${label}: ${Number(temp).toFixed(1)}℃${suffix}`;
|
||||
const suffix = status?.label ? `\uff08${status.label}\uff09` : "";
|
||||
return `${label}: ${Number(temp).toFixed(1)}\u2103${suffix}`;
|
||||
},
|
||||
|
||||
pointerItemTooltipInfo() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
const SIGNBOARD_CHARS_PER_LINE = 6;
|
||||
const SIGNBOARD_LINES = 3;
|
||||
const SIGNBOARD_TEXT_LIMIT = SIGNBOARD_CHARS_PER_LINE * SIGNBOARD_LINES;
|
||||
const CONNECTION_ITEM_TYPES = new Set(["rope", "rod", "spring", "wire", "insulated_wire"]);
|
||||
|
||||
function ensureEditorDialog(id, className, html) {
|
||||
let dialog = document.getElementById(id);
|
||||
|
|
@ -222,21 +223,21 @@
|
|||
return ensureEditorDialog("fanEditor", "rotator-editor fan-editor hidden", `
|
||||
<div class="rotator-editor-panel fan-editor-panel" role="dialog" aria-modal="true" aria-labelledby="fanEditorTitle">
|
||||
<div class="rotator-editor-titlebar">
|
||||
<h2 id="fanEditorTitle">扇風機</h2>
|
||||
<button id="fanEditorClose" class="rotator-editor-close" type="button" aria-label="閉じる">×</button>
|
||||
<h2 id="fanEditorTitle">\u6247\u98a8\u6a5f</h2>
|
||||
<button id="fanEditorClose" class="rotator-editor-close" type="button" aria-label="\u9589\u3058\u308b">\u00d7</button>
|
||||
</div>
|
||||
<div class="fan-editor-help">クリックした扇風機の風向きと首振りを設定します。保存しても本体の見た目の向きは変えず、風向き矢印だけが変わります。</div>
|
||||
<div class="fan-editor-help">\u30af\u30ea\u30c3\u30af\u3057\u305f\u6247\u98a8\u6a5f\u306e\u98a8\u5411\u304d\u3068\u9996\u632f\u308a\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u4fdd\u5b58\u3057\u3066\u3082\u672c\u4f53\u306e\u898b\u305f\u76ee\u306e\u5411\u304d\u306f\u5909\u3048\u305a\u3001\u98a8\u5411\u304d\u77e2\u5370\u3060\u3051\u304c\u5909\u308f\u308a\u307e\u3059\u3002</div>
|
||||
<div class="rotator-editor-controls fan-editor-controls">
|
||||
<label><span>向き</span><input id="fanCenterAngle" type="range" min="0" max="355" step="5"><input id="fanCenterAngleNum" type="number" min="0" max="355" step="5"><span>度</span></label>
|
||||
<label><span>振り幅</span><input id="fanSwingRange" type="range" min="0" max="120" step="5"><input id="fanSwingRangeNum" type="number" min="0" max="150" step="5"><span>度</span></label>
|
||||
<label><span>速度</span><input id="fanSwingSpeed" type="range" min="0" max="180" step="5"><input id="fanSwingSpeedNum" type="number" min="0" max="360" step="5"><span>度/秒</span></label>
|
||||
<label class="fan-editor-check"><input id="fanSwingEnabled" type="checkbox"> 首振りON</label>
|
||||
<label><span>\u5411\u304d</span><input id="fanCenterAngle" type="range" min="0" max="355" step="5"><input id="fanCenterAngleNum" type="number" min="0" max="355" step="5"><span>\u5ea6</span></label>
|
||||
<label><span>\u632f\u308a\u5e45</span><input id="fanSwingRange" type="range" min="0" max="120" step="5"><input id="fanSwingRangeNum" type="number" min="0" max="150" step="5"><span>\u5ea6</span></label>
|
||||
<label><span>\u901f\u5ea6</span><input id="fanSwingSpeed" type="range" min="0" max="180" step="5"><input id="fanSwingSpeedNum" type="number" min="0" max="360" step="5"><span>\u5ea6/\u79d2</span></label>
|
||||
<label class="fan-editor-check"><input id="fanSwingEnabled" type="checkbox"> \u9996\u632f\u308aON</label>
|
||||
</div>
|
||||
<p class="rotator-editor-hint">振り幅0度なら固定向きです。配置済み扇風機の本体角度は固定したまま、風向きだけを調整します。</p>
|
||||
<p class="rotator-editor-hint">\u632f\u308a\u5e450\u5ea6\u306a\u3089\u56fa\u5b9a\u5411\u304d\u3067\u3059\u3002\u914d\u7f6e\u6e08\u307f\u6247\u98a8\u6a5f\u306e\u672c\u4f53\u89d2\u5ea6\u306f\u56fa\u5b9a\u3057\u305f\u307e\u307e\u3001\u98a8\u5411\u304d\u3060\u3051\u3092\u8abf\u6574\u3057\u307e\u3059\u3002</p>
|
||||
<div class="rotator-editor-actions">
|
||||
<button id="fanEditorStop" class="btn" type="button">固定にする</button>
|
||||
<button id="fanEditorCancel" class="btn" type="button">キャンセル</button>
|
||||
<button id="fanEditorApply" class="btn primary" type="button">保存</button>
|
||||
<button id="fanEditorStop" class="btn" type="button">\u56fa\u5b9a\u306b\u3059\u308b</button>
|
||||
<button id="fanEditorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button>
|
||||
<button id="fanEditorApply" class="btn primary" type="button">\u4fdd\u5b58</button>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
|
@ -301,8 +302,8 @@
|
|||
item.angle = item.fanSwingCenter;
|
||||
worldRef?.markSpatialDirty?.("fan-swing-settings");
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
worldRef?.log?.(item.fanSwingOn ? "扇風機の首振り設定を変更した。" : "扇風機を固定向きにした。", "observe");
|
||||
if (typeof showToast === "function") showToast(item.fanSwingOn ? "扇風機: 首振りON" : "扇風機: 固定向き");
|
||||
worldRef?.log?.(item.fanSwingOn ? "\u6247\u98a8\u6a5f\u306e\u9996\u632f\u308a\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u305f\u3002" : "\u6247\u98a8\u6a5f\u3092\u56fa\u5b9a\u5411\u304d\u306b\u3057\u305f\u3002", "observe");
|
||||
if (typeof showToast === "function") showToast(item.fanSwingOn ? "\u6247\u98a8\u6a5f: \u9996\u632f\u308aON" : "\u6247\u98a8\u6a5f: \u56fa\u5b9a\u5411\u304d");
|
||||
global.render?.();
|
||||
close();
|
||||
};
|
||||
|
|
@ -412,7 +413,7 @@
|
|||
if (speedNum) speedNum.value = String(deg);
|
||||
if (thick) thick.value = String(initialThickness);
|
||||
if (thickNum) thickNum.value = String(initialThickness);
|
||||
if (poweredInput) { poweredInput.checked = phyScalar(item, "motorOn", true) !== false; poweredInput.disabled = Boolean(item._signalDriven); poweredInput.title = item._signalDriven ? "電線信号で制御中" : ""; }
|
||||
if (poweredInput) { poweredInput.checked = phyScalar(item, "motorOn", true) !== false; poweredInput.disabled = Boolean(item._signalDriven); poweredInput.title = item._signalDriven ? "\u96fb\u7dda\u4fe1\u53f7\u3067\u5236\u5fa1\u4e2d" : ""; }
|
||||
editor.syncInputPair(speed, speedNum, controller.draw);
|
||||
editor.syncInputPair(thick, thickNum, controller.draw);
|
||||
const close = () => {
|
||||
|
|
@ -550,7 +551,7 @@
|
|||
if (travelNum) travelNum.value = String(travelVal);
|
||||
if (thick) thick.value = String(initialThickness);
|
||||
if (thickNum) thickNum.value = String(initialThickness);
|
||||
if (powered) { powered.checked = phyScalar(item, "railOn", true) !== false; powered.disabled = Boolean(item._signalDriven); powered.title = item._signalDriven ? "電線信号で制御中" : ""; }
|
||||
if (powered) { powered.checked = phyScalar(item, "railOn", true) !== false; powered.disabled = Boolean(item._signalDriven); powered.title = item._signalDriven ? "\u96fb\u7dda\u4fe1\u53f7\u3067\u5236\u5fa1\u4e2d" : ""; }
|
||||
if (solidInput) solidInput.checked = phyScalar(item, "solid", true) !== false;
|
||||
editor.syncInputPair(angle, angleNum);
|
||||
editor.syncInputPair(speed, speedNum);
|
||||
|
|
@ -660,6 +661,14 @@
|
|||
if (item.type === "gate_fence") data.gateOpen = Boolean(item.gateOpen);
|
||||
if (item.type === "duplicator") { data.storedFoodType = item.storedFoodType || ""; data.storedFoodLabel = item.storedFoodLabel || ""; }
|
||||
if (item.type === "robot_cleaner") { data.robotCleanerMask = Number(item.robotCleanerMask ?? 3) | 0; data.robotCleanerHighSpeed = Boolean(item.robotCleanerHighSpeed); }
|
||||
if (item.type === "pressure_switch") {
|
||||
data.pressureTarget = item.pressureTarget === "time" ? "time" : (item.pressureTarget === "item" ? "item" : "tarinai");
|
||||
data.pressureThreshold = Math.max(1, Math.min(999, Number(item.pressureThreshold || 1) | 0));
|
||||
data.pressureWidth = Math.max(60, Math.min(840, Number(item.pressureWidth || 220) | 0));
|
||||
data.pressureHeight = Math.max(60, Math.min(840, Number(item.pressureHeight || 160) | 0));
|
||||
data.pressureTimeStart = Math.max(0, Math.min(1435, Number(item.pressureTimeStart ?? 360) | 0));
|
||||
data.pressureTimeEnd = Math.max(0, Math.min(1435, Number(item.pressureTimeEnd ?? 1080) | 0));
|
||||
}
|
||||
if (item.type === "fan") {
|
||||
data.fanSwingOn = Boolean(item.fanSwingOn);
|
||||
data.fanSwingCenter = Number(item.fanSwingCenter ?? item.angle ?? 0) || 0;
|
||||
|
|
@ -720,6 +729,15 @@
|
|||
item.storedFoodLabel = String(data.storedFoodLabel || (item.storedFoodType ? toolLabel(item.storedFoodType) : ""));
|
||||
if (item.roles) item.roles.food = Boolean(item.storedFoodType);
|
||||
}
|
||||
if (item.type === "pressure_switch") {
|
||||
item.pressureTarget = data.pressureTarget === "time" ? "time" : (data.pressureTarget === "item" ? "item" : "tarinai");
|
||||
item.pressureThreshold = Math.max(1, Math.min(999, Number(data.pressureThreshold || 1) | 0));
|
||||
item.pressureWidth = Math.max(60, Math.min(840, Number(data.pressureWidth || 220) | 0));
|
||||
item.pressureHeight = Math.max(60, Math.min(840, Number(data.pressureHeight || 160) | 0));
|
||||
item.pressureTimeStart = Math.max(0, Math.min(1435, Number(data.pressureTimeStart ?? 360) | 0));
|
||||
item.pressureTimeEnd = Math.max(0, Math.min(1435, Number(data.pressureTimeEnd ?? 1080) | 0));
|
||||
item.signalActive = false;
|
||||
}
|
||||
if (item.type === "robot_cleaner") {
|
||||
const mask = Number(data.robotCleanerMask);
|
||||
item.robotCleanerMask = Number.isFinite(mask) ? Math.max(0, Math.min(0x3f, mask | 0)) : (global.TarinaiItemDynamicToolSystem?.robotCleanerDefaultMask?.() ?? 3);
|
||||
|
|
@ -794,14 +812,22 @@
|
|||
function ensureSignalItemEditor() {
|
||||
return ensureEditorDialog("signalItemEditor", "rotator-editor hidden", `
|
||||
<div class="rotator-editor-panel" role="dialog" aria-modal="true" aria-labelledby="signalItemEditorTitle">
|
||||
<div class="rotator-editor-titlebar"><h2 id="signalItemEditorTitle">検知器設定</h2><button id="signalItemEditorClose" class="rotator-editor-close" type="button">×</button></div>
|
||||
<div class="rotator-editor-titlebar"><h2 id="signalItemEditorTitle">\u691c\u77e5\u5668\u8a2d\u5b9a</h2><button id="signalItemEditorClose" class="rotator-editor-close" type="button">\u00d7</button></div>
|
||||
<div class="rotator-editor-controls">
|
||||
<label>検知対象 <select id="pressureSwitchTarget"><option value="tarinai">たりない</option><option value="item">アイテム</option></select></label>
|
||||
<label>起動個数 <input id="pressureSwitchCount" type="number" min="1" max="999" step="1"></label>
|
||||
<label>横幅 <input id="pressureSwitchWidth" type="range" min="60" max="840" step="20"><input id="pressureSwitchWidthNum" type="number" min="60" max="840" step="20"> px</label>
|
||||
<label>縦幅 <input id="pressureSwitchHeight" type="range" min="60" max="840" step="20"><input id="pressureSwitchHeightNum" type="number" min="60" max="840" step="20"> px</label>
|
||||
<label>\u691c\u77e5\u5bfe\u8c61 <select id="pressureSwitchTarget"><option value="tarinai">\u305f\u308a\u306a\u3044</option><option value="item">\u30a2\u30a4\u30c6\u30e0</option><option value="time">\u6642\u523b</option></select></label>
|
||||
<label id="pressureSwitchCountRow" class="pressure-spatial-control">\u8d77\u52d5\u500b\u6570 <input id="pressureSwitchCount" type="number" min="1" max="999" step="1"></label>
|
||||
<label id="pressureSwitchWidthRow" class="pressure-spatial-control">\u6a2a\u5e45 <input id="pressureSwitchWidth" type="range" min="60" max="840" step="20"><input id="pressureSwitchWidthNum" type="number" min="60" max="840" step="20"> px</label>
|
||||
<label id="pressureSwitchHeightRow" class="pressure-spatial-control">\u7e26\u5e45 <input id="pressureSwitchHeight" type="range" min="60" max="840" step="20"><input id="pressureSwitchHeightNum" type="number" min="60" max="840" step="20"> px</label>
|
||||
<div id="pressureSwitchTimeRow" class="pressure-time-control" hidden>
|
||||
<div class="pressure-time-summary"><span>\u4f5c\u52d5\u6642\u9593\u5e2f</span><span><output id="pressureSwitchTimeStartLabel">06:00</output><span aria-hidden="true"> \u301c </span><output id="pressureSwitchTimeEndLabel">18:00</output></span></div>
|
||||
<div id="pressureSwitchTimeRange" class="pressure-time-range">
|
||||
<div id="pressureSwitchTimeTrack" class="pressure-time-track" aria-hidden="true"></div>
|
||||
<input id="pressureSwitchTimeStart" class="pressure-time-thumb pressure-time-thumb-start" type="range" min="0" max="1435" step="5" aria-label="\u4f5c\u52d5\u958b\u59cb\u6642\u523b">
|
||||
<input id="pressureSwitchTimeEnd" class="pressure-time-thumb pressure-time-thumb-end" type="range" min="0" max="1435" step="5" aria-label="\u4f5c\u52d5\u7d42\u4e86\u6642\u523b">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rotator-editor-actions"><button id="signalItemEditorCancel" class="btn" type="button">キャンセル</button><button id="signalItemEditorApply" class="btn primary" type="button">保存</button></div>
|
||||
<div class="rotator-editor-actions"><button id="signalItemEditorCancel" class="btn" type="button">\u30ad\u30e3\u30f3\u30bb\u30eb</button><button id="signalItemEditorApply" class="btn primary" type="button">\u4fdd\u5b58</button></div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
|
|
@ -814,21 +840,74 @@
|
|||
const widthNum = dialog.querySelector("#pressureSwitchWidthNum");
|
||||
const height = dialog.querySelector("#pressureSwitchHeight");
|
||||
const heightNum = dialog.querySelector("#pressureSwitchHeightNum");
|
||||
target.value = item.pressureTarget === "item" ? "item" : "tarinai";
|
||||
const countRow = dialog.querySelector("#pressureSwitchCountRow");
|
||||
const widthRow = dialog.querySelector("#pressureSwitchWidthRow");
|
||||
const heightRow = dialog.querySelector("#pressureSwitchHeightRow");
|
||||
const timeRow = dialog.querySelector("#pressureSwitchTimeRow");
|
||||
const timeRange = dialog.querySelector("#pressureSwitchTimeRange");
|
||||
const timeTrack = dialog.querySelector("#pressureSwitchTimeTrack");
|
||||
const timeStart = dialog.querySelector("#pressureSwitchTimeStart");
|
||||
const timeEnd = dialog.querySelector("#pressureSwitchTimeEnd");
|
||||
const timeStartLabel = dialog.querySelector("#pressureSwitchTimeStartLabel");
|
||||
const timeEndLabel = dialog.querySelector("#pressureSwitchTimeEndLabel");
|
||||
const normalizeMinute = (value, fallback) => {
|
||||
const n = Number(value);
|
||||
const minute = Number.isFinite(n) ? Math.round(n / 5) * 5 : fallback;
|
||||
return Math.max(0, Math.min(1435, minute));
|
||||
};
|
||||
const formatMinute = value => {
|
||||
const minute = normalizeMinute(value, 0);
|
||||
return `${String(Math.floor(minute / 60)).padStart(2, "0")}:${String(minute % 60).padStart(2, "0")}`;
|
||||
};
|
||||
target.value = item.pressureTarget === "time" ? "time" : (item.pressureTarget === "item" ? "item" : "tarinai");
|
||||
count.value = String(item.pressureThreshold || 1);
|
||||
width.value = widthNum.value = String(item.pressureWidth || 220);
|
||||
height.value = heightNum.value = String(item.pressureHeight || 160);
|
||||
timeStart.value = String(normalizeMinute(item.pressureTimeStart, 360));
|
||||
timeEnd.value = String(normalizeMinute(item.pressureTimeEnd, 1080));
|
||||
const updateTimeRange = () => {
|
||||
const start = normalizeMinute(timeStart.value, 360);
|
||||
const end = normalizeMinute(timeEnd.value, 1080);
|
||||
timeStart.value = String(start);
|
||||
timeEnd.value = String(end);
|
||||
timeStartLabel.textContent = formatMinute(start);
|
||||
timeEndLabel.textContent = formatMinute(end);
|
||||
const startPct = start / 1435 * 100;
|
||||
const endPct = end / 1435 * 100;
|
||||
const idle = "rgba(94,112,128,0.22)";
|
||||
const active = "rgba(67,158,218,0.78)";
|
||||
timeTrack.style.background = start <= end
|
||||
? `linear-gradient(to right, ${idle} 0%, ${idle} ${startPct}%, ${active} ${startPct}%, ${active} ${endPct}%, ${idle} ${endPct}%, ${idle} 100%)`
|
||||
: `linear-gradient(to right, ${active} 0%, ${active} ${endPct}%, ${idle} ${endPct}%, ${idle} ${startPct}%, ${active} ${startPct}%, ${active} 100%)`;
|
||||
if (timeRange) timeRange.dataset.wraps = start > end ? "1" : "0";
|
||||
};
|
||||
const updateOverlay = () => {
|
||||
worldRef._pressureSwitchOverlay = {
|
||||
item,
|
||||
width: Math.max(60, Math.min(840, Number(widthNum.value || width.value || 220) | 0)),
|
||||
height: Math.max(60, Math.min(840, Number(heightNum.value || height.value || 160) | 0)),
|
||||
};
|
||||
if (target.value === "time") {
|
||||
if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null;
|
||||
} else {
|
||||
worldRef._pressureSwitchOverlay = {
|
||||
item,
|
||||
width: Math.max(60, Math.min(840, Number(widthNum.value || width.value || 220) | 0)),
|
||||
height: Math.max(60, Math.min(840, Number(heightNum.value || height.value || 160) | 0)),
|
||||
};
|
||||
}
|
||||
global.render?.();
|
||||
};
|
||||
const updateMode = () => {
|
||||
const timeMode = target.value === "time";
|
||||
countRow.hidden = timeMode;
|
||||
widthRow.hidden = timeMode;
|
||||
heightRow.hidden = timeMode;
|
||||
timeRow.hidden = !timeMode;
|
||||
updateTimeRange();
|
||||
updateOverlay();
|
||||
};
|
||||
syncEditorInputPair(width, widthNum, updateOverlay);
|
||||
syncEditorInputPair(height, heightNum, updateOverlay);
|
||||
updateOverlay();
|
||||
timeStart.oninput = updateTimeRange;
|
||||
timeEnd.oninput = updateTimeRange;
|
||||
target.onchange = updateMode;
|
||||
updateMode();
|
||||
const close = () => {
|
||||
dialog.classList.add("hidden");
|
||||
if (worldRef._pressureSwitchOverlay?.item === item) worldRef._pressureSwitchOverlay = null;
|
||||
|
|
@ -836,10 +915,12 @@
|
|||
};
|
||||
dialog.querySelector("#signalItemEditorApply").onclick = () => {
|
||||
global.TarinaiHistory.capture(worldRef, "detector-edit");
|
||||
item.pressureTarget = target.value === "item" ? "item" : "tarinai";
|
||||
item.pressureTarget = target.value === "time" ? "time" : (target.value === "item" ? "item" : "tarinai");
|
||||
item.pressureThreshold = Math.max(1, Math.min(999, Number(count.value || 1) | 0));
|
||||
item.pressureWidth = Math.max(60, Math.min(840, Number(widthNum.value || width.value || 220) | 0));
|
||||
item.pressureHeight = Math.max(60, Math.min(840, Number(heightNum.value || height.value || 160) | 0));
|
||||
item.pressureTimeStart = normalizeMinute(timeStart.value, 360);
|
||||
item.pressureTimeEnd = normalizeMinute(timeEnd.value, 1080);
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.markSpatialDirty?.("detector-edit");
|
||||
global.render?.();
|
||||
|
|
@ -1030,19 +1111,6 @@
|
|||
});
|
||||
|
||||
|
||||
function ballPlacementBlocked(worldRef, item) {
|
||||
if (!worldRef || !item || item.type !== "ball") return false;
|
||||
const radiusFor = typeof itemRadiusFor === "function" ? itemRadiusFor : ((type, fallback = 12) => fallback);
|
||||
const r = Math.max(8, Number(item.r || item.radius || radiusFor(item.type, 18) || 18) || 18);
|
||||
const search = Math.max(72, r * 3.2);
|
||||
for (const other of worldRef.nearbyItems?.(item.x, item.y, search, true) || worldRef.items || []) {
|
||||
if (!other || other === item || other.dead || other.type !== "ball") continue;
|
||||
const or = Math.max(8, Number(other.r || other.radius || radiusFor(other.type, 18) || 18) || 18);
|
||||
if (distXY(item.x, item.y, other.x, other.y) < r + or - 1.0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function clearGrassBedsForPlacement(worldRef, item) {
|
||||
return clearSoftItemsUnderPlacementItem(worldRef, item, "placement-soft-overlap-clear");
|
||||
}
|
||||
|
|
@ -1135,7 +1203,6 @@
|
|||
}
|
||||
}
|
||||
if (item.type === "grass" && (this.grassBlockedAt?.(item.x, item.y, item, { ignoreSoftPlacement: true }) || this.grassOnTarinaiAt?.(item.x, item.y))) return true;
|
||||
if (ballPlacementBlocked(this, item)) return true;
|
||||
if (this.isFenceType(item.type) && this.fencePlacementBlocked(item)) return true;
|
||||
if (this.importantPlacementOverlapBlocked(item)) return true;
|
||||
if (item.type === "nest_box") {
|
||||
|
|
@ -1229,7 +1296,7 @@
|
|||
let foundItem = null, foundItemD = Infinity;
|
||||
for (let i = this.items.length - 1; i >= 0; i--) {
|
||||
const it = this.items[i];
|
||||
if (!it || it.dead || it.type === "trace" || it.type === "splat" || it.type === "plushie") continue;
|
||||
if (!it || it.dead || it.type === "trace" || it.type === "splat" || it.type === "plushie" || CONNECTION_ITEM_TYPES.has(it.type)) continue;
|
||||
let hit = false;
|
||||
let d = distXY(x, y, it.x, it.y);
|
||||
if (global.TarinaiMechanicalSystem.isMechanicalType(it.type)) {
|
||||
|
|
@ -1261,8 +1328,18 @@
|
|||
|
||||
placeItem(item, dropped = true) {
|
||||
if (!item) return null;
|
||||
const directFeeding = global.TarinaiDirectFeedingSystem;
|
||||
if (directFeeding?.isDirectFeedType?.(item)) {
|
||||
const directTarget = directFeeding.findTargetAt?.(this, item.x, item.y) || null;
|
||||
const given = directTarget ? directFeeding.give?.(this, directTarget, item, { source: "placement" }) : null;
|
||||
if (given) {
|
||||
this.drawListDirty = true;
|
||||
if (typeof global.render === "function") global.render();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
if (!(this.canAddObjects?.(1) ?? true)) {
|
||||
showToast(`オブジェクト総数は${this.objectLimit}個までです。`);
|
||||
showToast(`\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u7dcf\u6570\u306f${this.objectLimit}\u500b\u307e\u3067\u3067\u3059\u3002`);
|
||||
return null;
|
||||
}
|
||||
if (item.type === "grass" && !this.canAddGrass?.(1)) {
|
||||
|
|
@ -1286,7 +1363,7 @@
|
|||
if (removedSoft) this.updateItemCounts?.("placement-soft-overlap-clear");
|
||||
const placed = this.addItem?.(item, `place:${item.type}`);
|
||||
if (!placed) {
|
||||
if (!(this.canAddObjects?.(1) ?? true)) showToast(`オブジェクト総数は${this.objectLimit}個までです。`);
|
||||
if (!(this.canAddObjects?.(1) ?? true)) showToast(`\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u7dcf\u6570\u306f${this.objectLimit}\u500b\u307e\u3067\u3067\u3059\u3002`);
|
||||
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;
|
||||
}
|
||||
|
|
@ -1424,7 +1501,7 @@
|
|||
if (tool === "new") {
|
||||
const spot = this.edgeSpawnPoint(x, y, 42, true);
|
||||
const t = this.addTarinai({ x: spot.x, y: spot.y, vx: spot.vx, vy: spot.vy, generation: 1, entryTimer: 2.0 });
|
||||
if (!t) { showToast(`たりない個体数は${this.tarinaiPopulationLimit}匹までです。`); return; }
|
||||
if (!t) { showToast(`\u305f\u308a\u306a\u3044\u500b\u4f53\u6570\u306f${this.tarinaiPopulationLimit}\u5339\u307e\u3067\u3067\u3059\u3002`); return; }
|
||||
t.wanderAngle = spot.angle;
|
||||
this.log(`${t.name}\u304c\u753b\u9762\u5916\u304b\u3089\u8ff7\u3044\u8fbc\u3093\u3067\u304d\u305f\u3002`);
|
||||
return;
|
||||
|
|
@ -1436,7 +1513,6 @@
|
|||
const itemType = toolItemType(tool);
|
||||
// In placement mode, clicks always mean placement.
|
||||
// Editing rotators / reciprocators is reserved for observe or non-placement tools.
|
||||
if (itemType && this.directSetDuplicatorAt?.(x, y, itemType)) return;
|
||||
if (itemType) {
|
||||
let px = x, py = y;
|
||||
const rawItem = this.applyToolSize(new Item(itemType, px, py));
|
||||
|
|
@ -1444,6 +1520,17 @@
|
|||
const requestedAngle = this.toolAngleFor ? this.toolAngleFor(itemType) : defaultItemAngle(itemType);
|
||||
global.TarinaiPlacementPreviewSystem.applyToolOrientation(this, rawItem, requestedAngle);
|
||||
}
|
||||
const directFeeding = global.TarinaiDirectFeedingSystem;
|
||||
const directTarget = directFeeding?.isDirectFeedType?.(rawItem)
|
||||
? directFeeding.findTargetAt?.(this, x, y)
|
||||
: null;
|
||||
// A Tarinai under the pointer takes precedence over a duplicator load slot.
|
||||
if (directTarget) {
|
||||
const given = this.placeItem(rawItem, true);
|
||||
if (given) this._forceImmediateToolVisualRefresh?.("tool-change");
|
||||
return;
|
||||
}
|
||||
if (this.directSetDuplicatorAt?.(x, y, itemType)) return;
|
||||
// Placement uses the same footprint that the preview validates.
|
||||
const placed = this.placeItem(rawItem, true);
|
||||
if (!placed) return;
|
||||
|
|
@ -1474,8 +1561,8 @@
|
|||
if (type === "ball_poke" || type === "avoid_relationship" || type === "mechanism_poke") return true;
|
||||
if (/^(player_|tool_|tool:)/.test(type) || type.includes("tool") || type.includes("player")) return true;
|
||||
if (type.includes("robot")) return true;
|
||||
if (/ロボ掃除機|掃除機/.test(text)) return true;
|
||||
if (/範囲削除|コピーした|貼り付けた|つないだ|設定を変更|看板を書き換え|看板の文字を消した|を消した。|つついて動かした/.test(text)) return true;
|
||||
if (/\u30ed\u30dc\u6383\u9664\u6a5f|\u6383\u9664\u6a5f/.test(text)) return true;
|
||||
if (/\u7bc4\u56f2\u524a\u9664|\u30b3\u30d4\u30fc\u3057\u305f|\u8cbc\u308a\u4ed8\u3051\u305f|\u3064\u306a\u3044\u3060|\u8a2d\u5b9a\u3092\u5909\u66f4|\u770b\u677f\u3092\u66f8\u304d\u63db\u3048|\u770b\u677f\u306e\u6587\u5b57\u3092\u6d88\u3057\u305f|\u3092\u6d88\u3057\u305f\u3002|\u3064\u3064\u3044\u3066\u52d5\u304b\u3057\u305f/.test(text)) return true;
|
||||
return false;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const RESET_PRESETS = Object.freeze({
|
|||
family: { label: "\u5BB6\u65CF" },
|
||||
war: { label: "\u6226\u4E89" },
|
||||
happy: { label: "\u5E78\u798F" },
|
||||
hair_trigger: { label: "\u4E00\u89E6\u5373\u767A" },
|
||||
});
|
||||
|
||||
function normalizeResetPresetId(presetId = "default") {
|
||||
|
|
@ -152,6 +153,10 @@ const RESET_PRESETS = Object.freeze({
|
|||
const otherRects = worldRef.solidObstacleRects?.(other) || globalThis.TarinaiCollisionFootprints?.placementRectsFor?.(worldRef, other) || [];
|
||||
const otherRadius = presetItemFootprintRadius(worldRef, other);
|
||||
const margin = (itemIsFence || otherIsFence) ? 4 : Math.max(8, Math.min(itemRadius, otherRadius) * 0.28);
|
||||
if (opts.rotationEnvelope && item.type === "rotator" && otherIsFence && otherRects.length) {
|
||||
const envelope = Math.max(itemRadius, globalThis.TarinaiMechanicalSystem?.reach?.(item) || 0) + Math.max(8, Number(opts.rotationMargin || 12) || 12);
|
||||
if (otherRects.some(rect => globalThis.TarinaiCollisionFootprints?.rectCircleOverlap?.(rect, item.x, item.y, envelope, 0))) return true;
|
||||
}
|
||||
if (itemRects.length && otherRects.length) {
|
||||
if (itemRects.some(a => otherRects.some(b => globalThis.TarinaiCollisionFootprints?.rectsOverlap?.(a, b, margin)))) return true;
|
||||
continue;
|
||||
|
|
@ -200,8 +205,19 @@ const RESET_PRESETS = Object.freeze({
|
|||
function addPresetItem(worldRef, type, x, y, opts = {}) {
|
||||
const item = new Item(type, x, y);
|
||||
if (Number.isFinite(Number(opts.r))) item.r = Number(opts.r);
|
||||
if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle);
|
||||
if (opts.randomAngle) item.angle = randomPresetAngle(Number(opts.angleStep) || Math.PI / 4);
|
||||
else if (Number.isFinite(Number(opts.angle))) item.angle = Number(opts.angle);
|
||||
if (opts.toolSize) item.toolSize = opts.toolSize;
|
||||
if (type === "fan") {
|
||||
const center = Number.isFinite(Number(opts.fanSwingCenter)) ? Number(opts.fanSwingCenter) : (Number(item.angle) || 0);
|
||||
item.angle = center;
|
||||
item.fanBodyAngle = Number.isFinite(Number(opts.fanBodyAngle)) ? Number(opts.fanBodyAngle) : center;
|
||||
item.fanSwingCenter = center;
|
||||
item.fanSwingRange = Number.isFinite(Number(opts.fanSwingRange)) ? Number(opts.fanSwingRange) : (35 * Math.PI / 180);
|
||||
item.fanSwingSpeed = Number.isFinite(Number(opts.fanSwingSpeed)) ? Number(opts.fanSwingSpeed) : (48 * Math.PI / 180);
|
||||
item.fanSwingPhase = Number.isFinite(Number(opts.fanSwingPhase)) ? Number(opts.fanSwingPhase) : rand(0, Math.PI * 2);
|
||||
item.fanSwingOn = opts.fanSwingOn === true && item.fanSwingRange > 0 && item.fanSwingSpeed > 0;
|
||||
}
|
||||
if (type === "duplicator") {
|
||||
item.storedFoodType = opts.storedFoodType || "sweet";
|
||||
item.storedFoodLabel = typeof toolLabel === "function" ? toolLabel(item.storedFoodType) : item.storedFoodType;
|
||||
|
|
@ -235,13 +251,15 @@ const RESET_PRESETS = Object.freeze({
|
|||
|
||||
function addPresetItemsScattered(worldRef, type, count, zone = null, opts = {}) {
|
||||
let placed = 0;
|
||||
const placedItems = [];
|
||||
const n = Math.max(0, Number(count) | 0);
|
||||
const attempts = opts.avoidOverlap ? Math.max(n * 4, n + 8) : n;
|
||||
for (let i = 0; i < attempts && placed < n; i++) {
|
||||
const p = randPoint(worldRef, opts.pad || 82, zone);
|
||||
const item = addPresetItem(worldRef, type, p.x, p.y, opts);
|
||||
if (item) placed++;
|
||||
if (item) { placed++; placedItems.push(item); }
|
||||
}
|
||||
return placedItems;
|
||||
}
|
||||
|
||||
function addPresetFenceLine(worldRef, count, yRatio, angle = 0, type = "fence_h", opts = {}) {
|
||||
|
|
@ -264,6 +282,48 @@ const RESET_PRESETS = Object.freeze({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function addPresetBounceFenceBox(worldRef, cx, cy, halfW, halfH) {
|
||||
const fenceLength = 142;
|
||||
const horizontalCount = Math.max(2, Math.ceil((halfW * 2) / fenceLength));
|
||||
const verticalCount = Math.max(2, Math.ceil((halfH * 2) / fenceLength));
|
||||
const horizontalStep = (halfW * 2) / horizontalCount;
|
||||
const verticalStep = (halfH * 2) / verticalCount;
|
||||
for (let i = 0; i < horizontalCount; i++) {
|
||||
const x = cx - halfW + horizontalStep * (i + 0.5);
|
||||
addPresetItem(worldRef, "bounce_fence", x, cy - halfH, { angle: 0, reason: "preset-hair-trigger-boundary" });
|
||||
addPresetItem(worldRef, "bounce_fence", x, cy + halfH, { angle: 0, reason: "preset-hair-trigger-boundary" });
|
||||
}
|
||||
for (let i = 0; i < verticalCount; i++) {
|
||||
const y = cy - halfH + verticalStep * (i + 0.5);
|
||||
addPresetItem(worldRef, "bounce_fence", cx - halfW, y, { angle: Math.PI / 2, reason: "preset-hair-trigger-boundary" });
|
||||
addPresetItem(worldRef, "bounce_fence", cx + halfW, y, { angle: Math.PI / 2, reason: "preset-hair-trigger-boundary" });
|
||||
}
|
||||
return { cx, cy, halfW, halfH };
|
||||
}
|
||||
|
||||
function pointOutsidePresetBox(point, box, margin = 0) {
|
||||
return Math.abs((point?.x || 0) - box.cx) > box.halfW + margin || Math.abs((point?.y || 0) - box.cy) > box.halfH + margin;
|
||||
}
|
||||
|
||||
function addPresetTarinaiOutsideBox(worldRef, count, box) {
|
||||
const n = Math.max(0, Number(count) | 0);
|
||||
const zones = [
|
||||
{ x0: 82, x1: Math.max(82, box.cx - box.halfW - 70), y0: 82, y1: worldRef.h - 82 },
|
||||
{ x0: Math.min(worldRef.w - 82, box.cx + box.halfW + 70), x1: worldRef.w - 82, y0: 82, y1: worldRef.h - 82 },
|
||||
{ x0: box.cx - box.halfW, x1: box.cx + box.halfW, y0: 82, y1: Math.max(82, box.cy - box.halfH - 70) },
|
||||
{ x0: box.cx - box.halfW, x1: box.cx + box.halfW, y0: Math.min(worldRef.h - 82, box.cy + box.halfH + 70), y1: worldRef.h - 82 },
|
||||
].filter(zone => zone.x1 >= zone.x0 && zone.y1 >= zone.y0);
|
||||
for (let i = 0; i < n; i++) {
|
||||
let p = randPoint(worldRef, 82, zones[i % Math.max(1, zones.length)] || null);
|
||||
if (!pointOutsidePresetBox(p, box, 28)) {
|
||||
const side = i % 2 === 0 ? -1 : 1;
|
||||
p = { x: clamp(box.cx + side * (box.halfW + 96), 82, worldRef.w - 82), y: clamp(box.cy + rand(-box.halfH, box.halfH), 82, worldRef.h - 82) };
|
||||
}
|
||||
worldRef.addTarinai({ x: p.x, y: p.y, generation: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function generateDefaultPreset(worldRef, field, seedPopulation) {
|
||||
const population = seedPopulation ?? field.population ?? CONFIG.initialPopulation;
|
||||
const grassCount = field.grass ?? 18;
|
||||
|
|
@ -282,10 +342,11 @@ const RESET_PRESETS = Object.freeze({
|
|||
addPresetItemsScattered(worldRef, "stone", presetCount(field, compact ? 4 : 6, 3, 9), center, { pad: compact ? 118 : 132, reason: "preset-athletic-physical-block", avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 260 : 220 });
|
||||
addPresetItemsScattered(worldRef, "stove", presetCount(field, 2, 1, 3), center, { pad: 118, reason: "preset-athletic-stove", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: 210 });
|
||||
addPresetItemsScattered(worldRef, "ball", presetCount(field, compact ? 8 : 10, 4, 18), null, { pad: 96, reason: "preset-ball", avoidOverlap: true, avoidAllObstacles: true, attempts: 90, maxRadius: compact ? 230 : 190 });
|
||||
addPresetItemsScattered(worldRef, "balloon", presetCount(field, compact ? 5 : 7, 3, 12), center, { pad: 92, reason: "preset-athletic-water-balloon", avoidOverlap: true, avoidAllObstacles: true, attempts: 100, maxRadius: compact ? 240 : 200 });
|
||||
addPresetFenceLine(worldRef, Math.max(2, Math.round((compact ? 3 : 4) * factor)), 0.36, 0, "fence_h", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
|
||||
addPresetFenceLine(worldRef, Math.max(1, Math.round((compact ? 2 : 3) * factor)), 0.62, Math.PI / 2, "fence_v", { avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210, reason: "preset-athletic-fence-line" });
|
||||
addPresetFencesScattered(worldRef, Math.max(1, Math.round((compact ? 2 : 4) * factor)), null, { pad: 108, reason: "preset-athletic-fence", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: compact ? 250 : 210 });
|
||||
addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-slow-custom", motorSpeed: Math.PI * 0.18, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
|
||||
addPresetItemsScattered(worldRef, "rotator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-rotator-simple", segments: [[-66, 0, 66, 0]], motorSpeed: Math.PI * 0.18, thickness: 13, rotationEnvelope: true, rotationMargin: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 180, maxRadius: compact ? 320 : 280 });
|
||||
addPresetItemsScattered(worldRef, "reciprocator", Math.max(1, Math.round(2 * factor)), null, { pad: 148, reason: "preset-reciprocator-slow-custom", railMotorSpeed: 36, railTravel: 120, thickness: 14, avoidOverlap: true, avoidAllObstacles: true, attempts: 140, maxRadius: compact ? 280 : 240 });
|
||||
}
|
||||
|
||||
|
|
@ -317,6 +378,8 @@ const RESET_PRESETS = Object.freeze({
|
|||
addPresetItemsScattered(worldRef, "ant_nest", presetCount(field, 1.25, 1, 2), null, { pad: 116, reason: "preset-war-ant-nest", avoidOverlap: true, attempts: 80, maxRadius: 190 });
|
||||
addPresetItemsScattered(worldRef, "pushpin", presetCount(field, 5, 2, 9), null, { pad: 88, reason: "preset-war-pushpin-reduced" });
|
||||
addPresetItemsScattered(worldRef, "oshibyo", presetCount(field, 8, 4, 16), null, { pad: 88, reason: "preset-war-oshibyo" });
|
||||
addPresetItemsScattered(worldRef, "fan", presetCount(field, 2, 1, 4), null, { pad: 118, reason: "preset-war-rotating-fan", randomAngle: true, fanSwingOn: true, fanSwingRange: 110 * Math.PI / 180, fanSwingSpeed: 72 * Math.PI / 180, avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 230 });
|
||||
addPresetItemsScattered(worldRef, "bounce_fence", presetCount(field, 3, 2, 5), null, { pad: 104, reason: "preset-war-bounce-fence", randomAngle: true, avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 230 });
|
||||
addPresetFencesScattered(worldRef, presetCount(field, 8, 4, 14), null, { pad: 94, reason: "preset-war-fence" });
|
||||
}
|
||||
|
||||
|
|
@ -325,10 +388,38 @@ const RESET_PRESETS = Object.freeze({
|
|||
addPresetTarinai(worldRef, presetCount(field, 16, 8, 28), center);
|
||||
addPresetItemsScattered(worldRef, "nest_box", presetCount(field, 8, 4, 14), center, { pad: 96, reason: "preset-happy-nest-box" });
|
||||
addPresetItemsScattered(worldRef, "robot_cleaner", presetCount(field, 2, 1, 3), center, { pad: 110, reason: "preset-happy-robot-cleaner" });
|
||||
addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" });
|
||||
const happyDuplicators = addPresetItemsScattered(worldRef, "duplicator", presetCount(field, 2, 1, 4), center, { pad: 110, storedFoodType: "sweet", reason: "preset-happy-duplicator-zunda" });
|
||||
for (const duplicator of happyDuplicators) addPresetItem(worldRef, "toilet", duplicator.x, duplicator.y, { reason: "preset-happy-toilet-sand" });
|
||||
addPresetItemsScattered(worldRef, "rain_shelter", presetCount(field, 3, 2, 5), center, { pad: 132, reason: "preset-happy-parasol", avoidOverlap: true, avoidAllObstacles: true, attempts: 120, maxRadius: 240 });
|
||||
addPresetItemsScattered(worldRef, "balloon", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-water-balloon", avoidOverlap: true, attempts: 90, maxRadius: 190 });
|
||||
addPresetItemsScattered(worldRef, "ball", presetCount(field, 6, 3, 10), center, { pad: 88, reason: "preset-happy-ball" });
|
||||
}
|
||||
|
||||
|
||||
function generateHairTriggerPreset(worldRef, field) {
|
||||
const cx = worldRef.w * 0.5;
|
||||
const cy = worldRef.h * 0.5;
|
||||
const halfW = clamp(worldRef.w * 0.18, 132, 238);
|
||||
const halfH = clamp(worldRef.h * 0.18, 104, 182);
|
||||
const box = addPresetBounceFenceBox(worldRef, cx, cy, halfW, halfH);
|
||||
const inside = {
|
||||
x0: cx - halfW + 52,
|
||||
x1: cx + halfW - 52,
|
||||
y0: cy - halfH + 52,
|
||||
y1: cy + halfH - 52,
|
||||
};
|
||||
addPresetItemsScattered(worldRef, "ball", presetCount(field, 7, 4, 12), inside, { pad: 0, reason: "preset-hair-trigger-ball" });
|
||||
addPresetTarinaiOutsideBox(worldRef, presetCount(field, 14, 7, 24), box);
|
||||
const duplicatorCount = presetCount(field, 2, 1, 4);
|
||||
for (let i = 0; i < duplicatorCount; i++) {
|
||||
const side = i % 2 === 0 ? -1 : 1;
|
||||
const x = clamp(cx + side * (halfW + 112), 84, worldRef.w - 84);
|
||||
const spread = duplicatorCount <= 2 ? 0 : ((i / Math.max(1, duplicatorCount - 1)) - 0.5) * Math.min(worldRef.h * 0.42, 260);
|
||||
const y = clamp(cy + spread, 84, worldRef.h - 84);
|
||||
addPresetItem(worldRef, "duplicator", x, y, { storedFoodType: "food", reason: "preset-hair-trigger-pet-food-duplicator" });
|
||||
}
|
||||
}
|
||||
|
||||
function addDefaultPresetRobotCleaner(worldRef, presetId = "default") {
|
||||
if (!worldRef || normalizeResetPresetId(presetId) === "empty") return;
|
||||
if ((worldRef.items || []).some(item => item && !item.dead && item.type === "robot_cleaner")) return;
|
||||
|
|
@ -355,6 +446,7 @@ const RESET_PRESETS = Object.freeze({
|
|||
case "family": generateFamilyPreset(worldRef, field); break;
|
||||
case "war": generateWarPreset(worldRef, field); break;
|
||||
case "happy": generateHappyPreset(worldRef, field); break;
|
||||
case "hair_trigger": generateHairTriggerPreset(worldRef, field); break;
|
||||
case "default":
|
||||
default:
|
||||
generateDefaultPreset(worldRef, field, seedPopulation);
|
||||
|
|
|
|||
|
|
@ -113,25 +113,25 @@
|
|||
if (this.temperatureAuto === false) this.currentTemperature = this.manualTemperature;
|
||||
return this.manualTemperature;
|
||||
},
|
||||
weatherFeltTemperatureOffsetAt(x, y, actor = null) {
|
||||
if (!actor) return 0;
|
||||
const weather = String(this.weather || "sunny");
|
||||
if (weather === "sunny") return 3.5;
|
||||
if (weather === "light_rain") return -3.5;
|
||||
return 0;
|
||||
},
|
||||
temperatureAt(x, y, actor = null) {
|
||||
let temp = Number.isFinite(Number(this.currentTemperature)) ? Number(this.currentTemperature) : this.updateTemperature?.(0) ?? (CONFIG.standardTemperature ?? 15);
|
||||
if (this.temperatureAuto === false) temp = this.temperatureNumber(this.manualTemperature);
|
||||
if ((this.groundType || "soil") === "ice") temp -= 5;
|
||||
const range = Math.max(24, CONFIG.temperatureItemRange ?? 190);
|
||||
for (const it of this.nearbyItems(x, y, range + 8)) {
|
||||
const fanRange = 320;
|
||||
for (const it of this.nearbyItems(x, y, Math.max(range + 8, fanRange + 40))) {
|
||||
if (!it || it.dead || (it.type !== "dry_ice" && it.type !== "stove" && it.type !== "fan")) continue;
|
||||
if (it.type === "fan") {
|
||||
const angle = itemAngleFor(it);
|
||||
const dx = x - it.x;
|
||||
const dy = y - it.y;
|
||||
const along = dx * Math.cos(angle) + dy * Math.sin(angle);
|
||||
const fanRange = range * 1.35;
|
||||
if (along <= 8 || along > fanRange) continue;
|
||||
const lateral = Math.abs(-dx * Math.sin(angle) + dy * Math.cos(angle));
|
||||
const halfWidth = 28 + along * 0.72;
|
||||
if (lateral > halfWidth) continue;
|
||||
const edge = Math.pow(clamp(1 - along / fanRange, 0, 1), 0.62) * Math.pow(clamp(1 - lateral / Math.max(1, halfWidth), 0, 1), 0.30);
|
||||
temp -= 5 * edge;
|
||||
const wind = global.TarinaiItemDynamicBallSystem?.fanWindAt?.(it, x, y, { range: fanRange }) || null;
|
||||
if (!wind) continue;
|
||||
temp -= 5 * wind.falloff;
|
||||
continue;
|
||||
}
|
||||
const d = distXY(x, y, it.x, it.y);
|
||||
|
|
@ -151,7 +151,10 @@
|
|||
temp += clamp(toward, -nestWarmthLimit, nestWarmthLimit);
|
||||
break;
|
||||
}
|
||||
if (actor && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3;
|
||||
const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem;
|
||||
const weatherBlocked = Boolean(actor && parasolSystem?.blocksWeatherAt?.(this, x, y));
|
||||
temp += weatherBlocked ? 0 : (this.weatherFeltTemperatureOffsetAt?.(x, y, actor) || 0);
|
||||
if (actor && !weatherBlocked && (actor.state === "sunbath" || (actor.sunbathTimer || 0) > 0.04)) temp += 3;
|
||||
return this.temperatureNumber(temp);
|
||||
},
|
||||
pipeInteriorTemperatureFor(pipe, actor = null) {
|
||||
|
|
@ -204,6 +207,8 @@
|
|||
if (direction === "cold") types.push("stove");
|
||||
else if (direction === "hot") types.push("dry_ice", "fan");
|
||||
else types.push("stove", "dry_ice", "fan");
|
||||
const weather = String(this.weather || "sunny");
|
||||
if ((direction === "hot" && weather === "sunny") || (direction === "cold" && weather === "light_rain")) types.push("rain_shelter");
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const type of types) {
|
||||
|
|
@ -242,6 +247,19 @@
|
|||
const itemRange = Math.max(24, CONFIG.temperatureItemRange ?? 190);
|
||||
for (const it of this.temperatureComfortCandidateItems?.(currentStatus.direction) || this.items || []) {
|
||||
if (!it || it.dead) continue;
|
||||
if (it.type === "rain_shelter") {
|
||||
const parasolSystem = global.TarinaiParasolSystem || global.TarinaiRainShelterSystem;
|
||||
const footprint = parasolSystem?.parasolFootprint?.(it) || {
|
||||
centerX: it.x,
|
||||
centerY: it.y + (Number(it.r || 50) || 50) * 0.84,
|
||||
radiusX: (Number(it.r || 50) || 50) * 1.58,
|
||||
radiusY: (Number(it.r || 50) || 50) * 0.68,
|
||||
};
|
||||
for (const [nx, ny] of [[0.82, 0], [-0.82, 0], [0.72, 0.36], [-0.72, 0.36], [0.72, -0.36], [-0.72, -0.36]]) {
|
||||
pushCandidate(footprint.centerX + footprint.radiusX * nx, footprint.centerY + footprint.radiusY * ny, -72);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (it.type === "nest_box" || it.type === "pipe") {
|
||||
const shelterTemp = it.type === "pipe" && this.pipeInteriorTemperatureFor
|
||||
? this.pipeInteriorTemperatureFor(it, actor)
|
||||
|
|
@ -273,7 +291,7 @@
|
|||
let bestScore = Infinity;
|
||||
for (const c of candidates) {
|
||||
if (this.pointBlockedByObstacle?.(c.x, c.y, Math.max(14, radius * 0.72), { exclude: actor })) continue;
|
||||
const temp = Number.isFinite(Number(c.tempOverride)) ? Number(c.tempOverride) : this.temperatureAt(c.x, c.y, actor);
|
||||
const temp = c.tempOverride != null && Number.isFinite(Number(c.tempOverride)) ? Number(c.tempOverride) : this.temperatureAt(c.x, c.y, actor);
|
||||
const status = this.temperatureStatusFor(temp, actor);
|
||||
if (currentStatus.direction === "cold" && temp <= current + 0.6 && !status.comfortable) continue;
|
||||
if (currentStatus.direction === "hot" && temp >= current - 0.6 && !status.comfortable) continue;
|
||||
|
|
@ -282,7 +300,7 @@
|
|||
const score = status.discomfort * 110 + Math.abs(temp - targetTemp) * 2.8 + d * 0.22 + blockedPenalty + (c.extra || 0);
|
||||
if (score < bestScore) {
|
||||
bestScore = score;
|
||||
const shelterLabel = c.targetItem?.type === "pipe" ? "土管" : (c.targetItem?.type === "nest_box" ? "巣箱" : "快適な温度帯");
|
||||
const shelterLabel = c.targetItem?.type === "pipe" ? "\u571f\u7ba1" : (c.targetItem?.type === "nest_box" ? "\u5de3\u7bb1" : "\u5feb\u9069\u306a\u6e29\u5ea6\u5e2f");
|
||||
best = { x: c.x, y: c.y, dead: false, detour: !c.targetItem, temperatureTarget: true, temperature: temp, label: shelterLabel };
|
||||
if (c.targetItem) best.shelterItem = c.targetItem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,19 +21,6 @@
|
|||
findDeleteToolTargetAt(x, y) {
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
if (wireTool) {
|
||||
const antCandidates = this.nearbyAnts?.(x, y, 44, true) || this.ants || [];
|
||||
for (let i = antCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const ant = antCandidates[i];
|
||||
if (!ant || ant.dead) continue;
|
||||
const hit = Math.max(10, (Number(ant.r) || 6) * 1.9);
|
||||
const d = distXY(x, y, ant.x, ant.y);
|
||||
if (d <= hit && d < bestD) {
|
||||
best = { kind: "ant", id: ant.id || "", _ref: ant, type: ant.kind || "ant", label: ant.kind === "queen" ? "女王アリ" : "アリ", x: ant.x, y: ant.y, localX: 0, localY: 0, center: true };
|
||||
bestD = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
|
|
@ -156,7 +143,7 @@
|
|||
const itemCandidates = this.nearbyItems?.(x, y, 190, true) || this.items || [];
|
||||
for (let i = itemCandidates.length - 1; i >= 0; i -= 1) {
|
||||
const it = itemCandidates[i];
|
||||
if (!it || it.dead) continue;
|
||||
if (!it || it.dead || it.type === "splat") continue;
|
||||
if ((this.tool === "rope" || this.tool === "rod" || this.tool === "spring") && global.TarinaiPhysicsSoftClear.isSoftPlacementType(it.type)) continue;
|
||||
if (wireTool && ["rope", "rod", "spring", "wire", "insulated_wire"].includes(it.type)) continue;
|
||||
const isMechanism = Boolean(global.TarinaiMechanicalSystem.isMechanicalType(it.type));
|
||||
|
|
@ -215,8 +202,8 @@
|
|||
|
||||
useLinkToolAt(x, y) {
|
||||
const endpoint = this.findLinkEndpointAt?.(x, y);
|
||||
const linkNames = { rope: "ヒモ", rod: "棒", spring: "バネ", wire: "電線", insulated_wire: "絶縁電線" };
|
||||
const toolName = linkNames[this.tool] || "接続";
|
||||
const linkNames = { rope: "\u30d2\u30e2", rod: "\u68d2", spring: "\u30d0\u30cd", wire: "\u96fb\u7dda", insulated_wire: "\u7d76\u7e01\u96fb\u7dda" };
|
||||
const toolName = linkNames[this.tool] || "\u63a5\u7d9a";
|
||||
if (!endpoint) { showToast(`${toolName}\u3092\u3064\u306a\u3050\u5bfe\u8c61\u304c\u3042\u308a\u307e\u305b\u3093\u3002`); return true; }
|
||||
if (!this.pendingLinkEndpoint) {
|
||||
this.pendingLinkEndpoint = endpoint;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,11 +1,11 @@
|
|||
"use strict";
|
||||
|
||||
const APP_VERSION = "39.16.18";
|
||||
const APP_VERSION = "39.16.31";
|
||||
const CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const v = `v=${APP_VERSION}`;
|
||||
// Generated from app_manifest.json. Run scripts/generate_app_files.py after changing static files.
|
||||
const coreScriptNames = [
|
||||
"version", "event_bus", "domain_ids", "registry_base", "disease_registry", "sound_pack", "deterministic_helpers", "item_tool_metadata", "item_tool_definitions", "item_visual_definitions", "item_food_definitions", "item_effect_definitions", "item_registry", "data", "ground_types", "input_mode_manager", "math", "geometry_helpers", "collision_footprint_system", "physics_helpers", "placement_preview_system", "pin_attachment_system", "physics_shape_editor_system", "mechanical_system", "mechanical_shape_bridge", "constraint_system", "physics_world_system", "physics_projection_system", "signal_system", "assets", "audio", "perf_profiler", "display_helpers", "render", "sim_core", "tarinai_seed_factory", "structures", "item_type_catalog", "items", "item_type_initializers", "item_lifecycle_support", "item_update_policy", "fire_runtime_system", "robot_cleaner_system", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "update_step_pipeline_runner", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_helpers", "item_render_runtime", "burn_motion_util", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_nest_sleep_system", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_food_prototype_mixin", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_reset_presets", "world_family_social", "impact_core_system", "impact_response_system", "collision_response_system", "world_combat_effects", "world_environment", "world_temperature_system", "world_pathfinding_system", "world_grass_placement_system", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "system_order", "simulation", "colony_situation_system", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_pointer_action_system", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
"version", "event_bus", "domain_ids", "registry_base", "disease_registry", "sound_pack", "deterministic_helpers", "item_tool_metadata", "item_tool_definitions", "item_visual_definitions", "item_food_definitions", "item_effect_definitions", "item_registry", "data", "ground_types", "input_mode_manager", "math", "geometry_helpers", "collision_footprint_system", "physics_helpers", "placement_preview_system", "pin_attachment_system", "physics_shape_editor_system", "mechanical_system", "mechanical_shape_bridge", "constraint_system", "physics_world_system", "physics_projection_system", "signal_system", "assets", "audio", "perf_profiler", "display_helpers", "render", "sim_core", "tarinai_seed_factory", "structures", "item_type_catalog", "items", "item_type_initializers", "item_lifecycle_support", "item_update_policy", "fire_runtime_system", "robot_cleaner_system", "item_dynamic_tool_system", "item_dynamic_ball_system", "item_dynamic_duplicator_system", "item_dynamic_pin_system", "item_dynamic_zunchi_system", "item_dynamic_system", "item_lifecycle_decay_system", "item_lifecycle_growth_system", "item_lifecycle_step_frame", "item_lifecycle_step_decay", "item_lifecycle_step_dynamic", "item_lifecycle_step_growth", "update_step_pipeline_runner", "item_lifecycle_pipeline", "item_runtime", "structure_lifecycle", "item_render_helpers", "item_render_runtime", "burn_motion_util", "ants", "health", "tarinai", "tarinai_action_spec", "tarinai_behavior_state", "tarinai_identity_social", "tarinai_action_state", "tarinai_disease_nest", "tarinai_item_effects", "tarinai_needs_core", "tarinai_behavior_text", "tarinai_forced_behavior", "tarinai_nest_sleep_system", "tarinai_item_targeting", "tarinai_consumable_behavior", "tarinai_social_action_runtime", "tarinai_building_behavior", "tarinai_action_definitions", "tarinai_needs_items", "tarinai_food_prototype_mixin", "tarinai_direct_feeding_system", "tarinai_need_planner_system", "tarinai_item_interaction_context", "tarinai_food_interaction_system", "tarinai_contact_item_system", "tarinai_item_interaction_system", "tarinai_sunbath_system", "tarinai_cursor_care_system", "tarinai_local_environment_system", "tarinai_social_move_life", "tarinai_update_step_frame", "tarinai_update_step_ai", "tarinai_update_step_environment", "tarinai_update_step_movement", "tarinai_update_step_health", "tarinai_update_pipeline", "tarinai_runtime", "tarinai_render", "world", "world_view", "family_graph", "world_reset_presets", "world_family_social", "impact_core_system", "impact_response_system", "collision_response_system", "world_combat_effects", "world_environment", "world_temperature_system", "world_pathfinding_system", "world_grass_placement_system", "world_spatial_budget", "world_ants_system", "weather_system", "item_update_scheduler", "simulation_runtime_helpers", "tarinai_update_policy", "simulation_environment_system", "simulation_item_ant_system", "simulation_effects_system", "simulation_creature_system", "simulation_maintenance_system", "simulation_ambient_system", "simulation_systems", "system_order", "simulation", "colony_situation_system", "world_update", "world_tool_actions", "world_placement_log", "world_event_effects", "command_dispatcher", "text_catalog", "ui", "ui_log", "ui_helpers", "ui_selected", "save_schema", "snapshot_system", "restore_coordinator", "history_system", "save_codec", "save_storage", "save_system", "ui_tooltips", "ui_layout_dialogs", "ui_ground", "ui_tools", "ui_input_shared", "ui_pointer_action_system", "ui_input_touch", "ui_input_mouse", "ui_bind", "ui_charts", "ui_family_data", "ui_family_async", "ui_family_layout", "ui_family_paths", "ui_family_render", "main", "debug_tools"
|
||||
];
|
||||
const CORE_ASSETS = [
|
||||
"./",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue