duu
This commit is contained in:
parent
88f618cadf
commit
8f9f5b5100
108 changed files with 2139 additions and 1500 deletions
24
AI_MAP.md
24
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.15.103`.
|
||||
- Version/cache: `app_manifest.json`, `js/version.js`, and generated `service-worker.js` share `39.15.164`.
|
||||
- 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`.
|
||||
|
|
@ -59,4 +59,26 @@ Purpose: static Japanese browser simulation/game. Optimize context by reading th
|
|||
- Keep intentional global exports for compatibility with regression checks and browser runtime; remove public facades only after repo-wide caller checks.
|
||||
- Prefer focused edits in the owning subsystem; split modules are intentional compatibility boundaries.
|
||||
- Avoid reintroducing `simulationQualityTier` branches unless a real simulation-quality setting is restored; current quality controls are visual/detail controls.
|
||||
- `High` display quality may include quality-preserving optimizations, but should not reduce simulation cadence, object limits, or visible fidelity in a noticeable way.
|
||||
- Save format compatibility with old text saves is not required when intentionally shortening/replacing save hash text; keep current save/load internally consistent instead.
|
||||
- Run `node --check js/*.js` and `python scripts/regression_check.py` for behavior changes.
|
||||
|
||||
## Current Maintenance Notes
|
||||
|
||||
This section records current boundaries only. Version-by-version cleanup history and completed work logs are intentionally omitted.
|
||||
|
||||
### Live Compatibility Surfaces
|
||||
|
||||
- Ordered global-IIFE loading is still part of the app contract; check repo-wide callers before removing bare globals or facade objects.
|
||||
- Keep public event names with active listeners: `audio:play`, `selection:changed`, `tool:selected`, `world:phase`, `log:entry`, and `tool:placed`. Telemetry-only emit calls should stay removed.
|
||||
- Save compatibility with old exported text is not required, but the current save/load round-trip must remain internally consistent.
|
||||
- Tool icon ownership is `TOOL_DEFINITIONS.icon`; UI code converts asset paths for CSS variables. Avoid restoring CSS-local `url(...)` definitions.
|
||||
- Shared geometry/display/domain helpers should stay in their owning helper modules unless a dedicated namespace migration is being done.
|
||||
|
||||
### Open Architecture Candidates
|
||||
|
||||
- Full namespace migration for behavior/action/needs/tool helpers. This requires rewriting many bare global call sites and updating regression contracts.
|
||||
- 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.",
|
||||
"version": "39.15.103",
|
||||
"version": "39.15.164",
|
||||
"entrypoints": {
|
||||
"app": "index.html",
|
||||
"runtime": "js/main.js",
|
||||
|
|
|
|||
|
|
@ -1,26 +1,3 @@
|
|||
## 39.15.136 ツールチップ・統計・病気・空腹調整
|
||||
- たりないツールチップは4行固定のまま、横幅を内容に合わせて可変化。
|
||||
- コロニー統計に毎日0:00の灰色点線と日付ラベルを表示。
|
||||
- 持ち主IDが空、または持ち主が生存個体に存在しないぬいぐるみは削除。
|
||||
- きずつき病の個体は日光浴を開始せず、日光浴中に発症した場合も中断する。日光浴回復対象からも除外。
|
||||
- 病気中に体力切れ/ストレス過多で死亡した場合、病死として病名を優先記録するよう補強。
|
||||
- 空腹度合いの自然上昇幅を通常時・睡眠時ともに3/4倍へ調整。
|
||||
|
||||
## 39.15.136 水風船・扇風機差分
|
||||
|
||||
- 水風船の飛沫は雨滴と同じ `Item("water")` 生成経路に統一。専用タイプや専用IDは使わず、短時間だけ速度を持つ水滴として扱う。
|
||||
- 水風船の破裂飛沫を増量し、飛距離・初速・飛翔時間を拡大。
|
||||
- 水風船は射撃命中、爆竹/爆発病の爆風、げんこつ、火、鋲で破裂する。
|
||||
- 扇風機の本体角度と風向きを分離。風向き変更・首振りで変化するのは風範囲/矢印のみ。
|
||||
- 扇風機の風が火に当たると、火を単に弱めず、風下へ火種を飛ばして拡散する。
|
||||
- たりないが水風船で遊ぶ時の楽しさ・押し出し効果をボール寄りに調整。
|
||||
|
||||
## 39.15.118 土管ツールチップ・雨滴統一
|
||||
- 土管ツールチップは、どの土管にカーソルを合わせても土管内のたりないを一覧表示する。
|
||||
- 巣箱・土管に入り透明化済みのたりないは、個体ツールチップを出さない。
|
||||
- 土管内の体感温度補正は20℃へ最大±5℃まで寄せる。
|
||||
- 水風船破裂時に生成される水滴は、雨で生成される水滴と同じ初期化を使う。
|
||||
|
||||
# tarinai_ Game Features
|
||||
|
||||
This document describes the playable features of `tarinai_`: what the player sees, what tools do, and how tarinai behave. It is intentionally player/design-facing, not an implementation map.
|
||||
|
|
@ -166,6 +143,11 @@ Tarinai can be pushed by physics objects, carried or affected by ants, dragged b
|
|||
- **Grass ecology**: grass grows, feeds tarinai, comforts them, can become bed material, and has a small chance to reduce fire before burning.
|
||||
- **Dirt and corpses**: zunchi, splats, corpses, and ant corpses affect hygiene, disease, and observation results.
|
||||
- **Day/light conditions**: lighting and sun influence rendering and sunbath behavior.
|
||||
- **Season temperature**: automatic summer climate is roughly 5℃ hotter than the previous baseline; manual temperature control remains exact to the slider value.
|
||||
|
||||
## Crowded Scene Performance
|
||||
|
||||
Crowded fields are an intended sandbox outcome. Performance work should prefer cached, typed, spatial, dirty, or incremental lookups over lowering object limits. High display quality should preserve visible fidelity and simulation rules while avoiding repeated hidden scans in render, tooltip, family tree, collision, effect, fire, and maintenance paths.
|
||||
|
||||
## Disease And Recovery
|
||||
|
||||
|
|
@ -191,25 +173,6 @@ Recovery depends on the disease or effect. Water, rest, food, medicine, zunda ju
|
|||
## Save, Reset, And Persistence
|
||||
|
||||
- The app supports manual save and restore through browser storage.
|
||||
- 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.
|
||||
|
||||
|
||||
## 39.15.117 土管外観・内部温度
|
||||
|
||||
- 土管の外観を、丸い口と側面が見える斜め上からの視点に変更。
|
||||
- 土管内の体感温度を20℃へ近づける内部温度補正を追加。補正幅は最大7℃。
|
||||
- 土管ツールチップの内部気温表示にも、土管内補正後の温度を反映。
|
||||
|
||||
## 39.15.115 水風船の飛散強化・扇風機首振り設定
|
||||
- 水風船破裂時に水滴が放射状に飛び、短時間移動してから水たまり状に落ち着く。
|
||||
- 配置済み扇風機をクリックすると首振り設定UIを開ける。個別に首振りON/OFF、向き、振り幅、速度を設定可能。
|
||||
- 首振り中の扇風機は風の範囲表示と実際の風判定が現在角度に追従する。
|
||||
|
||||
## 39.15.115 土管
|
||||
|
||||
- 生活カテゴリに「土管」を追加。丸い口が見える配置アイテムとして表示。
|
||||
- たりないが巣箱のように土管の中へ入り、内部の個体は巣箱系ツールチップで確認可能。
|
||||
- 土管から出る時は、複数ある場合は別のランダムな土管から出る。
|
||||
- 土管内でも眠れるが、寝心地と睡眠優先順位は干草寝床の一段下。
|
||||
|
||||
|
|
|
|||
40
README.md
40
README.md
|
|
@ -2,11 +2,6 @@
|
|||
|
||||
AI-first entrypoint for a static browser simulation/game. Keep this file small; read deeper docs only when needed.
|
||||
|
||||
## v39.15.70 robot cleaner tuning
|
||||
- Tuned ロボ掃除機 targeting: normal speed is 60, high-speed mode is 120, and speed no longer changes based on the selected cleanup targets.
|
||||
- The cleaner checks the nearest matching target every 2 seconds, then keeps trying to clean that target between scans. Contact cleanup still removes every matching object it touches.
|
||||
- The 死骸 target also covers blood traces. When たりない is enabled, nearby Tarinai panic while chased and are cleaned on contact.
|
||||
|
||||
## Read Strategy
|
||||
|
||||
- Start here for architecture, script order constraints, and where to look next.
|
||||
|
|
@ -18,16 +13,17 @@ 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.15.103`.
|
||||
- No bundler; `index.html` loads ordered global-IIFE scripts with `?v=39.15.164`.
|
||||
- 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`.
|
||||
- Generated/static app shell: `app_manifest.json`, `service-worker.js`, and `js/version.js` must stay version-aligned.
|
||||
- Generated/static app shell: `app_manifest.json`, `service-worker.js`, `FILES.json`, and `js/version.js` must stay version-aligned.
|
||||
|
||||
## File Routing
|
||||
|
||||
- `index.html`: DOM shell, CSS/JS load order, panels/dialogs/canvas.
|
||||
- `css/*.css`: base/layout/panel/components/mobile styling.
|
||||
- `js/domain_ids.js`, `js/item_type_catalog.js`, `js/data.js`, `js/ground_types.js`: stable domain ids, current item-type save catalog, core config, field/ground definitions.
|
||||
- `js/world*.js`: world state, view, update phases, tools, placement, combat, environment, family/social, ants.
|
||||
- `js/tarinai*.js`: creature state, needs, actions, behavior, social life, item effects, update pipeline, rendering.
|
||||
- `js/item*.js`, `js/structures*.js`: item registry/runtime/lifecycle/dynamic behavior/rendering plus structures.
|
||||
|
|
@ -55,33 +51,3 @@ AI-first entrypoint for a static browser simulation/game. Keep this file small;
|
|||
- Syntax: `node --check js/*.js`
|
||||
- Regression: `python scripts/regression_check.py`
|
||||
- Inventory sanity: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All`
|
||||
|
||||
|
||||
## UI media isolation / scroll repair
|
||||
|
||||
- PC / smartphone UI override rules are separated into `@media (min-width: 981px)` and `@media (max-width: 980px)`.
|
||||
- PC family tree card is restored below the main game layout, not inside the right menu.
|
||||
- Smartphone manual scroll no longer uses internal vertical scroll traps for event log / family tree content.
|
||||
- Auto-scroll does not open collapsed cards.
|
||||
|
||||
|
||||
## Event/family/oshibyo UI fixes
|
||||
|
||||
- Event UI hides direct player item placement records while keeping those records in the internal log.
|
||||
- Collapsed Event UI no longer leaves a large blank area from the log-card min-height.
|
||||
- Family tree nodes show inherited stats in a 2x2 grid and move the tarinai portrait to the upper-right.
|
||||
- Oshibyo lodged position is anchored to the left side of the tarinai sprite.
|
||||
|
||||
|
||||
## Family polish
|
||||
|
||||
- Family tree generation row spacing is tightened.
|
||||
- Family tree personality tags also use a 2x2 grid (up to 4 visible tags).
|
||||
- Sprinkler effective cleaning radius is increased to 3x the previous value and still filters targets by actual distance.
|
||||
|
||||
|
||||
## Owned object panic / shoot effect
|
||||
|
||||
- Tarinai panic when their own plushie or simple grass bed is destroyed.
|
||||
- The selected Tarinai data behavior text explicitly shows the owned-object panic reason.
|
||||
- Shooting impact effects are larger and remain visible for slightly longer.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "39.15.136",
|
||||
"version": "39.15.164",
|
||||
"css": [
|
||||
"css/base.css",
|
||||
"css/layout.css",
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
"js": [
|
||||
"js/version.js",
|
||||
"js/event_bus.js",
|
||||
"js/domain_ids.js",
|
||||
"js/registry_base.js",
|
||||
"js/disease_registry.js",
|
||||
"js/sound_pack.js",
|
||||
|
|
@ -38,10 +39,12 @@
|
|||
"js/assets.js",
|
||||
"js/audio.js",
|
||||
"js/perf_profiler.js",
|
||||
"js/display_helpers.js",
|
||||
"js/render.js",
|
||||
"js/sim_core.js",
|
||||
"js/tarinai_seed_factory.js",
|
||||
"js/structures.js",
|
||||
"js/item_type_catalog.js",
|
||||
"js/items.js",
|
||||
"js/item_type_initializers.js",
|
||||
"js/item_lifecycle_support.js",
|
||||
|
|
|
|||
29
css/base.css
29
css/base.css
|
|
@ -402,35 +402,6 @@ h1 {
|
|||
.tool[data-tool="dwarf_drug"] .tool-sprite-preview,
|
||||
.tool[data-tool="oshibyo"] .tool-sprite-preview { width: 22px; height: 22px; left: 5px; }
|
||||
.tool[data-tool="poke"], .tool[data-tool="water_hose"] { --tool-icon-zoom: 0.92; }
|
||||
.tool[data-tool="observe"] { --tool-icon: url("../assets/ui/tool_observe.webp"); }
|
||||
.tool[data-tool="zunchi"] { --tool-icon: url("../assets/ui/tool_zunchi.webp"); }
|
||||
.tool[data-tool="sweet"] { --tool-icon: url("../assets/ui/tool_sweet.webp"); }
|
||||
.tool[data-tool="grass"] { --tool-icon: url("../assets/ui/tool_grass.webp"); }
|
||||
.tool[data-tool="stone"] { --tool-icon: url("../assets/ui/tool_stone.webp"); }
|
||||
.tool[data-tool="genkotsu"] { --tool-icon: url("../assets/ui/tool_genkotsu.webp"); }
|
||||
.tool[data-tool="firecracker"] { --tool-icon: url("../assets/ui/tool_firecracker.webp"); }
|
||||
.tool[data-tool="shoot"] { --tool-icon: url("../assets/ui/tool_shoot.png"); --tool-icon-zoom: 1.48; }
|
||||
.tool[data-tool="fence_v"] { --tool-icon: url("../assets/ui/tool_fence_v.webp"); }
|
||||
.tool[data-tool="fence_h"] { --tool-icon: url("../assets/ui/tool_fence_h.webp"); }
|
||||
.tool[data-tool="bed"] { --tool-icon: url("../assets/ui/tool_bed.webp"); }
|
||||
.tool[data-tool="love_mochi"] { --tool-icon: url("../assets/ui/tool_love_mochi.webp"); }
|
||||
.tool[data-tool="fight_mochi"] { --tool-icon: url("../assets/ui/tool_fight_mochi.webp"); }
|
||||
|
||||
.tool[data-tool="signboard"] { --tool-icon: url("../assets/ui/tool_signboard.webp"); }
|
||||
.tool[data-tool="duplicator"] { --tool-icon: url("../assets/ui/tool_duplicator.webp"); }
|
||||
.tool[data-tool="pushpin"] { --tool-icon: url("../assets/ui/tool_pushpin.webp"); }
|
||||
.tool[data-tool="oshibyo"] { --tool-icon: url("../assets/ui/tool_oshibyo.webp"); }
|
||||
|
||||
/* Generated parameter item tool icons. */
|
||||
.tool[data-tool="laxative"] { --tool-icon: url("../assets/ui/tool_laxative.webp"); }
|
||||
.tool[data-tool="protein"] { --tool-icon: url("../assets/ui/tool_protein.webp"); }
|
||||
.tool[data-tool="niteropu"] { --tool-icon: url("../assets/ui/tool_niteropu.webp"); }
|
||||
.tool[data-tool="ammo"] { --tool-icon: url("../assets/ui/tool_ammo.webp"); }
|
||||
.tool[data-tool="mystery_drug"] { --tool-icon: url("../assets/ui/tool_mystery_drug.webp"); }
|
||||
.tool[data-tool="mercury"] { --tool-icon: url("../assets/ui/tool_mercury.webp"); }
|
||||
.tool[data-tool="giant_drug"] { --tool-icon: url("../assets/ui/tool_giant_drug.webp"); }
|
||||
.tool[data-tool="dwarf_drug"] { --tool-icon: url("../assets/ui/tool_dwarf_drug.webp"); }
|
||||
.tool[data-tool="zunda_juice"] { --tool-icon: url("../assets/ui/tool_zunda_juice.webp"); }
|
||||
|
||||
.signboard-editor {
|
||||
position: fixed;
|
||||
|
|
|
|||
|
|
@ -790,16 +790,6 @@
|
|||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.tool-category[data-tool-category="operate"]:not(.collapsed) { top: 58px; }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
body.touch-mobile-ui .tool-tip-popover {
|
||||
max-width: min(280px, calc(100vw - 24px));
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapsed event card must shrink to its header.
|
||||
layout.css gives .log-card a min-height, which left a large blank panel when folded. */
|
||||
.log-card.collapsed { min-height: 0 !important; height: auto !important; flex: 0 0 auto !important; flex-basis: auto !important; }
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
/* CSS split file; preserve manifest load order. */
|
||||
.tool[data-tool="sleep_drug"] { --tool-icon: url("../assets/ui/tool_sleep_drug.webp"); }
|
||||
.tool[data-tool="nest_box"] { --tool-icon: url("../assets/ui/tool_nest_box.webp"); }
|
||||
.tool[data-tool="ant_nest"] { --tool-icon: url("../assets/ui/tool_ant_nest.webp"); }
|
||||
.tool[data-tool="ball"] { --tool-icon: url("../assets/ui/tool_ball.webp"); --tool-icon-zoom: 1.36; }
|
||||
.tool[data-tool="delete"] { --tool-icon: url("../assets/ui/tool_delete.webp"); }
|
||||
.tool[data-tool="new"] { --tool-icon: url("../assets/ui/tool_new.webp"); }
|
||||
.tool[data-tool="shoot"] { --tool-icon: url("../assets/ui/tool_shoot.png"); --tool-icon-zoom: 1.48; }
|
||||
/* CSS split file; preserve manifest load order. Tool image paths come from TOOL_DEFINITIONS. */
|
||||
.tool[data-tool="ball"] { --tool-icon-zoom: 1.36; }
|
||||
.tool[data-tool="shoot"] { --tool-icon-zoom: 1.48; }
|
||||
|
||||
.tool.tool-emoji-icon { --tool-icon: none; }
|
||||
.tool.tool-emoji-icon::before {
|
||||
|
|
@ -302,24 +297,6 @@ meter {
|
|||
.log-observe { border-left-color: rgba(175, 146, 86, 0.62); }
|
||||
.log-event { border-left-color: rgba(132, 120, 210, 0.62); }
|
||||
|
||||
@media (max-width: 980px) {
|
||||
#app { padding: 10px; }
|
||||
.topbar { align-items: flex-start; flex-direction: column; }
|
||||
.top-left { align-items: flex-start; flex-direction: column; }
|
||||
.brand { align-items: center; flex-direction: row; gap: 10px; }
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.panel { max-height: none; overflow: visible; }
|
||||
#gameCanvas { height: var(--field-mobile-height, 66vh); min-height: var(--field-mobile-min-height, 460px); }
|
||||
.stage-wrap { min-height: var(--field-mobile-min-height, 460px); }
|
||||
.field-choice-grid { grid-template-columns: 1fr; }
|
||||
.reset-preset-choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Clearer tool category grouping. */
|
||||
.tool-category[data-tool-category="operate"] { --tool-cat-a: rgba(224,239,255,0.82); --tool-cat-b: rgba(203,226,255,0.74); --tool-cat-line: rgba(82,128,190,0.26); }
|
||||
.tool-category[data-tool-category="food"] { --tool-cat-a: rgba(236,252,219,0.82); --tool-cat-b: rgba(214,244,194,0.74); --tool-cat-line: rgba(96,156,72,0.26); }
|
||||
|
|
|
|||
|
|
@ -227,7 +227,16 @@ body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
|
|||
align-items: stretch;
|
||||
gap: 8px;
|
||||
}
|
||||
.top-left { gap: 8px; }
|
||||
.top-left {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.brand {
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
}
|
||||
.brand h1,
|
||||
h1 {
|
||||
max-width: 100%;
|
||||
|
|
@ -265,9 +274,16 @@ body.touch-mobile-ui.mobile-mode-tool #gameCanvas { cursor: crosshair; }
|
|||
gap: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
.field-choice-grid { grid-template-columns: 1fr; }
|
||||
.reset-preset-choice-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
#gameCanvas {
|
||||
height: var(--field-mobile-height, 66vh);
|
||||
min-height: var(--field-mobile-min-height, 460px);
|
||||
}
|
||||
.stage-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: var(--field-mobile-min-height, 460px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel {
|
||||
|
|
|
|||
|
|
@ -137,22 +137,10 @@ body::before {
|
|||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.family-genetic-stats b,
|
||||
.family-fight-stat b {
|
||||
.family-genetic-stats b {
|
||||
font-weight: 800;
|
||||
color: rgba(120, 86, 30, 0.92);
|
||||
}
|
||||
.family-fight-stat {
|
||||
display: block;
|
||||
margin-top: 1px;
|
||||
max-width: 82px;
|
||||
font-size: 9.7px;
|
||||
line-height: 1.13;
|
||||
color: rgba(88, 70, 48, 0.82);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.lineage-family-tree .tree-node.champion {
|
||||
border-color: rgba(202, 146, 33, 0.58);
|
||||
box-shadow: inset 0 0 0 1px rgba(247, 200, 64, 0.30), 0 3px 8px rgba(120, 78, 12, 0.09);
|
||||
|
|
|
|||
347
index.html
347
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=16.07.17" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=16.07.17" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=16.07.17" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.136" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.136" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.136" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.136" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.136" />
|
||||
<link rel="icon" type="image/png" href="assets/ui/favicon.png?v=39.15.164" />
|
||||
<link rel="apple-touch-icon" href="assets/ui/apple-touch-icon.png?v=39.15.164" />
|
||||
<link rel="shortcut icon" href="favicon.ico?v=39.15.164" />
|
||||
<link rel="stylesheet" href="css/base.css?v=39.15.164" />
|
||||
<link rel="stylesheet" href="css/layout.css?v=39.15.164" />
|
||||
<link rel="stylesheet" href="css/panel.css?v=39.15.164" />
|
||||
<link rel="stylesheet" href="css/components.css?v=39.15.164" />
|
||||
<link rel="stylesheet" href="css/mobile.css?v=39.15.164" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="loadingScreen" class="loading-screen" aria-live="polite">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<aside class="panel">
|
||||
<nav id="panelQuickTabs" class="panel-quick-tabs" aria-label="右メニューのタブ">
|
||||
<button type="button" data-panel-tab="game">本体</button>
|
||||
<button type="button" data-panel-tab="selected">たりないデータ</button>
|
||||
<button type="button" data-panel-tab="selected">たりない<br>データ</button>
|
||||
<button type="button" data-panel-tab="tools">道具</button>
|
||||
<button type="button" data-panel-tab="colony">コロニー</button>
|
||||
<button type="button" data-panel-tab="events">イベント</button>
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
<div id="selectedInfo" class="selected-info empty">たりないを選択してください</div>
|
||||
</section>
|
||||
|
||||
<section id="toolCard" class="card tool-card collapsible-card" data-card-key="tools">
|
||||
<section id="toolCard" class="card tool-card collapsible-card">
|
||||
<button class="panel-card-toggle" type="button" aria-expanded="true"><span class="panel-card-title">道具</span><span class="panel-card-state">折り畳み</span></button>
|
||||
<div class="panel-card-body">
|
||||
<div class="tool-body">
|
||||
|
|
@ -94,11 +94,11 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card colony-card collapsible-card" data-card-key="colony">
|
||||
<section class="card colony-card collapsible-card">
|
||||
<button class="panel-card-toggle" type="button" aria-expanded="true"><span class="panel-card-title">コロニー</span><span class="panel-card-state">折り畳み</span></button>
|
||||
<div class="panel-card-body">
|
||||
<div class="stats colony-stats">
|
||||
<div class="stat"><span>個体</span><strong id="statPop">0</strong></div>
|
||||
<div class="stat"><span>総数</span><strong id="statPop">0</strong></div>
|
||||
<div class="stat"><span>死亡</span><strong id="statDead">0</strong></div>
|
||||
<div class="stat"><span>状態</span><strong id="statColonyMood">安定</strong></div>
|
||||
<div class="stat ground-stat" data-ground-toggle role="button" tabindex="0"><span>地面</span><button id="groundTypeBtn" class="ground-type-btn" type="button" data-ground-toggle aria-label="地面をきりかえる"><strong id="statGroundType">砂</strong></button></div>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card log-card collapsible-card" data-card-key="events">
|
||||
<section class="card log-card collapsible-card">
|
||||
<button class="panel-card-toggle" type="button" aria-expanded="true"><span class="panel-card-title">イベント</span><span class="panel-card-state">折り畳み</span></button>
|
||||
<div class="panel-card-body">
|
||||
<div id="log" class="log"></div>
|
||||
|
|
@ -252,164 +252,167 @@
|
|||
</div>
|
||||
|
||||
<div id="logPushOverlay" class="log-push-overlay" aria-live="polite" aria-atomic="false"></div>
|
||||
<script src="js/version.js?v=39.15.136" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.136" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.136" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.136" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.136" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_tool_metadata.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.136" defer></script>
|
||||
<script src="js/data.js?v=39.15.136" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.136" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.136" defer></script>
|
||||
<script src="js/math.js?v=39.15.136" defer></script>
|
||||
<script src="js/geometry_helpers.js?v=39.15.136" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.136" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.15.136" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/assets.js?v=39.15.136" defer></script>
|
||||
<script src="js/audio.js?v=39.15.136" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.136" defer></script>
|
||||
<script src="js/render.js?v=39.15.136" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.136" defer></script>
|
||||
<script src="js/structures.js?v=39.15.136" defer></script>
|
||||
<script src="js/items.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.136" defer></script>
|
||||
<script src="js/fire_runtime_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/robot_cleaner_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.136" defer></script>
|
||||
<script src="js/update_step_pipeline_runner.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.136" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_render_helpers.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.136" defer></script>
|
||||
<script src="js/burn_motion_util.js?v=39.15.136" defer></script>
|
||||
<script src="js/ants.js?v=39.15.136" defer></script>
|
||||
<script src="js/health.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.136" defer></script>
|
||||
<script src="js/world.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.136" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_reset_presets.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.136" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_temperature_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_pathfinding_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_grass_placement_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.136" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.136" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.136" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.136" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.136" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.136" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.136" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.136" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.136" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.136" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.136" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.136" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.136" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.136" defer></script>
|
||||
<script src="js/main.js?v=39.15.136" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.136" defer></script>
|
||||
<script src="js/version.js?v=39.15.164" defer></script>
|
||||
<script src="js/event_bus.js?v=39.15.164" defer></script>
|
||||
<script src="js/domain_ids.js?v=39.15.164" defer></script>
|
||||
<script src="js/registry_base.js?v=39.15.164" defer></script>
|
||||
<script src="js/disease_registry.js?v=39.15.164" defer></script>
|
||||
<script src="js/sound_pack.js?v=39.15.164" defer></script>
|
||||
<script src="js/deterministic_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_tool_metadata.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_tool_definitions.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_visual_definitions.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_food_definitions.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_effect_definitions.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_registry.js?v=39.15.164" defer></script>
|
||||
<script src="js/data.js?v=39.15.164" defer></script>
|
||||
<script src="js/ground_types.js?v=39.15.164" defer></script>
|
||||
<script src="js/input_mode_manager.js?v=39.15.164" defer></script>
|
||||
<script src="js/math.js?v=39.15.164" defer></script>
|
||||
<script src="js/geometry_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/collision_footprint_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/physics_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/placement_preview_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/pin_attachment_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/physics_shape_editor_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/mechanical_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/mechanical_shape_bridge.js?v=39.15.164" defer></script>
|
||||
<script src="js/constraint_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/physics_world_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/physics_projection_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/assets.js?v=39.15.164" defer></script>
|
||||
<script src="js/audio.js?v=39.15.164" defer></script>
|
||||
<script src="js/perf_profiler.js?v=39.15.164" defer></script>
|
||||
<script src="js/display_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/render.js?v=39.15.164" defer></script>
|
||||
<script src="js/sim_core.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_seed_factory.js?v=39.15.164" defer></script>
|
||||
<script src="js/structures.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_type_catalog.js?v=39.15.164" defer></script>
|
||||
<script src="js/items.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_type_initializers.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_support.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_update_policy.js?v=39.15.164" defer></script>
|
||||
<script src="js/fire_runtime_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/robot_cleaner_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_dynamic_tool_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_dynamic_ball_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_dynamic_duplicator_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_dynamic_pin_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_dynamic_zunchi_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_dynamic_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_decay_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_growth_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_step_frame.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_step_decay.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_step_dynamic.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_step_growth.js?v=39.15.164" defer></script>
|
||||
<script src="js/update_step_pipeline_runner.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_lifecycle_pipeline.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_runtime.js?v=39.15.164" defer></script>
|
||||
<script src="js/structure_lifecycle.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_render_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_render_runtime.js?v=39.15.164" defer></script>
|
||||
<script src="js/burn_motion_util.js?v=39.15.164" defer></script>
|
||||
<script src="js/ants.js?v=39.15.164" defer></script>
|
||||
<script src="js/health.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_action_spec.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_behavior_state.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_identity_social.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_action_state.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_disease_nest.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_item_effects.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_needs_core.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_behavior_text.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_forced_behavior.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_nest_sleep_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_item_targeting.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_consumable_behavior.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_social_action_runtime.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_building_behavior.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_action_definitions.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_needs_items.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_food_prototype_mixin.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_need_planner_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_item_interaction_context.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_food_interaction_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_contact_item_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_item_interaction_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_sunbath_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_cursor_care_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_local_environment_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_social_move_life.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_step_frame.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_step_ai.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_step_environment.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_step_movement.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_step_health.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_pipeline.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_runtime.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_render.js?v=39.15.164" defer></script>
|
||||
<script src="js/world.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_view.js?v=39.15.164" defer></script>
|
||||
<script src="js/family_graph.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_reset_presets.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_family_social.js?v=39.15.164" defer></script>
|
||||
<script src="js/impact_core_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/impact_response_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/collision_response_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_combat_effects.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_environment.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_temperature_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_pathfinding_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_grass_placement_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_spatial_budget.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_ants_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/weather_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/item_update_scheduler.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_runtime_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/tarinai_update_policy.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_environment_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_item_ant_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_effects_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_creature_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_maintenance_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_ambient_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation_systems.js?v=39.15.164" defer></script>
|
||||
<script src="js/system_order.js?v=39.15.164" defer></script>
|
||||
<script src="js/simulation.js?v=39.15.164" defer></script>
|
||||
<script src="js/colony_situation_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_update.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_tool_actions.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_placement_log.js?v=39.15.164" defer></script>
|
||||
<script src="js/world_event_effects.js?v=39.15.164" defer></script>
|
||||
<script src="js/command_dispatcher.js?v=39.15.164" defer></script>
|
||||
<script src="js/text_catalog.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_log.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_helpers.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_selected.js?v=39.15.164" defer></script>
|
||||
<script src="js/save_schema.js?v=39.15.164" defer></script>
|
||||
<script src="js/snapshot_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/restore_coordinator.js?v=39.15.164" defer></script>
|
||||
<script src="js/history_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/save_codec.js?v=39.15.164" defer></script>
|
||||
<script src="js/save_storage.js?v=39.15.164" defer></script>
|
||||
<script src="js/save_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_tooltips.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_layout_dialogs.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_ground.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_tools.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_input_shared.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_pointer_action_system.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_input_touch.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_input_mouse.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_bind.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_charts.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_family_data.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_family_async.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_family_layout.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_family_paths.js?v=39.15.164" defer></script>
|
||||
<script src="js/ui_family_render.js?v=39.15.164" defer></script>
|
||||
<script src="js/main.js?v=39.15.164" defer></script>
|
||||
<script src="js/debug_tools.js?v=39.15.164" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ class AntActor {
|
|||
this.burnWanderAngle = rand(0, Math.PI * 2);
|
||||
this.hpBarTimer = Math.max(this.hpBarTimer || 0, 1.0);
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(10, this.r * 2.8), life: 0.18, color: "rgba(255,110,36,0.56)" }));
|
||||
this.world?.emit?.("fire:ignite-ant", { ant: this, source });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +296,6 @@ class AntActor {
|
|||
target.fearTimer = Math.max(target.fearTimer || 0, 0.55);
|
||||
target.thought = "\u30a2\u30ea\u306b\u904b\u3070\u308c\u3066\u3044\u308b";
|
||||
target.recordChangeCause?.("\u30a2\u30ea\u306b\u904b\u3070\u308c\u305f", "\u72b6\u614b");
|
||||
this.world?.emit?.("ant:grabbed", { ant: this, target });
|
||||
audio.antDrag?.();
|
||||
this.world.drawListDirty = true;
|
||||
return;
|
||||
|
|
@ -361,7 +359,6 @@ class AntActor {
|
|||
target.antDraggedBy = this.id;
|
||||
if (leadHauler && (this.world?.time || 0) - (this.lastCarryEventAt || -999) > 0.75) {
|
||||
this.lastCarryEventAt = this.world?.time || 0;
|
||||
this.world?.emit?.("ant:carried", { ant: this, target, lead: true, haulers });
|
||||
}
|
||||
target.target = null;
|
||||
if (targetWasSleeping) {
|
||||
|
|
@ -415,7 +412,6 @@ class AntActor {
|
|||
if (this.dead) return;
|
||||
this.dead = true;
|
||||
audio.antDie?.();
|
||||
this.world?.emit?.("ant:died", { ant: this, home, reason: "hp" });
|
||||
const corpse = new Item("ant_corpse", this.x, this.y);
|
||||
corpse.amount = 24;
|
||||
this.world.addItem?.(corpse, "ant-corpse") || this.world.items.push(corpse);
|
||||
|
|
@ -445,7 +441,6 @@ class AntActor {
|
|||
if (!force && this.world.placementBlocked?.(base)) return null;
|
||||
audio.antNest?.();
|
||||
this.world.addItem?.(base, "ant-nest-built") || this.world.items.push(base);
|
||||
this.world.emit?.("ant:nest-built", { nest: base, queen: this });
|
||||
this.world.ensureSpatial?.("ant-nest-built");
|
||||
this.world.drawListDirty = true;
|
||||
this.world.log("\u5973\u738b\u30a2\u30ea\u304c\u65b0\u3057\u3044\u30a2\u30ea\u306e\u5de3\u3092\u4f5c\u3063\u305f\u3002", "event");
|
||||
|
|
|
|||
|
|
@ -93,9 +93,6 @@ const audio = {
|
|||
const a = new Audio(path);
|
||||
a.preload = "auto";
|
||||
a.volume = this.sampleVolumeFor(key);
|
||||
a.addEventListener("error", () => {
|
||||
window.TarinaiEvents.emit("audio:sample-error", { id: key, path });
|
||||
}, { once: true });
|
||||
this.samples[key] = a;
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,10 +15,6 @@
|
|||
const aabbOverlap = Geometry.aabbOverlap;
|
||||
const rectOverlapInfo = Geometry.rectOverlapInfo;
|
||||
const rectsOverlap = Geometry.rectsOverlap;
|
||||
function pointInRect(x, y, rect, padding = 0) {
|
||||
return Geometry.pointInRect(x, y, rect, padding);
|
||||
}
|
||||
|
||||
function rectCircleOverlap(rect, cx, cy, radius, margin = 0) {
|
||||
if (!rect) return false;
|
||||
const r = Math.max(0, num(radius));
|
||||
|
|
@ -46,7 +42,7 @@
|
|||
}
|
||||
return Math.max(14, reach);
|
||||
}
|
||||
const radiusFor = typeof global.itemRadiusFor === "function" ? global.itemRadiusFor : ((type, fallback = 12) => fallback);
|
||||
const radiusFor = global.itemRadiusFor;
|
||||
if (item.type === "duplicator") {
|
||||
// \u4FDD\u5B58\u6E08\u307F\u30C7\u30FC\u30BF\u306B\u53E4\u3044 r=34 \u304C\u6B8B\u3063\u3066\u3044\u3066\u3082\u3001\u914D\u7F6E\u5224\u5B9A\u306F\u5916\u89B3\u306B\u8FD1\u3044\u534A\u5F84\u3067\u6271\u3046\u3002
|
||||
return Math.max(14, Math.min(24, num(item.r, radiusFor(item.type, 24)) || radiusFor(item.type, 24)));
|
||||
|
|
@ -75,7 +71,7 @@
|
|||
let hit = false;
|
||||
if (rects.length) {
|
||||
for (const rect of rects) {
|
||||
if (pointInRect(x, y, rect, padding)) hit = true;
|
||||
if (Geometry.pointInRect(x, y, rect, padding)) hit = true;
|
||||
bestDistance = Math.min(bestDistance, Math.hypot(num(x) - num(rect.cx, item.x), num(y) - num(rect.cy, item.y)));
|
||||
}
|
||||
}
|
||||
|
|
@ -108,8 +104,8 @@
|
|||
const otherRects = placementRectsFor(worldRef, other);
|
||||
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
|
||||
const hasMechanism = global.TarinaiMechanicalSystem.isMechanicalType(item.type) || global.TarinaiMechanicalSystem.isMechanicalType(other.type);
|
||||
const itemIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(item.type) : /fence/.test(String(item.type || ""));
|
||||
const otherIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(other.type) : /fence/.test(String(other.type || ""));
|
||||
const itemIsFence = global.isFenceItemType(item.type);
|
||||
const otherIsFence = global.isFenceItemType(other.type);
|
||||
const margin = (itemIsFence || otherIsFence) ? (item.type === "gate_fence" || other.type === "gate_fence" ? 1.5 : 2.5) : (hasMechanism ? 1.25 : Math.max(4, Math.min(itemRadius, otherRadius) * 0.18));
|
||||
if (itemRects.length && otherRects.length) {
|
||||
if (itemRects.some(a => otherRects.some(b => rectsOverlap(a, b, margin)))) return true;
|
||||
|
|
@ -129,15 +125,6 @@
|
|||
}
|
||||
|
||||
global.TarinaiCollisionFootprints = Object.freeze({
|
||||
rectCorners,
|
||||
rectAxes,
|
||||
projectPoints,
|
||||
aabbOverlap,
|
||||
rectOverlapInfo,
|
||||
rectLocalPoint,
|
||||
rectWorldVector,
|
||||
pointInRect,
|
||||
rectsOverlap,
|
||||
rectCircleOverlap,
|
||||
isSoftPlacementIgnoredType,
|
||||
itemFootprintRadius,
|
||||
|
|
|
|||
|
|
@ -280,11 +280,16 @@
|
|||
const threshold = PHYSICAL_DAMAGE_SPEED_THRESHOLD;
|
||||
const now = worldRef.time || 0;
|
||||
if (!worldRef.tarinaiCollisionMemo) worldRef.tarinaiCollisionMemo = new Map();
|
||||
const live = Array.from(worldRef.tarinai || [])
|
||||
.filter(t => t && !t.dead && !worldRef.isTarinaiHiddenInNestBox(t))
|
||||
.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
||||
const liveSource = worldRef.tarinai || [];
|
||||
const live = [];
|
||||
for (const t of liveSource) {
|
||||
if (t && !t.dead && !worldRef.isTarinaiHiddenInNestBox(t)) live.push(t);
|
||||
}
|
||||
if (live.length < 2) return;
|
||||
live.sort((a, b) => String(a.id || "").localeCompare(String(b.id || "")));
|
||||
const candidates = [];
|
||||
const maxCandidates = Math.max(24, Math.min(320, live.length * 6));
|
||||
const seenPairs = new Set();
|
||||
for (let i = 0; i < live.length; i += 1) {
|
||||
const a = live[i];
|
||||
const avx = a.vx || 0;
|
||||
|
|
@ -297,10 +302,16 @@
|
|||
const ay1 = Number.isFinite(a.y) ? a.y : ay0;
|
||||
const asx = ax1 - ax0;
|
||||
const asy = ay1 - ay0;
|
||||
for (let j = 0; j < live.length; j += 1) {
|
||||
const b = live[j];
|
||||
if (!b || b === a) continue;
|
||||
const moveDist = Math.hypot(asx, asy);
|
||||
const queryX = ax0 + asx * 0.5;
|
||||
const queryY = ay0 + asy * 0.5;
|
||||
const queryRadius = Math.max(140, (a.radius || 22) * 1.6 + moveDist + threshold * Math.max(0.016, Number(dt || 0.016) || 0.016) + 96);
|
||||
const nearby = typeof worldRef.nearbyTarinai === "function" ? Array.from(worldRef.nearbyTarinai(queryX, queryY, queryRadius, true) || []) : live;
|
||||
for (const b of nearby) {
|
||||
if (!b || b === a || b.dead || worldRef.isTarinaiHiddenInNestBox(b)) continue;
|
||||
const key = a.id < b.id ? `${a.id}:${b.id}` : `${b.id}:${a.id}`;
|
||||
if (seenPairs.has(key)) continue;
|
||||
seenPairs.add(key);
|
||||
if (now - (worldRef.tarinaiCollisionMemo.get(key) || -999) < 0.42) continue;
|
||||
const bvx = b.vx || 0;
|
||||
const bvy = b.vy || 0;
|
||||
|
|
@ -395,7 +406,6 @@
|
|||
if (worldRef.relationNotice(target.id, impactor.id, "tarinai-highspeed-collision", 2.6)) worldRef.log(`${target.name}\u306f\u9ad8\u901f\u306e${impactor.name}\u306b\u885d\u7a81\u3057\u3066\u5f3e\u304d\u98db\u3070\u3055\u308c\u305f\u3002`, "accident", { participants: [target, impactor] });
|
||||
}
|
||||
if (solved && worldRef.spatialDirty) worldRef.rebuildSpatial?.(true, "post-tarinai-highspeed-collision");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@
|
|||
if (!worldRef) return fail("missing_world");
|
||||
if (!target || target.dead) return fail("missing_target");
|
||||
target.favorite = !target.favorite;
|
||||
emit(worldRef, "selection:favorite-changed", { selected: target, favorite: Boolean(target.favorite) });
|
||||
return ok({ selected: target, favorite: Boolean(target.favorite) });
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +82,6 @@
|
|||
if (!worldRef.toolSizes) worldRef.toolSizes = {};
|
||||
worldRef.toolSizes[id] = next;
|
||||
worldRef.toolSize = next;
|
||||
emit(worldRef, "tool:size-changed", { toolId: id, size: next });
|
||||
return ok({ toolId: id, size: next });
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +101,6 @@
|
|||
function togglePaused(worldRef) {
|
||||
if (!worldRef) return fail("missing_world");
|
||||
worldRef.paused = !worldRef.paused;
|
||||
emit(worldRef, "simulation:pause-changed", { paused: Boolean(worldRef.paused) });
|
||||
return ok({ paused: Boolean(worldRef.paused) });
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +110,6 @@
|
|||
const list = Array.isArray(speeds) && speeds.length ? speeds.map(Number).filter(Number.isFinite) : Array.from(DEFAULT_SPEEDS);
|
||||
const i = list.indexOf(Number(worldRef.speed) || 1);
|
||||
worldRef.speed = list[(i + 1 + list.length) % list.length];
|
||||
emit(worldRef, "simulation:speed-changed", { speed: worldRef.speed });
|
||||
return ok({ speed: worldRef.speed });
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +121,6 @@
|
|||
if (typeof worldRef.handleClick !== "function") return fail("missing_pointer_handler");
|
||||
if (worldRef.tool && worldRef.tool !== "observe") global.TarinaiHistory.capture(worldRef, `tool:${worldRef.tool}`);
|
||||
worldRef.handleClick(px, py);
|
||||
emit(worldRef, "pointer:primary", { x: px, y: py, toolId: worldRef.tool || "" });
|
||||
return ok({ x: px, y: py, toolId: worldRef.tool || "" });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -980,10 +980,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function pointSegmentDistance(px, py, ax, ay, bx, by) {
|
||||
return global.TarinaiGeometry.pointSegmentDistance(px, py, ax, ay, bx, by);
|
||||
}
|
||||
|
||||
const api = Object.freeze({
|
||||
updateFlexibleLink,
|
||||
updateRigidLink,
|
||||
|
|
@ -991,5 +987,5 @@
|
|||
});
|
||||
|
||||
global.TarinaiConstraintSystem = api;
|
||||
global.TarinaiLinkRuntime = Object.freeze({ endpointWorld, pointSegmentDistance, snapEndpointToTarget, remapLinksForEditedMechanicalItem });
|
||||
global.TarinaiLinkRuntime = Object.freeze({ endpointWorld, snapEndpointToTarget, remapLinksForEditedMechanicalItem });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ const CONFIG = {
|
|||
reproductionCooldown: 18,
|
||||
dayLength: 120,
|
||||
standardTemperature: 15,
|
||||
summerTemperatureBoost: 5,
|
||||
climateTemperatureMin: -20,
|
||||
climateTemperatureMax: 50,
|
||||
temperatureMin: -Infinity,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
(function () {
|
||||
const params = new URLSearchParams(location.search || "");
|
||||
const enabled = params.get("debug") === "1" || params.get("debug") === "true";
|
||||
window.__tarinaiDebugOverlayEnabled = enabled;
|
||||
if (!enabled) return;
|
||||
|
||||
const host = document.createElement("div");
|
||||
|
|
|
|||
|
|
@ -26,18 +26,16 @@
|
|||
for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); }
|
||||
return ((h >>> 0) % 100000) / 100000;
|
||||
};
|
||||
global.deterministicFrame = global.deterministicFrame || ((worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6));
|
||||
global.deterministicUnit = global.deterministicUnit || ((worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, global.deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|")));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + global.deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => global.deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && global.deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
const deterministicFrame = (worldRef = null, hz = 60) => Math.floor((Number(worldRef?.time || 0) || 0) * Math.max(1, Number(hz) || 60) + 1e-6);
|
||||
const deterministicUnit = (worldRef = null, salt = "", ...parts) => hashUnit(String(worldRef?.worldSeed || "tarinai-world"), [salt, deterministicFrame(worldRef, 60), Number(worldRef?._deterministicEpoch || 0) || 0, ...parts.map(keyPart)].join("|"));
|
||||
global.deterministicRange = global.deterministicRange || ((worldRef = null, salt = "", min = 0, max = 1, ...parts) => (Number(min) || 0) + deterministicUnit(worldRef, salt, ...parts) * ((Number(max) || 0) - (Number(min) || 0)));
|
||||
global.deterministicSigned = global.deterministicSigned || ((worldRef = null, salt = "", ...parts) => deterministicUnit(worldRef, salt, ...parts) < 0.5 ? -1 : 1);
|
||||
global.deterministicChance = global.deterministicChance || ((worldRef = null, salt = "", chance = 0, ...parts) => { const p = clamp01(chance); return p >= 1 || (p > 0 && deterministicUnit(worldRef, salt, ...parts) < p); });
|
||||
global.deterministicAngle = global.deterministicAngle || ((worldRef = null, salt = "", ...parts) => global.deterministicRange(worldRef, salt, 0, Math.PI * 2, ...parts));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
// Keep the historical bare helper names available to legacy scripts while the
|
||||
// Keep the historical bare high-level helper names available to legacy scripts while the
|
||||
// implementation lives on globalThis.
|
||||
var deterministicFrame = (typeof window !== "undefined" ? window : globalThis).deterministicFrame;
|
||||
var deterministicUnit = (typeof window !== "undefined" ? window : globalThis).deterministicUnit;
|
||||
var deterministicRange = (typeof window !== "undefined" ? window : globalThis).deterministicRange;
|
||||
var deterministicSigned = (typeof window !== "undefined" ? window : globalThis).deterministicSigned;
|
||||
var deterministicChance = (typeof window !== "undefined" ? window : globalThis).deterministicChance;
|
||||
|
|
|
|||
36
js/display_helpers.js
Normal file
36
js/display_helpers.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: display/helpers
|
||||
// Shared canvas and display-label helpers used by render, item, creature, and weather modules.
|
||||
(function (global) {
|
||||
const WEATHER_LABELS = Object.freeze({ sunny: "\u6674\u308c", cloudy: "\u66c7\u308a", light_rain: "\u5c0f\u96e8" });
|
||||
|
||||
function canvasEllipsizeText(ctx, text, maxW) {
|
||||
let s = String(text || "");
|
||||
const limit = Number(maxW);
|
||||
if (!ctx || !Number.isFinite(limit) || limit <= 0) return s ? "\u2026" : "";
|
||||
if (ctx.measureText(s).width <= limit) return s;
|
||||
while (s.length > 1 && ctx.measureText(`${s}\u2026`).width > limit) s = s.slice(0, -1);
|
||||
return `${s}\u2026`;
|
||||
}
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
if (!ctx) return;
|
||||
const radius = Math.max(0, Math.min(Math.abs(Number(r) || 0), Math.abs(Number(w) || 0) * 0.5, Math.abs(Number(h) || 0) * 0.5));
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + radius, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
||||
ctx.arcTo(x, y + h, x, y, radius);
|
||||
ctx.arcTo(x, y, x + w, y, radius);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function weatherLabel(weather = "sunny") {
|
||||
return WEATHER_LABELS[String(weather || "sunny")] || WEATHER_LABELS.sunny;
|
||||
}
|
||||
|
||||
global.canvasEllipsizeText = canvasEllipsizeText;
|
||||
global.roundedRect = roundedRect;
|
||||
global.weatherLabel = weatherLabel;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
70
js/domain_ids.js
Normal file
70
js/domain_ids.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: data/domain-ids
|
||||
// Owns current domain identifier order shared by UI, simulation labels, and save schema.
|
||||
// Old save compatibility is intentionally not preserved; current encode/decode only need
|
||||
// to agree within the same app build.
|
||||
(function (global) {
|
||||
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
|
||||
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
|
||||
const WEATHER_IDS = Object.freeze(["sunny", "cloudy", "light_rain"]);
|
||||
const MOOD_IDS = Object.freeze(["relaxed", "devastation", "crisis", "confusion", "fearful", "disease_spread", "weakened", "breeding", "happy", "overcrowded", "isolated"]);
|
||||
|
||||
const STATE_IDS = Object.freeze(["idle", "sleep"]);
|
||||
const POWER_MODE_IDS = Object.freeze(["", "protein", "niteropu"]);
|
||||
const SIZE_MODE_IDS = Object.freeze(["", "giant_drug", "dwarf_drug"]);
|
||||
const LIFE_MODE_IDS = Object.freeze(["", "mercury"]);
|
||||
const PIN_STATE_IDS = Object.freeze(["loose", "lodged", "falling", "stuck"]);
|
||||
const STAGE_IDS = Object.freeze(["fresh", "aging", "old", "dry"]);
|
||||
|
||||
function indexMap(list) {
|
||||
return Object.freeze(Object.fromEntries(list.map((id, index) => [id, index])));
|
||||
}
|
||||
|
||||
const FIELD_INDEX = indexMap(FIELD_IDS);
|
||||
const GROUND_INDEX = indexMap(GROUND_IDS);
|
||||
const WEATHER_INDEX = indexMap(WEATHER_IDS);
|
||||
const MOOD_INDEX = indexMap(MOOD_IDS);
|
||||
function enumIndex(list, value, fallback = 0) {
|
||||
const i = list.indexOf(String(value ?? ""));
|
||||
return i >= 0 ? i : fallback;
|
||||
}
|
||||
|
||||
function enumValue(list, index, fallback = "") {
|
||||
const i = Number(index) | 0;
|
||||
return list[i] ?? fallback ?? list[0] ?? "";
|
||||
}
|
||||
|
||||
function canonicalFrom(list, value, fallback) {
|
||||
const id = String(value || fallback || list[0] || "");
|
||||
return list.includes(id) ? id : (fallback || list[0] || "");
|
||||
}
|
||||
|
||||
function canonicalFieldId(value = "garden") { return canonicalFrom(FIELD_IDS, value, "garden"); }
|
||||
function canonicalGroundId(value = "soil") { return canonicalFrom(GROUND_IDS, value, "soil"); }
|
||||
function canonicalWeatherId(value = "sunny") { return canonicalFrom(WEATHER_IDS, value, "sunny"); }
|
||||
function canonicalMoodId(value = "relaxed") { return canonicalFrom(MOOD_IDS, value, "relaxed"); }
|
||||
|
||||
global.TarinaiDomainIds = Object.freeze({
|
||||
FIELD_IDS,
|
||||
GROUND_IDS,
|
||||
WEATHER_IDS,
|
||||
MOOD_IDS,
|
||||
STATE_IDS,
|
||||
POWER_MODE_IDS,
|
||||
SIZE_MODE_IDS,
|
||||
LIFE_MODE_IDS,
|
||||
PIN_STATE_IDS,
|
||||
STAGE_IDS,
|
||||
FIELD_INDEX,
|
||||
GROUND_INDEX,
|
||||
WEATHER_INDEX,
|
||||
MOOD_INDEX,
|
||||
enumIndex,
|
||||
enumValue,
|
||||
canonicalFieldId,
|
||||
canonicalGroundId,
|
||||
canonicalWeatherId,
|
||||
canonicalMoodId,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const TARINAI_PUBLIC_EVENT_TYPES = new Set(["audio:play", "selection:changed", "tool:selected", "world:phase", "log:entry", "tool:placed"]);
|
||||
|
||||
class TarinaiEventBus {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
|
|
@ -15,12 +17,28 @@ class TarinaiEventBus {
|
|||
list.add(handler);
|
||||
}
|
||||
|
||||
hasDirectListeners(type) {
|
||||
if (!type) return false;
|
||||
return Boolean(this.handlers.get(type)?.size || 0);
|
||||
}
|
||||
|
||||
hasWildcardListeners(type) {
|
||||
if (!type || !TARINAI_PUBLIC_EVENT_TYPES.has(String(type))) return false;
|
||||
return Boolean(this.handlers.get("*")?.size || 0);
|
||||
}
|
||||
|
||||
hasListeners(type) {
|
||||
if (!type) return false;
|
||||
return Boolean(this.hasDirectListeners(type) || this.hasWildcardListeners(type));
|
||||
}
|
||||
|
||||
emit(type, detail = {}) {
|
||||
if (!type) return null;
|
||||
const event = { type, detail: detail || {}, at: performance?.now?.() ?? Date.now() };
|
||||
const direct = this.handlers.get(type);
|
||||
const wildcard = TARINAI_PUBLIC_EVENT_TYPES.has(String(type)) ? this.handlers.get("*") : null;
|
||||
if (!(direct?.size || 0) && !(wildcard?.size || 0)) return null;
|
||||
const event = { type, detail: detail || {}, at: performance?.now?.() ?? Date.now() };
|
||||
if (direct) for (const handler of [...direct]) this.safeCall(handler, event);
|
||||
const wildcard = this.handlers.get("*");
|
||||
if (wildcard) for (const handler of [...wildcard]) this.safeCall(handler, event);
|
||||
return event;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,50 +78,12 @@
|
|||
return childListsParent || parentListsChild;
|
||||
}
|
||||
|
||||
function relatedNodesFromIndex(family = {}, relationIndex = buildRelationIndex(family)) {
|
||||
const relatedIds = new Set();
|
||||
for (const n of Object.values(family || {})) {
|
||||
if (!n?.id) continue;
|
||||
const parents = relationIndex.parentsByChild.get(n.id) || new Set();
|
||||
const children = relationIndex.childrenByParent.get(n.id) || new Set();
|
||||
if (parents.size || children.size) relatedIds.add(n.id);
|
||||
for (const id of parents) relatedIds.add(id);
|
||||
for (const id of children) relatedIds.add(id);
|
||||
}
|
||||
return Array.from(relatedIds).map(id => family[id]).filter(Boolean);
|
||||
}
|
||||
|
||||
function buildLineageGraph(family = {}, nodes = relatedNodesFromIndex(family), relationIndex = buildRelationIndex(family)) {
|
||||
const graph = new Map();
|
||||
for (const n of nodes || []) if (n?.id) graph.set(n.id, new Set());
|
||||
const addGraphEdge = (a, b) => {
|
||||
if (!a || !b || !graph.has(a) || !graph.has(b)) return;
|
||||
graph.get(a).add(b);
|
||||
graph.get(b).add(a);
|
||||
};
|
||||
for (const n of nodes || []) {
|
||||
if (!n?.id) continue;
|
||||
const parentCandidates = new Set((n.parents || []).filter(Boolean));
|
||||
for (const parentId of relationIndex.parentsByChild.get(n.id) || []) parentCandidates.add(parentId);
|
||||
for (const parentId of parentCandidates) {
|
||||
if (parentId !== n.id && relationAllowsParentChild(family, n.id, parentId)) addGraphEdge(n.id, parentId);
|
||||
}
|
||||
const childCandidates = new Set((n.children || []).filter(Boolean));
|
||||
for (const childId of relationIndex.childrenByParent.get(n.id) || []) childCandidates.add(childId);
|
||||
for (const childId of childCandidates) {
|
||||
if (childId !== n.id && relationAllowsParentChild(family, childId, n.id)) addGraphEdge(n.id, childId);
|
||||
}
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
|
||||
global.TarinaiFamilyGraph = {
|
||||
connectedComponents,
|
||||
addParentEdge,
|
||||
buildRelationIndex,
|
||||
buildLineageGraph,
|
||||
relatedNodesFromIndex,
|
||||
relationAllowsParentChild,
|
||||
};
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
const FIRE_MAX_ACTIVE_DESKTOP = 72;
|
||||
const FIRE_MAX_ACTIVE_MOBILE = 44;
|
||||
const GRASS_FIRE_EXTINGUISH_RATE = 0.56;
|
||||
const NEST_BOX_FIRE_EVACUATE_AFTER = 10.0;
|
||||
const NEST_BOX_FIRE_MIN_LIFE = 14.0;
|
||||
const NEST_BOX_FIRE_WET_REIGNITE_DELAY = 1.65;
|
||||
const NEST_BOX_FIRE_WATER_INTERVAL = 0.16;
|
||||
const NEST_BOX_FIRE_EFFECT_INTERVAL = 0.28;
|
||||
|
||||
function fireTempMultiplier(worldRef, x, y, entity = null) {
|
||||
const weather = String(worldRef?.weather || "");
|
||||
|
|
@ -97,7 +102,6 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
t.fearTimer = Math.max(t.fearTimer || 0, 1.5);
|
||||
t.sleeping = false;
|
||||
t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(source || null, true) : null, reason: FIRE_BURNING_TEXT, wake: true });
|
||||
worldRef?.emit?.("fire:ignite", { tarinai: t, source });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +127,144 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
ant.state = "panic";
|
||||
ant.targetId = "";
|
||||
ant.targetRef = null;
|
||||
worldRef?.emit?.("fire:ignite-ant", { ant, source });
|
||||
return true;
|
||||
}
|
||||
|
||||
function isIgnitableNestBox(item) {
|
||||
return Boolean(item && !item.dead && item.type === "nest_box");
|
||||
}
|
||||
|
||||
function nestBoxFireRadius(box) {
|
||||
return Math.max(30, (Number(box?.r || 42) || 42) * 1.36);
|
||||
}
|
||||
|
||||
function nestBoxFireActive(box) {
|
||||
return Boolean(isIgnitableNestBox(box) && (box.burning || (box.burnTimer || 0) > 0.02 || (box.nestFireEvacuationTimer || 0) > 0.02));
|
||||
}
|
||||
|
||||
function igniteNestBoxByFire(box, source = null, worldRef = null) {
|
||||
if (!isIgnitableNestBox(box)) return false;
|
||||
const now = Number(worldRef?.time || box.world?.time || 0) || 0;
|
||||
if (now < Number(box.nestFireWetUntil || 0)) return false;
|
||||
const already = nestBoxFireActive(box);
|
||||
box.burning = true;
|
||||
box.burnTimer = Math.max(Number(box.burnTimer || 0) || 0, NEST_BOX_FIRE_MIN_LIFE);
|
||||
if (!already) {
|
||||
box.nestFireStartedAt = now;
|
||||
box.nestFireEvacuationTimer = NEST_BOX_FIRE_EVACUATE_AFTER;
|
||||
box.nestFireEvacuated = false;
|
||||
box.flameWaterCooldown = 0;
|
||||
box.nestFireEffectCooldown = 0;
|
||||
worldRef?.log?.("巣箱に火が燃え移った。", "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");
|
||||
worldRef?.markItemBucketsDirty?.("nest-box-fire");
|
||||
worldRef?.markSpatialDirty?.("nest-box-fire");
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return !already;
|
||||
}
|
||||
|
||||
function extinguishNestBoxFire(box, worldRef = null, reason = "water") {
|
||||
if (!isIgnitableNestBox(box) || !nestBoxFireActive(box)) return false;
|
||||
box.burning = false;
|
||||
box.burnTimer = 0;
|
||||
box.nestFireEvacuationTimer = 0;
|
||||
box.nestFireEvacuated = false;
|
||||
box.nestFireStartedAt = 0;
|
||||
box.nestFireWetUntil = reason === "water" ? ((Number(worldRef?.time || box.world?.time || 0) || 0) + NEST_BOX_FIRE_WET_REIGNITE_DELAY) : 0;
|
||||
box.flameWaterCooldown = 0;
|
||||
box.nestFireEffectCooldown = 0;
|
||||
worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.22, life: 0.24, color: "rgba(108,192,236,0.62)" }));
|
||||
worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire-extinguished");
|
||||
worldRef?.markItemBucketsDirty?.("nest-box-fire-extinguished");
|
||||
worldRef?.markSpatialDirty?.("nest-box-fire-extinguished");
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function evacuateBurningNestBox(box, worldRef = null, reason = "nest-box-fire") {
|
||||
if (!isIgnitableNestBox(box) || !worldRef || box.nestFireEvacuated) return 0;
|
||||
const occupants = global.TarinaiNestSleepSystem?.nestBoxOccupants
|
||||
? global.TarinaiNestSleepSystem.nestBoxOccupants(worldRef, box, Infinity)
|
||||
: (worldRef.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||||
box.nestFireEvacuated = true;
|
||||
let count = 0;
|
||||
const now = Number(worldRef?.time || box.world?.time || 0) || 0;
|
||||
for (const t of occupants || []) {
|
||||
if (!t || t.dead) continue;
|
||||
t._burningNestExitBoxId = box.id || t._burningNestExitBoxId || "";
|
||||
t._burningNestExitUntil = Math.max(Number(t._burningNestExitUntil || 0) || 0, now + 1.8);
|
||||
t.leaveNestBox?.(box);
|
||||
igniteTarinaiByFire(t, box, worldRef, true);
|
||||
t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 1.2);
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 2.4);
|
||||
count += 1;
|
||||
}
|
||||
if (count) {
|
||||
worldRef.log?.(`炎上した巣箱から${count}匹のたりないが追い出された。`, "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;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function burnOutNestBox(box, worldRef = null, reason = "burnout") {
|
||||
if (!isIgnitableNestBox(box)) return false;
|
||||
if (!box.nestFireEvacuated) evacuateBurningNestBox(box, worldRef, "nest-box-fire-burnout-evacuate");
|
||||
box.burning = false;
|
||||
box.burnTimer = 0;
|
||||
box.nestFireEvacuationTimer = 0;
|
||||
box.nestFireEvacuated = true;
|
||||
box.nestFireStartedAt = 0;
|
||||
box.nestFireWetUntil = 0;
|
||||
box.flameWaterCooldown = 0;
|
||||
box.nestFireEffectCooldown = 0;
|
||||
box.amount = 0;
|
||||
if (box.isStructure) {
|
||||
box.hp = 0;
|
||||
box.dead = true;
|
||||
}
|
||||
worldRef?.log?.("巣箱が焼失した。", "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");
|
||||
worldRef?.markSpatialDirty?.("nest-box-burnout");
|
||||
worldRef?.compactItems?.();
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateNestBoxFire(box, dt, worldRef) {
|
||||
if (!isIgnitableNestBox(box) || !nestBoxFireActive(box) || !worldRef) return false;
|
||||
dt = Math.max(0, Math.min(0.24, Number(dt || 0) || 0));
|
||||
box.burning = true;
|
||||
box.burnTimer = Math.max(0, Number(box.burnTimer || 0) - dt);
|
||||
box.nestFireEvacuationTimer = Math.max(0, Number(box.nestFireEvacuationTimer || 0) - dt);
|
||||
box.flameWaterCooldown = Math.max(0, Number(box.flameWaterCooldown || 0) - dt);
|
||||
box.nestFireEffectCooldown = Math.max(0, Number(box.nestFireEffectCooldown || 0) - dt);
|
||||
if (box.flameWaterCooldown <= 0) {
|
||||
box.flameWaterCooldown = NEST_BOX_FIRE_WATER_INTERVAL;
|
||||
const water = contactWaterDrop(worldRef, box.x, box.y, nestBoxFireRadius(box));
|
||||
if (water) {
|
||||
consumeWaterForFire(worldRef, water, Math.max(12, (box.r || 42) * 0.34));
|
||||
extinguishNestBoxFire(box, worldRef, "water");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!box.nestFireEvacuated && box.nestFireEvacuationTimer <= 0) {
|
||||
evacuateBurningNestBox(box, worldRef, "nest-box-fire-evacuate");
|
||||
}
|
||||
if (box.nestFireEffectCooldown <= 0) {
|
||||
box.nestFireEffectCooldown = NEST_BOX_FIRE_EFFECT_INTERVAL;
|
||||
pushFireEffect(worldRef, box.x, box.y - (box.r || 42) * 0.35, Math.max(13, (box.r || 42) * 0.40), box);
|
||||
}
|
||||
if (box.burnTimer <= 0.01) {
|
||||
burnOutNestBox(box, worldRef, "burnout");
|
||||
return true;
|
||||
}
|
||||
worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +324,6 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
zunchi.stageTimer = Math.max(0, Number(zunchi.stageTimer || 0) + dt * 2.4);
|
||||
if (!wasBurning) {
|
||||
pushFireEffect(worldRef, zunchi.x, zunchi.y - Math.max(5, (zunchi.r || 14) * 0.2), Math.max(10, zunchi.r || 14), zunchi);
|
||||
worldRef?.emit?.("fire:ignite-zunchi", { item: zunchi, source });
|
||||
}
|
||||
if ((zunchi.amount || 0) <= 0.5) zunchi.amount = 0;
|
||||
worldRef?.markTerrainDirtyAt?.(zunchi.x, zunchi.y, Math.max(zunchi.r || 15, 28), "zunchi-fire");
|
||||
|
|
@ -199,7 +339,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
const spreadRadius = r + 16;
|
||||
let changed = false;
|
||||
for (const t of worldRef.nearbyTarinai?.(item.x, item.y, spreadRadius + 32, true) || []) {
|
||||
if (!t || t.dead) continue;
|
||||
if (!t || t.dead || t.insideNestBoxId || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const d = distXY(item.x, item.y, t.x, t.y);
|
||||
if (d <= spreadRadius + Math.max(t.radius || 18, 12) * 0.72) changed = igniteTarinaiByFire(t, item, worldRef, false) || changed;
|
||||
}
|
||||
|
|
@ -217,6 +357,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
if (!extinguished && (item.amount || 0) > 0) changed = igniteGrassByFire(it, item, worldRef, dt) || changed;
|
||||
}
|
||||
if (it.type === "zunchi" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 15, 12)) changed = igniteZunchiByFire(it, item, worldRef, dt) || changed;
|
||||
if (it.type === "nest_box" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 42, 28) * 1.15) changed = igniteNestBoxByFire(it, item, worldRef) || changed;
|
||||
if (it.type === "water" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 10, 8)) {
|
||||
consumeWaterForFire(worldRef, it, Math.max(6, r * 0.45));
|
||||
item.amount = 0;
|
||||
|
|
@ -230,7 +371,7 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
if (!item || !worldRef) return;
|
||||
const r = Math.max(170, (item.r || 14) + 170);
|
||||
for (const t of worldRef.nearbyTarinai?.(item.x, item.y, r, true) || []) {
|
||||
if (!t || t.dead) continue;
|
||||
if (!t || t.dead || t.insideNestBoxId || worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
panicTarinaiFromFire(t, item, worldRef);
|
||||
}
|
||||
}
|
||||
|
|
@ -315,8 +456,11 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
let best = null;
|
||||
let bestD = Infinity;
|
||||
const exclude = opts.exclude || null;
|
||||
for (const fire of fires) {
|
||||
if (!fire || fire.dead) continue;
|
||||
const searchRadius = Math.max(96, Number(opts.mergeRadius || opts.radius || 48) * 5.0);
|
||||
const candidates = typeof worldRef?.nearbyItems === "function" ? (worldRef.nearbyItems(x, y, searchRadius, true) || []) : fires;
|
||||
const scan = candidates.length ? candidates : fires;
|
||||
for (const fire of scan) {
|
||||
if (!fire || fire.dead || fire.type !== "fire") continue;
|
||||
if (exclude && fire === exclude) continue;
|
||||
const d = distXY(x, y, fire.x, fire.y);
|
||||
if (d < bestD) { best = fire; bestD = d; }
|
||||
|
|
@ -460,10 +604,14 @@ function updateFirecracker(item, dt, worldRef) {
|
|||
fireTempMultiplier,
|
||||
contactWaterDrop,
|
||||
consumeWaterForFire,
|
||||
igniteTarinaiByFire,
|
||||
panicTarinaiFromFire,
|
||||
igniteAntByFire,
|
||||
igniteGrassByFire,
|
||||
igniteNestBoxByFire,
|
||||
extinguishNestBoxFire,
|
||||
updateNestBoxFire,
|
||||
burnOutNestBox,
|
||||
nestBoxFireActive,
|
||||
spawnFireAt: (worldRef, x, y, opts = {}) => spawnFireAt(worldRef, x, y, opts),
|
||||
scatterFireFrom,
|
||||
compactWorldFires,
|
||||
|
|
|
|||
|
|
@ -145,6 +145,20 @@
|
|||
&& num(y) >= num(rect.top) - pad && num(y) <= num(rect.bottom) + pad;
|
||||
}
|
||||
|
||||
function circleOverlapsRect(x, y, radius, rect, padding = 0) {
|
||||
if (!rect) return false;
|
||||
const rr = Math.max(0, num(radius)) + Math.max(0, num(padding));
|
||||
if (rect.oriented) {
|
||||
const local = rectLocalPoint(rect, x, y);
|
||||
const clx = clampValue(local.x, -num(rect.halfW), num(rect.halfW));
|
||||
const cly = clampValue(local.y, -num(rect.halfH), num(rect.halfH));
|
||||
return Math.hypot(local.x - clx, local.y - cly) < rr;
|
||||
}
|
||||
const cx = clampValue(num(x), num(rect.left), num(rect.right));
|
||||
const cy = clampValue(num(y), num(rect.top), num(rect.bottom));
|
||||
return Math.hypot(num(x) - cx, num(y) - cy) < rr;
|
||||
}
|
||||
|
||||
function pointSegmentDistance(px, py, ax, ay, bx, by) {
|
||||
const dx = num(bx) - num(ax);
|
||||
const dy = num(by) - num(ay);
|
||||
|
|
@ -194,6 +208,7 @@
|
|||
rectOverlapInfo,
|
||||
rectsOverlap,
|
||||
pointInRect,
|
||||
circleOverlapsRect,
|
||||
pointSegmentDistance,
|
||||
segmentAabbHit,
|
||||
segmentIntersectsOrientedRect,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
const GROUND_TYPE_ORDER = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
|
||||
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 }),
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@ const HEALTH = (() => {
|
|||
}
|
||||
|
||||
if (direct === CAUSES.fire || recent === CAUSES.fire || t.lastMajorDamageCause === CAUSES.fire) return CAUSES.fire;
|
||||
if (direct === CAUSES.heatstroke || recent === CAUSES.heatstroke || t.lastMajorDamageCause === CAUSES.heatstroke) return CAUSES.heatstroke;
|
||||
if (direct === CAUSES.freeze || recent === CAUSES.freeze || t.lastMajorDamageCause === CAUSES.freeze) return CAUSES.freeze;
|
||||
if (recentMajor && t.lastMajorDamageCause === CAUSES.collision && !(t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) return CAUSES.collision;
|
||||
if (hasLingeringMajorDamage(t) && (!direct || direct === t.lastMajorDamageCause || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) {
|
||||
return weakenedDeathReasonFor(t.lastMajorDamageCause);
|
||||
|
|
@ -295,8 +297,7 @@ const HEALTH = (() => {
|
|||
let cause = "";
|
||||
if (actualLoss > 0.01) {
|
||||
cause = rememberDamage(t, actualLoss, reason);
|
||||
t.world?.emit?.("tarinai:damaged", { tarinai: t, amount: actualLoss, cause, reason });
|
||||
const plushie = (t.world?.itemsOfType?.("plushie") || t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id);
|
||||
const plushie = (t.world?.ownedItemsFor?.(t.id) || t.world?.itemsOfType?.("plushie") || t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id);
|
||||
if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null);
|
||||
if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure();
|
||||
const majorDamageReacted = maybeTriggerMajorDamageReaction(t, actualLoss, cause || reason);
|
||||
|
|
|
|||
|
|
@ -19,13 +19,96 @@
|
|||
|
||||
function snapshotKey(snapshot) {
|
||||
try {
|
||||
const clone = cloneSnapshot(snapshot);
|
||||
return JSON.stringify(clone);
|
||||
// Snapshot payloads are produced as plain serializable data; stringify them
|
||||
// directly for duplicate suppression instead of deep-cloning once just to
|
||||
// compute the history key. The stored history entry is still cloned below.
|
||||
return JSON.stringify(snapshot);
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const DELETE_MARK = Object.freeze({ __delete: true });
|
||||
const HISTORY_FULL_INTERVAL = 6;
|
||||
const HISTORY_DELTA_MIN_SAVING = 0.72;
|
||||
|
||||
function isPlainObject(value) {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
return proto === Object.prototype || proto === null;
|
||||
}
|
||||
|
||||
function diffValue(prev, next) {
|
||||
if (Object.is(prev, next)) return undefined;
|
||||
if (Array.isArray(prev) || Array.isArray(next)) {
|
||||
const a = JSON.stringify(prev);
|
||||
const b = JSON.stringify(next);
|
||||
return a === b ? undefined : cloneSnapshot(next);
|
||||
}
|
||||
if (!isPlainObject(prev) || !isPlainObject(next)) return cloneSnapshot(next);
|
||||
const patch = {};
|
||||
const keys = new Set([...Object.keys(prev), ...Object.keys(next)]);
|
||||
for (const key of keys) {
|
||||
if (!(key in next)) {
|
||||
patch[key] = DELETE_MARK;
|
||||
continue;
|
||||
}
|
||||
const child = diffValue(prev[key], next[key]);
|
||||
if (child !== undefined) patch[key] = child;
|
||||
}
|
||||
return Object.keys(patch).length ? patch : undefined;
|
||||
}
|
||||
|
||||
function applyPatch(base, patch) {
|
||||
if (patch === undefined) return cloneSnapshot(base);
|
||||
if (!isPlainObject(patch) || Array.isArray(patch)) return cloneSnapshot(patch);
|
||||
const out = isPlainObject(base) ? cloneSnapshot(base) : {};
|
||||
for (const [key, value] of Object.entries(patch)) {
|
||||
if (value && value.__delete === true && Object.keys(value).length === 1) {
|
||||
delete out[key];
|
||||
continue;
|
||||
}
|
||||
out[key] = isPlainObject(value) && isPlainObject(out[key]) ? applyPatch(out[key], value) : cloneSnapshot(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function materializeHistoryEntry(stack, entry) {
|
||||
if (!entry) return null;
|
||||
if (entry.snapshot) return cloneSnapshot(entry.snapshot);
|
||||
const sequence = [...(stack || []), entry];
|
||||
let start = -1;
|
||||
for (let i = sequence.length - 1; i >= 0; i -= 1) {
|
||||
if (sequence[i]?.snapshot) { start = i; break; }
|
||||
}
|
||||
if (start < 0) return null;
|
||||
let snap = cloneSnapshot(sequence[start].snapshot);
|
||||
for (let i = start + 1; i < sequence.length; i += 1) {
|
||||
const patch = sequence[i]?.patch;
|
||||
if (patch !== undefined) snap = applyPatch(snap, patch);
|
||||
}
|
||||
return snap;
|
||||
}
|
||||
|
||||
function makeHistoryEntry(world, snap, key, label, stack) {
|
||||
const fullEntry = { snapshot: cloneSnapshot(snap), key, label: String(label || "action"), time: world.time || 0 };
|
||||
const stackLen = Array.isArray(stack) ? stack.length : 0;
|
||||
if (stackLen <= 0 || stackLen % HISTORY_FULL_INTERVAL === 0) return fullEntry;
|
||||
const prev = materializeHistoryEntry(stack.slice(0, -1), stack[stack.length - 1]);
|
||||
if (!prev) return fullEntry;
|
||||
const patch = diffValue(prev, snap);
|
||||
if (patch === undefined) return { patch: {}, key, label: String(label || "action"), time: world.time || 0 };
|
||||
try {
|
||||
const fullSize = JSON.stringify(fullEntry.snapshot).length;
|
||||
const patchSize = JSON.stringify(patch).length;
|
||||
if (patchSize > fullSize * HISTORY_DELTA_MIN_SAVING) return fullEntry;
|
||||
} catch (_) {
|
||||
return fullEntry;
|
||||
}
|
||||
return { patch, key, label: String(label || "action"), time: world.time || 0 };
|
||||
}
|
||||
|
||||
function capture(worldRef = global.world, label = "action") {
|
||||
const world = ensure(worldRef);
|
||||
if (!world || world._historyRestoring) return false;
|
||||
|
|
@ -33,18 +116,19 @@
|
|||
if (!snap) return false;
|
||||
const key = snapshotKey(snap);
|
||||
if (key && key === world._lastUndoSnapshotKey) return false;
|
||||
world._undoStack.push({ snapshot: cloneSnapshot(snap), key, label: String(label || "action"), time: world.time || 0 });
|
||||
world._undoStack.push(makeHistoryEntry(world, snap, key, label, world._undoStack));
|
||||
if (world._undoStack.length > MAX_HISTORY) world._undoStack.splice(0, world._undoStack.length - MAX_HISTORY);
|
||||
world._redoStack.length = 0;
|
||||
world._lastUndoSnapshotKey = key;
|
||||
return true;
|
||||
}
|
||||
|
||||
function restore(worldRef, entry) {
|
||||
if (!worldRef || !entry?.snapshot) return false;
|
||||
function restore(worldRef, entry, stack = []) {
|
||||
const snapshot = materializeHistoryEntry(stack, entry);
|
||||
if (!worldRef || !snapshot) return false;
|
||||
worldRef._historyRestoring = true;
|
||||
try {
|
||||
global.TarinaiRestoreCoordinator.restoreSnapshot(cloneSnapshot(entry.snapshot), worldRef, { syncUi: true });
|
||||
global.TarinaiRestoreCoordinator.restoreSnapshot(snapshot, worldRef, { syncUi: true });
|
||||
worldRef._lastUndoSnapshotKey = snapshotKey(global.TarinaiSnapshot.createSnapshot(worldRef));
|
||||
return true;
|
||||
} finally {
|
||||
|
|
@ -57,9 +141,9 @@
|
|||
if (!world || !world._undoStack.length) return { ok: false, reason: "empty" };
|
||||
const current = global.TarinaiSnapshot.createSnapshot(world);
|
||||
const prev = world._undoStack.pop();
|
||||
if (current) world._redoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "redo", time: world.time || 0 });
|
||||
if (current) world._redoStack.push(makeHistoryEntry(world, current, snapshotKey(current), "redo", world._redoStack));
|
||||
if (world._redoStack.length > MAX_HISTORY) world._redoStack.splice(0, world._redoStack.length - MAX_HISTORY);
|
||||
const ok = restore(world, prev);
|
||||
const ok = restore(world, prev, world._undoStack);
|
||||
return { ok, label: prev.label || "" };
|
||||
}
|
||||
|
||||
|
|
@ -68,9 +152,9 @@
|
|||
if (!world || !world._redoStack.length) return { ok: false, reason: "empty" };
|
||||
const current = global.TarinaiSnapshot.createSnapshot(world);
|
||||
const next = world._redoStack.pop();
|
||||
if (current) world._undoStack.push({ snapshot: cloneSnapshot(current), key: snapshotKey(current), label: "undo", time: world.time || 0 });
|
||||
if (current) world._undoStack.push(makeHistoryEntry(world, current, snapshotKey(current), "undo", world._undoStack));
|
||||
if (world._undoStack.length > MAX_HISTORY) world._undoStack.splice(0, world._undoStack.length - MAX_HISTORY);
|
||||
const ok = restore(world, next);
|
||||
const ok = restore(world, next, world._redoStack);
|
||||
return { ok, label: next.label || "" };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -93,9 +93,11 @@
|
|||
const items = worldRef.nearbyItems(item.x, item.y, searchRadius) || [];
|
||||
const seen = new Set(items);
|
||||
// 大きな機構は中心が近傍セル外でも当たる可能性がある。
|
||||
// 中心ではなく外接半径で追加走査して、長い棒・往復幅にも衝突させる。
|
||||
if (Array.isArray(worldRef.items)) {
|
||||
for (const it of worldRef.items) {
|
||||
// 全item走査ではなく、機構type bucketだけを追加確認する。
|
||||
const mechanicalTypes = ["rotator", "poison_block", "reciprocator"];
|
||||
for (const type of mechanicalTypes) {
|
||||
const bucket = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType(type) : (worldRef.items || []);
|
||||
for (const it of bucket || []) {
|
||||
if (!it || it.dead || it === item || !global.TarinaiMechanicalSystem.isMechanicalType(it.type) || seen.has(it)) continue;
|
||||
const reach = global.TarinaiMechanicalSystem.reach(it) || worldRef.rotatorExtent?.(it) || Math.max(64, it.r || 64);
|
||||
if (distXY(item.x, item.y, it.x, it.y) <= searchRadius + reach + 36) {
|
||||
|
|
@ -258,6 +260,30 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
const FAN_WIND_ITEM_TYPES = Object.freeze(["balloon", "ball", "pushpin", "oshibyo", "water", "zunda_juice", "mercury", "zunchi", "fire"]);
|
||||
|
||||
function fanWindItemCandidates(fan, worldRef, cx, cy, radius) {
|
||||
if (!worldRef) return [];
|
||||
if (typeof worldRef.itemsOfType === "function") {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const r2 = Math.max(1, Number(radius || 0) || 0) ** 2;
|
||||
for (const type of FAN_WIND_ITEM_TYPES) {
|
||||
for (const it of worldRef.itemsOfType(type) || []) {
|
||||
if (!it || it.dead || it === fan || seen.has(it.id || it)) continue;
|
||||
const dx = Number(it.x || 0) - cx;
|
||||
const dy = Number(it.y || 0) - cy;
|
||||
const reach = radius + Math.max(0, Number(it.r || it.radius || 0) || 0);
|
||||
if (dx * dx + dy * dy > Math.max(r2, reach * reach)) continue;
|
||||
seen.add(it.id || it);
|
||||
out.push(it);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return worldRef.nearbyItems?.(cx, cy, radius) || worldRef.items || [];
|
||||
}
|
||||
|
||||
function fanWindAt(fan, x, y, opts = {}) {
|
||||
if (!fan || fan.dead || fan.type !== "fan") return null;
|
||||
const range = Math.max(80, Number(opts.range || 300) || 300);
|
||||
|
|
@ -295,7 +321,9 @@
|
|||
const ay = Math.sin(angle);
|
||||
const range = 320;
|
||||
const searchRadius = range + 40;
|
||||
const nearbyItems = worldRef?.nearbyItems?.(item.x + ax * range * 0.45, item.y + ay * range * 0.45, searchRadius) || [];
|
||||
const queryX = item.x + ax * range * 0.45;
|
||||
const queryY = item.y + ay * range * 0.45;
|
||||
const nearbyItems = fanWindItemCandidates(item, worldRef, queryX, queryY, searchRadius);
|
||||
for (const target of nearbyItems) {
|
||||
if (!target || target.dead || target === item) continue;
|
||||
const wind = fanWindAt(item, target.x, target.y, { range });
|
||||
|
|
@ -303,7 +331,7 @@
|
|||
applyFanToItem(item, target, wind, frameDt, worldRef);
|
||||
}
|
||||
|
||||
const nearbyTarinai = worldRef?.nearbyTarinai?.(item.x + ax * range * 0.45, item.y + ay * range * 0.45, searchRadius) || [];
|
||||
const nearbyTarinai = worldRef?.nearbyTarinai?.(queryX, queryY, searchRadius) || [];
|
||||
for (const t of nearbyTarinai) {
|
||||
if (!t || t.dead || worldRef?.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const wind = fanWindAt(item, t.x, t.y, { range });
|
||||
|
|
@ -616,7 +644,6 @@
|
|||
update,
|
||||
updateFan,
|
||||
updateWaterDropMotion,
|
||||
fanWindAt,
|
||||
resolveObstacleCollisions,
|
||||
popBalloon,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
case "sticky_bomb":
|
||||
case "fire":
|
||||
case "genkotsu":
|
||||
case "nest_box":
|
||||
case "rotator":
|
||||
case "poison_block":
|
||||
case "reciprocator":
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
}
|
||||
|
||||
|
||||
const STICKY_BOMB_PANIC_TEXT = "くっつきボムが付いてパニックになっている。";
|
||||
const STICKY_BOMB_PANIC_TEXT = "粘着ボムが付いてパニックになっている。";
|
||||
const STICKY_BOMB_FUSE_SECONDS = 12.0;
|
||||
|
||||
function tarinaiById(worldRef, id = "") {
|
||||
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === id) || null;
|
||||
|
|
@ -81,7 +82,7 @@
|
|||
panicStickyBombCarrier(carrier, item, worldRef, true);
|
||||
if (old && old !== carrier) {
|
||||
old.surpriseTimer = Math.max(old.surpriseTimer || 0, 0.18);
|
||||
old.thought = "くっつきボムが移った";
|
||||
old.thought = "粘着ボムが移った";
|
||||
}
|
||||
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)" }));
|
||||
|
|
@ -102,14 +103,14 @@
|
|||
item.amount = 0;
|
||||
item.hp = 0;
|
||||
audio.explode?.();
|
||||
worldRef?.log?.("くっつきボムが爆発した。", "accident");
|
||||
worldRef?.log?.("粘着ボムが爆発した。", "accident");
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateStickyBomb(item, dt, worldRef) {
|
||||
if (!item || item.dead || item.type !== "sticky_bomb") return false;
|
||||
const step = Math.max(0.016, Number(dt || 0.016) || 0.016);
|
||||
item.fuseTimer = Math.max(0, Number(item.fuseTimer ?? 6) - step);
|
||||
item.fuseTimer = Math.max(0, Number(item.fuseTimer ?? STICKY_BOMB_FUSE_SECONDS) - step);
|
||||
item.stickyBombTransferCooldown = Math.max(0, Number(item.stickyBombTransferCooldown || 0) - step);
|
||||
item.spin = (Number(item.spin || 0) || 0) + (Number(item.spinVelocity || 0) || 0) * step;
|
||||
if (item.fuseTimer <= 0) return explodeStickyBomb(item, worldRef);
|
||||
|
|
@ -154,7 +155,7 @@
|
|||
}
|
||||
if (best) attachStickyBomb(item, best, worldRef, "attach");
|
||||
}
|
||||
if (worldRef?.effects && typeof Effect !== "undefined" && item.fuseTimer < Math.max(1.2, (item.fuseMax || 6) * 0.35) && deterministicChance(worldRef, "sticky-bomb-spark", step * 5.8, item, Math.floor((worldRef.time || 0) * 20))) {
|
||||
if (worldRef?.effects && typeof Effect !== "undefined" && item.fuseTimer < Math.max(1.2, (item.fuseMax || STICKY_BOMB_FUSE_SECONDS) * 0.35) && deterministicChance(worldRef, "sticky-bomb-spark", step * 5.8, item, Math.floor((worldRef.time || 0) * 20))) {
|
||||
worldRef.effects.push(new Effect("fight", item.x, item.y - (item.r || 16) * 0.55, { size: 4 + Math.random() * 4, life: 0.16, color: "rgba(255,224,92,0.78)", vy: -20 }));
|
||||
}
|
||||
worldRef && (worldRef.drawListDirty = true);
|
||||
|
|
@ -184,6 +185,7 @@
|
|||
if (item.type === "flame_firecracker") return fireApi().updateFlameFirecracker(item, dt, worldRef);
|
||||
if (item.type === "sticky_bomb") return updateStickyBomb(item, dt, worldRef);
|
||||
if (item.type === "fire") return fireApi().updateFire(item, dt, worldRef);
|
||||
if (item.type === "nest_box") return fireApi().updateNestBoxFire(item, dt, worldRef);
|
||||
if (item.type === "genkotsu") return updateGenkotsu(item, dt, worldRef);
|
||||
if (item.type === "rotator") return updateRotator(item, dt, worldRef);
|
||||
if (item.type === "poison_block") return updatePoisonBlock(item, dt, worldRef);
|
||||
|
|
@ -217,9 +219,12 @@
|
|||
panicTarinaiFromFire: (...args) => fireApi().panicTarinaiFromFire(...args),
|
||||
igniteAntByFire: (...args) => fireApi().igniteAntByFire(...args),
|
||||
igniteGrassByFire: (...args) => fireApi().igniteGrassByFire(...args),
|
||||
igniteNestBoxByFire: (...args) => fireApi().igniteNestBoxByFire(...args),
|
||||
extinguishNestBoxFire: (...args) => fireApi().extinguishNestBoxFire(...args),
|
||||
updateNestBoxFire: (...args) => fireApi().updateNestBoxFire(...args),
|
||||
nestBoxFireActive: (...args) => fireApi().nestBoxFireActive(...args),
|
||||
spawnFireAt: (...args) => fireApi().spawnFireAt(...args),
|
||||
scatterFireFrom: (...args) => fireApi().scatterFireFrom(...args),
|
||||
updateStickyBomb,
|
||||
compactWorldFires: (...args) => fireApi().compactWorldFires(...args),
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -93,9 +93,15 @@
|
|||
color: "rgba(236,46,24,0.72)",
|
||||
}));
|
||||
}
|
||||
if (item.burnTimer <= 0.01) item.burning = false;
|
||||
if ((item.amount || 0) <= 0.5) item.amount = 0;
|
||||
if (item.burnTimer <= 0.01 || (item.amount || 0) <= 0.5) {
|
||||
item.burning = false;
|
||||
item.burnTimer = 0;
|
||||
item.amount = 0;
|
||||
worldRef?.effects?.push(new Effect("zunchi_miasma", item.x, item.y - 4, { size: Math.max(13, (item.r || 15) * 1.25), life: 0.42, color: "rgba(64,44,34,0.44)" }));
|
||||
}
|
||||
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || 15, 28), "zunchi-burning");
|
||||
worldRef?.markItemBucketsDirty?.("zunchi-burning");
|
||||
worldRef?.markSpatialDirty?.("zunchi-burning");
|
||||
worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -133,8 +139,16 @@
|
|||
item.fertility = clamp(1 - item.stageTimer / 360, 0, 1);
|
||||
item.amount -= decayDt * 0.090 * decayBoost;
|
||||
}
|
||||
if ((item.amount || 0) <= 0.5) {
|
||||
item.amount = 0;
|
||||
worldRef?.effects?.push(new Effect("zunchi_miasma", item.x, item.y - 4, { size: Math.max(12, (item.r || 15) * 1.15), life: 0.34, color: "rgba(64,44,34,0.34)" }));
|
||||
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || 15, 28), "zunchi-decay-out");
|
||||
worldRef?.markItemBucketsDirty?.("zunchi-decay-out");
|
||||
worldRef?.markSpatialDirty?.("zunchi-decay-out");
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
global.TarinaiItemDynamicZunchiSystem = Object.freeze({ updateMotion, updateLifecycle, updateBurningLifecycle });
|
||||
global.TarinaiItemDynamicZunchiSystem = Object.freeze({ updateMotion, updateLifecycle });
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
if (worldRef) worldRef.drawListDirty = true;
|
||||
}
|
||||
worldRef?.markTerrainDirty?.("food-passive-decay");
|
||||
worldRef?.emit?.("item:decayed", { item, type: item.type, amount: lost, passive: true });
|
||||
if (item.foodServingsRemaining <= 0.015) item.amount = 0;
|
||||
}
|
||||
} else if (MEDICINE_LIKE_FOOD_TYPES.includes(item.type)) {
|
||||
|
|
@ -43,7 +42,6 @@
|
|||
if (lost > 0) {
|
||||
changed = true;
|
||||
worldRef?.markTerrainDirty?.("food-passive-decay");
|
||||
worldRef?.emit?.("item:decayed", { item, type: item.type, amount: lost, passive: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,11 @@
|
|||
item.lifecycleTimer += dt;
|
||||
const interval = item.lifecycleInterval || (0.95 + stableUnit(item.id, "zunchi-life") * 0.45);
|
||||
if (item.lifecycleTimer < interval) return false;
|
||||
const lifecycleDt = Math.min(item.lifecycleTimer, 2.4);
|
||||
item.lifecycleTimer = 0;
|
||||
const groundId = worldRef?.groundType || "soil";
|
||||
const groundDecayMul = Number(global.TarinaiGround?.zunchiDecayMultiplier?.(groundId) || 1) || 1;
|
||||
const maxLifecycleDt = groundDecayMul >= 100 ? 0.35 : 2.4;
|
||||
const lifecycleDt = Math.min(item.lifecycleTimer, maxLifecycleDt);
|
||||
item.lifecycleTimer = Math.max(0, item.lifecycleTimer - lifecycleDt);
|
||||
global.TarinaiItemDynamicZunchiSystem.updateLifecycle(item, lifecycleDt, worldRef);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ function duplicatorSetStoredType(duplicator, storedType = "", worldRef = null, o
|
|||
worldRef?.markItemBucketsDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
worldRef?.markSpatialDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
worldRef?.markTerrainDirty?.(changed ? "duplicator-direct-reloaded" : "duplicator-direct-loaded");
|
||||
if (options.emit !== false) worldRef?.emit?.("duplicator:loaded", { duplicator, foodType: loadType, replaced: changed, direct: Boolean(options.direct) });
|
||||
if (options.log !== false) worldRef?.log?.(`\u8907\u88fd\u6a5f\u306b${duplicator.storedFoodLabel}\u3092\u30bb\u30c3\u30c8\u3057\u305f\u3002`, isPinType(loadType) ? "observe" : "food");
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,6 +309,21 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.beginPath();
|
||||
ctx.arc(0, -this.r * 0.08, this.r * 0.40, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (this.burning || (this.burnTimer || 0) > 0.02 || (this.nestFireEvacuationTimer || 0) > 0.02) {
|
||||
const phase = t * 7.4 + this.seed;
|
||||
ctx.save();
|
||||
ctx.globalAlpha *= 0.78;
|
||||
if (typeof drawSimpleFireShape === "function") {
|
||||
drawSimpleFireShape(ctx, -this.r * 0.42, -this.r * 0.64, this.r * 0.38, { phase, shadow: itemShadowsEnabled() });
|
||||
drawSimpleFireShape(ctx, this.r * 0.38, -this.r * 0.56, this.r * 0.32, { phase: phase + 1.7, shadow: false });
|
||||
} else {
|
||||
ctx.fillStyle = "rgba(236,46,24,0.78)";
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-this.r * 0.35, -this.r * 0.58, this.r * 0.18, this.r * 0.36, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
} else if (this.type === "pipe") {
|
||||
const r = this.r || 36;
|
||||
ctx.save();
|
||||
|
|
@ -929,7 +944,7 @@ draw(ctx, t, lighting = null) {
|
|||
ctx.restore();
|
||||
} else if (this.type === "sticky_bomb") {
|
||||
const r = this.r || 16;
|
||||
const fuse = clamp((this.fuseTimer ?? 6) / Math.max(this.fuseMax || 6, 0.1), 0, 1);
|
||||
const fuse = clamp((this.fuseTimer ?? 12) / Math.max(this.fuseMax || 12, 0.1), 0, 1);
|
||||
const flash = fuse < 0.34 ? (0.5 + Math.sin(t * 24 + this.seed) * 0.5) : 0;
|
||||
ctx.save();
|
||||
ctx.rotate((this.spin || 0) * 0.18 + Math.sin(t * 2.0 + this.seed) * 0.05);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
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, iconText: "\u26bd", collisionShape: "circle", tooltip: "\u305f\u308a\u306a\u3044\u304c\u904a\u3076\u305f\u3081\u306e\u304a\u3082\u3061\u3083\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で置く向きを変える。配置後にクリックすると首振り設定を開く。" },
|
||||
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" },
|
||||
|
|
@ -52,7 +52,7 @@ const TOOL_DEFINITIONS = Object.freeze({
|
|||
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: "\u304f\u3063\u3064\u304d\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" },
|
||||
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" },
|
||||
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" },
|
||||
|
|
@ -112,6 +112,12 @@ function itemHasTrait(type = "", trait = "") {
|
|||
function isPinType(type = "") { return itemHasTrait(type, "pin"); }
|
||||
function isSleepFurnitureType(type = "") { return itemHasTrait(type, "sleepFurniture"); }
|
||||
function isNestContainerType(type = "") { const id = String(type || ""); return id === "nest_box" || id === "pipe"; }
|
||||
function isNestContainerItem(item, options = {}) {
|
||||
if (!item) return false;
|
||||
const requireAlive = options?.alive !== false;
|
||||
if (requireAlive && item.dead) return false;
|
||||
return isNestContainerType(item.type);
|
||||
}
|
||||
function isLodgedPin(item = null) { return Boolean(item && !item.dead && isPinType(item.type) && item.pinState === "lodged"); }
|
||||
function lodgedPinFor(tarinai = null, worldRef = null) {
|
||||
if (!tarinai?.stuckPushpinId) return null;
|
||||
|
|
@ -175,6 +181,42 @@ function toolTipsFromDefinitions() {
|
|||
return Object.fromEntries(Object.entries(TOOL_DEFINITIONS).filter(([, def]) => def.tooltip).map(([id, def]) => [id, def.tooltip]));
|
||||
}
|
||||
|
||||
function toolItemTypeIds() {
|
||||
return Object.values(TOOL_DEFINITIONS)
|
||||
.map(def => def?.itemType || (def?.placeable ? def.id : ""))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
const ToolRuntime = Object.freeze({
|
||||
TOOL_DEFINITIONS,
|
||||
isFenceItemType,
|
||||
isRotatableItemType,
|
||||
defaultItemAngle,
|
||||
normalizedItemAngle,
|
||||
itemAngleFor,
|
||||
itemHasTrait,
|
||||
isPinType,
|
||||
isSleepFurnitureType,
|
||||
isNestContainerType,
|
||||
isNestContainerItem,
|
||||
lodgedPinFor,
|
||||
hasLodgedPinEffect,
|
||||
pinBehaviorFor,
|
||||
toolCategories,
|
||||
toolDefinition,
|
||||
toolLabel,
|
||||
toolItemType,
|
||||
itemDefinition,
|
||||
itemRadiusFor,
|
||||
itemAmountFor,
|
||||
normalizedToolSizeFor,
|
||||
toolSizeScaleFor,
|
||||
scalableToolIds,
|
||||
toolTipsFromDefinitions,
|
||||
toolItemTypeIds,
|
||||
});
|
||||
global.TarinaiToolRuntime = ToolRuntime;
|
||||
|
||||
Object.assign(global, {
|
||||
isFenceItemType,
|
||||
|
|
@ -196,12 +238,11 @@ function toolTipsFromDefinitions() {
|
|||
itemDefinition,
|
||||
itemRadiusFor,
|
||||
itemAmountFor,
|
||||
TOOL_SIZE_ORDER,
|
||||
TOOL_SIZE_LABELS,
|
||||
normalizedToolSizeFor,
|
||||
toolSizeScaleFor,
|
||||
scalableToolIds,
|
||||
toolTipsFromDefinitions,
|
||||
toolItemTypeIds,
|
||||
});
|
||||
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -48,9 +48,4 @@
|
|||
TOOL_SIZE_LABELS,
|
||||
TOOL_SIZE_SCALES,
|
||||
});
|
||||
|
||||
Object.assign(global, {
|
||||
TOOL_SIZE_ORDER,
|
||||
TOOL_SIZE_LABELS,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
50
js/item_type_catalog.js
Normal file
50
js/item_type_catalog.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: data/item-type-catalog
|
||||
// Builds the current save item-type table from live tool, structure, and link definitions.
|
||||
// Save compatibility is intentionally not preserved; current encode/decode only need to agree
|
||||
// within the same app build.
|
||||
(function (global) {
|
||||
const DomainIds = global.TarinaiDomainIds;
|
||||
if (!DomainIds) throw new Error("TarinaiDomainIds must be loaded before item_type_catalog.js");
|
||||
|
||||
function addOrdered(out, seen, values = []) {
|
||||
for (const value of values || []) {
|
||||
const id = String(value || "").trim();
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
function buildItemTypeIds() {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
const structureTypes = Object.keys(global.StructureRegistry?.defs || {});
|
||||
const toolTypes = typeof global.toolItemTypeIds === "function" ? global.toolItemTypeIds() : [];
|
||||
const linkToolTypes = global.TarinaiItemToolMetadata?.PHYSICS_TOOL_IDS || [];
|
||||
addOrdered(out, seen, structureTypes);
|
||||
addOrdered(out, seen, toolTypes);
|
||||
addOrdered(out, seen, linkToolTypes);
|
||||
return Object.freeze(out);
|
||||
}
|
||||
|
||||
const ITEM_TYPE_IDS = buildItemTypeIds();
|
||||
const ITEM_TYPE_INDEX = Object.freeze(Object.fromEntries(ITEM_TYPE_IDS.map((id, index) => [id, index])));
|
||||
|
||||
function itemTypeIndex(value = "", fallback = -1) {
|
||||
const index = ITEM_TYPE_INDEX[String(value ?? "")];
|
||||
return Number.isInteger(index) ? index : fallback;
|
||||
}
|
||||
|
||||
function itemTypeValue(index = 0, fallback = "") {
|
||||
return DomainIds.enumValue(ITEM_TYPE_IDS, index, fallback);
|
||||
}
|
||||
|
||||
global.TarinaiItemTypeCatalog = Object.freeze({
|
||||
ITEM_TYPE_IDS,
|
||||
ITEM_TYPE_INDEX,
|
||||
itemTypeIndex,
|
||||
itemTypeValue,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
@ -113,8 +113,8 @@ function initializeItemTypeState(item, type, x, y) {
|
|||
item.fuseMax = 5;
|
||||
}
|
||||
if (type === "sticky_bomb") {
|
||||
item.fuseTimer = 6.0;
|
||||
item.fuseMax = 6.0;
|
||||
item.fuseTimer = 12.0;
|
||||
item.fuseMax = 12.0;
|
||||
item.vx = rand(-8, 8);
|
||||
item.vy = rand(-8, 8);
|
||||
item.prevX = x;
|
||||
|
|
|
|||
|
|
@ -35,8 +35,18 @@
|
|||
if (fireCount > 36) return mobile ? 0.42 : 0.25;
|
||||
return mobile ? 0.30 : 0.18;
|
||||
}
|
||||
if (item.type === "nest_box" && global.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(item)) return mobile ? 0.30 : 0.18;
|
||||
if ((item.dropTimer || 0) > 0 || Math.hypot(item.vx || 0, item.vy || 0) > 0.08) return 0;
|
||||
if (item.type === "zunchi") return 2.4;
|
||||
if (item.type === "zunchi" && (item.burning || (item.burnTimer || 0) > 0.02)) return mobile ? 0.30 : 0.18;
|
||||
if (item.type === "zunchi") {
|
||||
const groundId = item.world?.groundType || (typeof world !== "undefined" ? world?.groundType : "") || "soil";
|
||||
const decayMul = Number(global.TarinaiGround?.zunchiDecayMultiplier?.(groundId) || 1) || 1;
|
||||
// High-decay grounds need a shorter update cadence; otherwise the amount
|
||||
// changes only once every few seconds and the laboratory effect looks idle.
|
||||
if (decayMul >= 100) return mobile ? 0.18 : 0.12;
|
||||
if (decayMul > 1.01) return mobile ? 0.42 : 0.28;
|
||||
return 2.4;
|
||||
}
|
||||
if (item.type === "grass") return Infinity;
|
||||
if (item.type === "duplicator") return mobile ? 0.8 : 0.5;
|
||||
if (item.type === "water") return mobile ? 2.4 : 1.8;
|
||||
|
|
|
|||
|
|
@ -116,8 +116,9 @@
|
|||
global.TarinaiItemRuntime.updateOne(item, dt, worldRef);
|
||||
item._schedulerLastAt = now;
|
||||
ran += 1;
|
||||
if (Number.isFinite(itemUpdateInterval(item)) && itemUpdateInterval(item) <= 0) nextRealtime.push(item);
|
||||
else scheduleItem(state, item, now, itemUpdateInterval(item));
|
||||
const nextInterval = itemUpdateInterval(item);
|
||||
if (Number.isFinite(nextInterval) && nextInterval <= 0) nextRealtime.push(item);
|
||||
else scheduleItem(state, item, now, nextInterval);
|
||||
} else {
|
||||
scheduleItem(state, item, now, interval);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -628,8 +628,6 @@ function drawToolItemPreview(ctx, type = "", opts = {}) {
|
|||
|
||||
if (typeof window !== "undefined") {
|
||||
window.isConsumableSpriteType = isConsumableSpriteType;
|
||||
window.foodServingSizeForRemaining = foodServingSizeForRemaining;
|
||||
window.foodServingVisualScaleFor = foodServingVisualScaleFor;
|
||||
window.updateServingFoodVisualSize = updateServingFoodVisualSize;
|
||||
window.drawConsumableFieldSprite = drawConsumableFieldSprite;
|
||||
window.drawStoredConsumableSprite = drawStoredConsumableSprite;
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ loadImages((done, total) => {
|
|||
setLoadingProgress("\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u69cb\u7bc9\u4e2d", 0.72);
|
||||
applyFieldLayout("garden");
|
||||
world.reset(null, "garden");
|
||||
window.bindGroundUI?.(world);
|
||||
window.TarinaiGroundUI?.bind?.(world);
|
||||
selectTool("observe");
|
||||
setLoadingProgress("\u8868\u793a\u3092\u521d\u671f\u5316\u4e2d", 0.84);
|
||||
renderLog(world.logs);
|
||||
|
|
|
|||
|
|
@ -7,17 +7,13 @@
|
|||
function mech() { return global.TarinaiMechanicalSystem || null; }
|
||||
|
||||
function isMechanicalType(itemOrType) { return Boolean(mech()?.isMechanicalType?.(itemOrType)); }
|
||||
function worldSegments(item) { return mech()?.worldSegments?.(item) || []; }
|
||||
function obstacleRects(item) { return mech()?.obstacleRects?.(item) || []; }
|
||||
function boundsAabb(item) { return mech()?.boundsAabb?.(item) || null; }
|
||||
function extent(item) { return mech()?.extent?.(item) || 0; }
|
||||
function reach(item) { return mech()?.reach?.(item) || extent(item); }
|
||||
|
||||
global.TarinaiMechanicalShapeSystem = Object.freeze({
|
||||
isMechanicalType,
|
||||
worldSegments,
|
||||
obstacleRects,
|
||||
boundsAabb,
|
||||
extent,
|
||||
reach,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -262,10 +262,6 @@
|
|||
return value;
|
||||
}
|
||||
|
||||
function orientedRectAabb(cx, cy, halfW, halfH, angle) {
|
||||
return global.TarinaiGeometry.orientedRectAabb(cx, cy, halfW, halfH, angle);
|
||||
}
|
||||
|
||||
function worldSegments(item) {
|
||||
if (!item || item.dead || !isMechanicalType(item)) return [];
|
||||
const cache = geomCache(item);
|
||||
|
|
@ -364,7 +360,7 @@
|
|||
const halfH = visualThickness / 2 + collisionSkin;
|
||||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const aabb = orientedRectAabb(cx, cy, halfW, halfH, angle);
|
||||
const aabb = global.TarinaiGeometry.orientedRectAabb(cx, cy, halfW, halfH, angle);
|
||||
const base = {
|
||||
left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom,
|
||||
cx, cy, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
|
||||
|
|
@ -1282,7 +1278,7 @@
|
|||
const rr = Math.max(8, (Number(t.radius) || 22) * 0.74);
|
||||
if (Math.hypot(num(t.x) - num(item.x), num(t.y) - num(item.y)) > radius + rr) continue;
|
||||
for (const rect of rects) {
|
||||
if (!worldRef.circleOverlapsObstacleRect?.(t.x, t.y, rr, rect, 1.0)) continue;
|
||||
if (!global.TarinaiGeometry.circleOverlapsRect(t.x, t.y, rr, rect, 1.0)) continue;
|
||||
const beforeX = num(t.x);
|
||||
const beforeY = num(t.y);
|
||||
const pushed = worldRef.pushTarinaiOutOfRect?.(t, rect, rr, {
|
||||
|
|
@ -1815,18 +1811,15 @@
|
|||
isMechanicalType,
|
||||
isPowered,
|
||||
sanitizeSegments,
|
||||
worldSegments,
|
||||
obstacleRects,
|
||||
placementRects,
|
||||
poisonHazardRects,
|
||||
boundsAabb,
|
||||
extent,
|
||||
reach,
|
||||
resetAnchor,
|
||||
axis,
|
||||
railAxisAngle,
|
||||
applySurfaceVelocityToCircle,
|
||||
resolveInteractions,
|
||||
resolveMechanicalPairs,
|
||||
updateWorld: updateMechanicalWorld,
|
||||
updateRotator,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,12 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
|
||||
function rectLocalPoint(rect, x, y) {
|
||||
return global.TarinaiCollisionFootprints.rectLocalPoint(rect, x, y);
|
||||
}
|
||||
|
||||
function rectWorldVector(rect, x, y) {
|
||||
return global.TarinaiCollisionFootprints.rectWorldVector(rect, x, y);
|
||||
}
|
||||
const Geometry = global.TarinaiGeometry;
|
||||
|
||||
function circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts = {}) {
|
||||
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
|
||||
const minBounceSpeed = Math.max(0, Number(opts.minBounceSpeed ?? rect.minBounceSpeed ?? 0) || 0);
|
||||
const local = rectLocalPoint(rect, obj.x, obj.y);
|
||||
const local = Geometry.rectLocalPoint(rect, obj.x, obj.y);
|
||||
let clx = clamp(local.x, -(rect.halfW || 0), rect.halfW || 0);
|
||||
let cly = clamp(local.y, -(rect.halfH || 0), rect.halfH || 0);
|
||||
let dx = local.x - clx;
|
||||
|
|
@ -23,7 +16,7 @@
|
|||
const px = obj.prevX;
|
||||
const py = obj.prevY;
|
||||
if (!Number.isFinite(px) || !Number.isFinite(py)) return false;
|
||||
const prev = rectLocalPoint(rect, px, py);
|
||||
const prev = Geometry.rectLocalPoint(rect, px, py);
|
||||
const expanded = { left: -(rect.halfW || 0) - hitRadius, right: (rect.halfW || 0) + hitRadius, top: -(rect.halfH || 0) - hitRadius, bottom: (rect.halfH || 0) + hitRadius };
|
||||
const insideNow = local.x >= expanded.left && local.x <= expanded.right && local.y >= expanded.top && local.y <= expanded.bottom;
|
||||
const insidePrev = prev.x >= expanded.left && prev.x <= expanded.right && prev.y >= expanded.top && prev.y <= expanded.bottom;
|
||||
|
|
@ -54,10 +47,10 @@
|
|||
}
|
||||
const nxLocal = dx / d;
|
||||
const nyLocal = dy / d;
|
||||
const n = rectWorldVector(rect, nxLocal, nyLocal);
|
||||
const n = Geometry.rectWorldVector(rect, nxLocal, nyLocal);
|
||||
const nx = n.x;
|
||||
const ny = n.y;
|
||||
const worldClosest = rectWorldVector(rect, clx, cly);
|
||||
const worldClosest = Geometry.rectWorldVector(rect, clx, cly);
|
||||
obj.x = (rect.cx || 0) + worldClosest.x + nx * (hitRadius + 0.5);
|
||||
obj.y = (rect.cy || 0) + worldClosest.y + ny * (hitRadius + 0.5);
|
||||
const vx = obj.vx || 0;
|
||||
|
|
|
|||
|
|
@ -287,16 +287,7 @@
|
|||
|
||||
global.TarinaiPhysicsShapeEditorSystem = Object.freeze({
|
||||
decorateToolbar,
|
||||
templateSegments,
|
||||
pointSegmentDistance,
|
||||
canvasLocalPoint,
|
||||
cleanSegments,
|
||||
drawEditorBase,
|
||||
drawSegments,
|
||||
eraseSegmentsAt,
|
||||
syncInputPair,
|
||||
drawDraftSegment,
|
||||
drawErasePreview,
|
||||
createSegmentEditorController,
|
||||
syncInputPair,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
function itemRadius(type, fallback = 12) {
|
||||
return typeof global.itemRadiusFor === "function" ? global.itemRadiusFor(type, fallback) : fallback;
|
||||
return global.itemRadiusFor(type, fallback);
|
||||
}
|
||||
function toolTypeFor(worldRef) {
|
||||
return typeof global.toolItemType === "function" ? global.toolItemType(worldRef?.tool || "") : "";
|
||||
return global.toolItemType(worldRef?.tool || "");
|
||||
}
|
||||
function toolSizeScale(worldRef, type) {
|
||||
return worldRef?.toolSizeScale ? worldRef.toolSizeScale(type) : 1;
|
||||
|
|
@ -22,10 +22,10 @@
|
|||
return worldRef?.toolSizeFor ? worldRef.toolSizeFor(type) : (worldRef?.toolSize || "medium");
|
||||
}
|
||||
function isRotatable(type) {
|
||||
return typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type);
|
||||
return global.isRotatableItemType(type);
|
||||
}
|
||||
function defaultAngle(type) {
|
||||
return typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
|
||||
return global.defaultItemAngle(type);
|
||||
}
|
||||
function rectOutside(worldRef, rect) {
|
||||
if (!worldRef || !rect) return true;
|
||||
|
|
|
|||
93
js/render.js
93
js/render.js
|
|
@ -13,14 +13,6 @@ function toolCursorIconDefinition(world) {
|
|||
}
|
||||
|
||||
|
||||
function canvasEllipsizeText(ctx, text, maxW) {
|
||||
let s = String(text || "");
|
||||
if (!ctx || ctx.measureText(s).width <= maxW) return s;
|
||||
while (s.length > 1 && ctx.measureText(`${s}\u2026`).width > maxW) s = s.slice(0, -1);
|
||||
return `${s}\u2026`;
|
||||
}
|
||||
TARINAI_RENDER_GLOBAL.canvasEllipsizeText = TARINAI_RENDER_GLOBAL.canvasEllipsizeText || canvasEllipsizeText;
|
||||
|
||||
function staticVersionedAssetPath(path) {
|
||||
const raw = String(path || "");
|
||||
if (!raw) return "";
|
||||
|
|
@ -924,7 +916,7 @@ function isEntityVisibleInRect(entity, rect, extra = 0) {
|
|||
|
||||
function syncCarriedPlushieToOwner(it, worldRef) {
|
||||
if (!it || !worldRef || !it.isStructure || it.type !== "plushie" || !it.carriedById) return false;
|
||||
const owner = (worldRef.tarinai || []).find(t => t && !t.dead && t.id === it.carriedById);
|
||||
const owner = worldRef.liveTarinaiById?.(it.carriedById) || (worldRef.tarinai || []).find(t => t && !t.dead && t.id === it.carriedById);
|
||||
if (!owner) return false;
|
||||
const ox = it.x, oy = it.y;
|
||||
it.ownerId = it.ownerId || owner.id;
|
||||
|
|
@ -989,7 +981,8 @@ function collectVisibleRenderStack(worldRef, visibleRect) {
|
|||
antScratch.length = 0;
|
||||
worldRef.spatial.nearbyRectInto(worldRef.spatial.antCells, visibleRect, antScratch);
|
||||
for (const a of antScratch) addLayered(a);
|
||||
for (const it of worldRef.items || []) {
|
||||
const plushieCandidates = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType("plushie") : (worldRef.items || []);
|
||||
for (const it of plushieCandidates || []) {
|
||||
if (it && it.isStructure && it.type === "plushie" && it.carriedById) addItem(it);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -1258,27 +1251,7 @@ function drawGardenBed(w, h, lighting) {
|
|||
activeCtx.fill();
|
||||
}
|
||||
|
||||
// Edge grass decorations were only visual and could be mistaken for actual grass items.
|
||||
// Keep the field edge clear; real grass is rendered through item/terrain systems.
|
||||
const edgeGrassCount = 0;
|
||||
for (let i = 0; i < edgeGrassCount; i++) {
|
||||
const side = i % 2;
|
||||
const px = side === 0 ? randSeed(5000 + i, x + 36, x + bw - 36) : randSeed(5400 + i, x + 30, x + bw - 30);
|
||||
const py = side === 0 ? y + 20 + randSeed(5700 + i, -4, 8) : y + bh - 24 + randSeed(6000 + i, -8, 4);
|
||||
activeCtx.save();
|
||||
activeCtx.translate(px, py);
|
||||
activeCtx.strokeStyle = light > 0.34 ? `rgba(101, 157, 68, ${0.38 + lighting.goldenStrength * 0.16})` : "rgba(97, 126, 128, 0.25)";
|
||||
activeCtx.lineWidth = 1.4;
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const len = randSeed(6300 + i * 5 + j, 7, 14);
|
||||
const dir = randSeed(6600 + i * 5 + j, -0.65, 0.65);
|
||||
activeCtx.beginPath();
|
||||
activeCtx.moveTo(0, 0);
|
||||
activeCtx.quadraticCurveTo(Math.sin(dir) * len * 0.35, -len * 0.45, Math.sin(dir) * len, -len);
|
||||
activeCtx.stroke();
|
||||
}
|
||||
activeCtx.restore();
|
||||
}
|
||||
// Edge grass decorations are intentionally disabled; real grass is rendered through item/terrain systems.
|
||||
|
||||
const flowerCount = groundType === "laboratory" || groundType === "concrete" || groundType === "blanket" || groundType === "foot_massage" || groundType === "ice" ? 0 : fieldType === "park" ? 18 : fieldType === "cage" ? 0 : 9;
|
||||
for (let i = 0; i < flowerCount; i++) {
|
||||
|
|
@ -1432,9 +1405,6 @@ function drawRain(ctx, w, h, t, opts = {}) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
function weatherLabel(weather) {
|
||||
return { sunny: "\u6674\u308c", cloudy: "\u66c7\u308a", light_rain: "\u5c0f\u96e8" }[weather] || "\u6674\u308c";
|
||||
}
|
||||
|
||||
function renderPerfBegin(label) {
|
||||
return window.TarinaiPerf.begin(label);
|
||||
|
|
@ -1524,8 +1494,6 @@ function render() {
|
|||
const renderStack = collectVisibleRenderStack(world, visibleRect);
|
||||
window.TarinaiPerf.setMetric("render.visibleBackItems", renderStack.backItems.length);
|
||||
window.TarinaiPerf.setMetric("render.visibleLayered", renderStack.layered.length);
|
||||
window.TarinaiPerf.setMetric("render.visibleEffects", (world.effects || []).length);
|
||||
|
||||
const endBackItems = renderPerfBegin("render.draw.backItems");
|
||||
for (const it of renderStack.backItems) {
|
||||
it.draw(ctx, world.time, lighting);
|
||||
|
|
@ -1592,13 +1560,22 @@ function render() {
|
|||
const effectLevel = renderVisualLevel("effects");
|
||||
let drawnEffects = 0;
|
||||
const maxEffects = visualEffectLimit(effectLevel);
|
||||
for (const ef of world.effects) {
|
||||
if (drawnEffects >= maxEffects) break;
|
||||
if (isEntityVisibleInRect(ef, visibleRect)) {
|
||||
ef.draw(ctx);
|
||||
drawnEffects += 1;
|
||||
const effects = world.effects || [];
|
||||
const totalEffects = effects.length;
|
||||
if (totalEffects > 0 && maxEffects > 0) {
|
||||
const scanCap = totalEffects > 180 ? Math.min(totalEffects, Math.max(maxEffects * 8, 160)) : totalEffects;
|
||||
let cursor = totalEffects > 180 ? (Math.max(0, Math.floor(world._renderEffectCursor || 0)) % totalEffects) : 0;
|
||||
for (let scanned = 0; scanned < scanCap && drawnEffects < maxEffects; scanned += 1) {
|
||||
const ef = effects[cursor];
|
||||
cursor = (cursor + 1) % totalEffects;
|
||||
if (isEntityVisibleInRect(ef, visibleRect)) {
|
||||
ef.draw(ctx);
|
||||
drawnEffects += 1;
|
||||
}
|
||||
}
|
||||
if (totalEffects > 180) world._renderEffectCursor = cursor;
|
||||
}
|
||||
window.TarinaiPerf.setMetric("render.visibleEffects", drawnEffects);
|
||||
renderPerfEnd(endEffects);
|
||||
ctx.restore();
|
||||
|
||||
|
|
@ -1617,11 +1594,20 @@ function render() {
|
|||
const endHud = renderPerfBegin("render.hud");
|
||||
// Time HUD
|
||||
ctx.save();
|
||||
const hudW = 126;
|
||||
const hudH = 50;
|
||||
const w = screenW, h = screenH;
|
||||
const hudX = w - hudW - 22;
|
||||
const hudY = 16;
|
||||
const seasonDay = world.seasonDayString?.() || `${world.day}\u65E5`;
|
||||
const playDay = `${Math.max(1, Number(world.day || 1) || 1)}\u65E5\u76EE`;
|
||||
const hudTemp = world.currentTemperature ?? world.updateTemperature?.(0) ?? 15;
|
||||
const hudLine1 = `${seasonDay} ${playDay} ${world.clockString()}`;
|
||||
const hudLine2 = `${world.phaseName()} ${weatherLabel(world.weather)} ${Math.round(hudTemp)}\u2103`;
|
||||
ctx.font = "bold 13px ui-rounded, sans-serif";
|
||||
const line1W = ctx.measureText(hudLine1).width;
|
||||
ctx.font = "11px ui-rounded, sans-serif";
|
||||
const line2W = ctx.measureText(hudLine2).width;
|
||||
const hudW = Math.ceil(Math.min(Math.max(104, w - 44), Math.max(line1W, line2W) + 20));
|
||||
const hudX = w - hudW - 22;
|
||||
ctx.fillStyle = light > 0.42 ? "rgba(255,251,244,0.76)" : "rgba(247,246,255,0.62)";
|
||||
roundedRect(ctx, hudX, hudY, hudW, hudH, 14);
|
||||
ctx.fill();
|
||||
|
|
@ -1630,12 +1616,10 @@ function render() {
|
|||
ctx.fillStyle = "#2a241d";
|
||||
ctx.font = "bold 13px ui-rounded, sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
const seasonDay = world.seasonDayString?.() || `${world.day}\u65E5\u76EE`;
|
||||
ctx.fillText(`${seasonDay} ${world.clockString()}`, hudX + 10, hudY + 20);
|
||||
ctx.fillText(hudLine1, hudX + 10, hudY + 20);
|
||||
ctx.font = "11px ui-rounded, sans-serif";
|
||||
ctx.fillStyle = "rgba(111,101,88,0.86)";
|
||||
const hudTemp = world.currentTemperature ?? world.updateTemperature?.(0) ?? 15;
|
||||
ctx.fillText(`${world.phaseName()} ${weatherLabel(world.weather)} ${Math.round(hudTemp)}\u2103`, hudX + 10, hudY + 38);
|
||||
ctx.fillText(hudLine2, hudX + 10, hudY + 38);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
|
|
@ -1659,7 +1643,8 @@ function drawPointerItemTooltip(ctx, world) {
|
|||
const canvasW = world.viewportW || world.w || ctx.canvas.width || 1;
|
||||
const canvasH = world.viewportH || world.h || ctx.canvas.height || 1;
|
||||
const isTarinaiTooltip = info?.kind === "tarinai";
|
||||
const maxTextW = isTarinaiTooltip ? Math.max(120, canvasW - 38) : 238;
|
||||
const isPipeTooltip = info?.kind === "pipe";
|
||||
const maxTextW = (isTarinaiTooltip || isPipeTooltip) ? Math.max(120, canvasW - 38) : 238;
|
||||
const ellipsize = (text, maxW) => canvasEllipsizeText(ctx, text, maxW);
|
||||
const wrapLine = (text, maxW) => {
|
||||
const s = String(text || "");
|
||||
|
|
@ -1688,7 +1673,7 @@ function drawPointerItemTooltip(ctx, world) {
|
|||
if (isTarinaiTooltip) lines = lines.slice(0, 4);
|
||||
const textW = lines.reduce((m, line) => Math.max(m, ctx.measureText(line).width), 0);
|
||||
const lineH = 17;
|
||||
const maxBoxW = isTarinaiTooltip ? Math.max(120, canvasW - 16) : 280;
|
||||
const maxBoxW = (isTarinaiTooltip || isPipeTooltip) ? Math.max(120, canvasW - 16) : 280;
|
||||
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);
|
||||
|
|
@ -1722,15 +1707,5 @@ function drawPointerItemTooltip(ctx, world) {
|
|||
}
|
||||
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
|
||||
window.TarinaiRender = { invalidateRenderCaches };
|
||||
|
|
|
|||
|
|
@ -95,13 +95,13 @@ const ROBOT_DEFAULT_MASK = 0b000011; // zunchi + corpses and stains
|
|||
const id = String(item?.robotCleanerTargetId || "");
|
||||
if (!key || !id || !worldRef) return null;
|
||||
if (key === "tarinai") {
|
||||
const t = (worldRef.tarinai || []).find(t => t && t.id === id && !t.dead);
|
||||
const t = worldRef.liveTarinaiById?.(id) || (worldRef.tarinai || []).find(t => t && t.id === id && !t.dead);
|
||||
return robotTargetAlive(t) && !robotTargetBlocked(item, t, worldRef) ? t : null;
|
||||
}
|
||||
const found = (worldRef.items || []).find(it => it && it.id === id && !it.dead && (
|
||||
it.type === key || (key === "ant_corpse" && (it.type === "trace" || it.type === "splat"))
|
||||
));
|
||||
return robotTargetAlive(found) && !robotTargetBlocked(item, found, worldRef) ? found : null;
|
||||
const byId = typeof worldRef.itemById === "function" ? worldRef.itemById(id) : null;
|
||||
const found = byId || (worldRef.items || []).find(it => it && it.id === id && !it.dead);
|
||||
const matchesKey = found && (found.type === key || (key === "ant_corpse" && (found.type === "trace" || found.type === "splat")));
|
||||
return matchesKey && robotTargetAlive(found) && !robotTargetBlocked(item, found, worldRef) ? found : null;
|
||||
}
|
||||
|
||||
function robotTargetCandidates(item, worldRef) {
|
||||
|
|
|
|||
303
js/save_codec.js
303
js/save_codec.js
|
|
@ -6,27 +6,11 @@
|
|||
if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_codec.js");
|
||||
if (!SaveSchema) throw new Error("TarinaiSaveSchema is not available for save_codec.js");
|
||||
const EXPORT_PREFIX = "\u305f";
|
||||
const RAW_CODEC = "raw";
|
||||
const DEFLATE_CODEC = "deflate";
|
||||
const DICT_CODEC = "dict";
|
||||
const DICT_DEFLATE_CODEC = "dict-deflate";
|
||||
const SAVE_HEADER_CHARS = "\u3089\u308A\u308B\u308C\u308D\u308F\u3092\u3093\u3083\u3085\u3087\u3063";
|
||||
const SAVE_HEADER_CODECS = Object.freeze([RAW_CODEC, DEFLATE_CODEC, DICT_CODEC, DICT_DEFLATE_CODEC]);
|
||||
const SAVE_HEADER_CHARS = "\u3089\u308A\u308B";
|
||||
const {
|
||||
BINARY_SCHEMA_VERSION,
|
||||
FIELD_IDS,
|
||||
STRUCTURE_TYPE_IDS,
|
||||
SERVING_FOOD_TYPE_IDS,
|
||||
PIN_TYPE_IDS,
|
||||
ROTATABLE_ITEM_TYPE_IDS,
|
||||
JSON_EXTRA_ITEM_TYPE_IDS,
|
||||
ITEM_TYPE_IDS,
|
||||
} = SaveSchema;
|
||||
const FIRE_TYPE_ID = ITEM_TYPE_IDS.indexOf("fire");
|
||||
const FIRECRACKER_TYPE_ID = ITEM_TYPE_IDS.indexOf("firecracker");
|
||||
const FLAME_FIRECRACKER_TYPE_ID = ITEM_TYPE_IDS.indexOf("flame_firecracker");
|
||||
const BALLOON_TYPE_ID = ITEM_TYPE_IDS.indexOf("balloon");
|
||||
const STICKY_BOMB_TYPE_ID = ITEM_TYPE_IDS.indexOf("sticky_bomb");
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const textDecoder = new TextDecoder();
|
||||
|
|
@ -89,71 +73,14 @@
|
|||
return new Uint8Array(out);
|
||||
}
|
||||
|
||||
function saveHeader(codec = RAW_CODEC, byteLength = 0) {
|
||||
const codecIndex = Math.max(0, SAVE_HEADER_CODECS.indexOf(codec));
|
||||
function saveHeader(byteLength = 0) {
|
||||
const remainder = ((Number(byteLength) || 0) % 3 + 3) % 3;
|
||||
return SAVE_HEADER_CHARS.charAt(codecIndex * 3 + remainder);
|
||||
return SAVE_HEADER_CHARS.charAt(remainder);
|
||||
}
|
||||
function parseSaveHeader(ch = "") {
|
||||
const index = SAVE_HEADER_CHARS.indexOf(String(ch || "").charAt(0));
|
||||
if (index < 0) throw new Error("unsupported Japanese save header");
|
||||
return {
|
||||
codec: SAVE_HEADER_CODECS[Math.floor(index / 3)] || RAW_CODEC,
|
||||
remainder: index % 3,
|
||||
};
|
||||
}
|
||||
|
||||
const BYTE_DICTIONARY = Object.freeze([
|
||||
[0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[100, 100, 100, 100],
|
||||
[0, 0, 100],
|
||||
[0, 0, 0, 100],
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
].map(row => Uint8Array.from(row)));
|
||||
const DICT_ESCAPE = 255;
|
||||
function dictionaryTransform(bytes) {
|
||||
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||
const out = [];
|
||||
for (let i = 0; i < source.length;) {
|
||||
let match = -1;
|
||||
for (let d = BYTE_DICTIONARY.length - 1; d >= 0; d--) {
|
||||
const entry = BYTE_DICTIONARY[d];
|
||||
if (i + entry.length > source.length) continue;
|
||||
let ok = true;
|
||||
for (let j = 0; j < entry.length; j++) {
|
||||
if (source[i + j] !== entry[j]) { ok = false; break; }
|
||||
}
|
||||
if (ok) { match = d; break; }
|
||||
}
|
||||
if (match >= 0) {
|
||||
out.push(DICT_ESCAPE, match + 1);
|
||||
i += BYTE_DICTIONARY[match].length;
|
||||
} else {
|
||||
const byte = source[i++];
|
||||
if (byte === DICT_ESCAPE) out.push(DICT_ESCAPE, 0);
|
||||
else out.push(byte);
|
||||
}
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
}
|
||||
function dictionaryRestore(bytes) {
|
||||
const source = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes || []);
|
||||
const out = [];
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
const byte = source[i];
|
||||
if (byte !== DICT_ESCAPE) { out.push(byte); continue; }
|
||||
const code = source[++i];
|
||||
if (code == null) throw new Error("truncated Japanese save dictionary payload");
|
||||
if (code === 0) out.push(DICT_ESCAPE);
|
||||
else {
|
||||
const entry = BYTE_DICTIONARY[code - 1];
|
||||
if (!entry) throw new Error("invalid Japanese save dictionary token");
|
||||
for (const v of entry) out.push(v);
|
||||
}
|
||||
}
|
||||
return Uint8Array.from(out);
|
||||
const remainder = SAVE_HEADER_CHARS.indexOf(String(ch || "").charAt(0));
|
||||
if (remainder < 0) throw new Error("unsupported Japanese save header");
|
||||
return { remainder };
|
||||
}
|
||||
|
||||
function toSafeUInt(value = 0) {
|
||||
|
|
@ -236,10 +163,8 @@
|
|||
add(snapshot?.w?.[8]);
|
||||
for (const row of snapshot?.t || []) add(row?.[1]);
|
||||
for (const row of snapshot?.i || []) {
|
||||
const typeId = row?.[0];
|
||||
const extra = row?.[4];
|
||||
if (typeId === 22) add(extra?.[0]);
|
||||
else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) add(JSON.stringify(extra || []));
|
||||
if (extra && (Array.isArray(extra) ? extra.length : Object.keys(extra || {}).length)) add(JSON.stringify(extra));
|
||||
}
|
||||
return [...counts.entries()]
|
||||
.filter(([, count]) => count > 1)
|
||||
|
|
@ -338,23 +263,6 @@
|
|||
return readBitFields(reader, Array.from({ length: count }, () => bits)).map(v => v * Math.max(1, step));
|
||||
}
|
||||
|
||||
async function compressBytes(bytes) {
|
||||
if (typeof CompressionStream === "function") {
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw"));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
async function decompressBytes(bytes, codec) {
|
||||
if (codec === DEFLATE_CODEC) {
|
||||
if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser");
|
||||
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
if (codec !== RAW_CODEC) throw new Error("unsupported Japanese save codec");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function writeFixedPacked(writer, arr = [], count = 0, bits = 8, step = 1) {
|
||||
writeQuantizedBitFields(writer, arr, count, bits, step);
|
||||
}
|
||||
|
|
@ -423,16 +331,6 @@
|
|||
function readModes(reader) {
|
||||
return readBitFields(reader, [2, 2, 1]);
|
||||
}
|
||||
function commonItemAmountNeeded(typeId) {
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) return false;
|
||||
if (SERVING_FOOD_TYPE_IDS.has(typeId)) return false;
|
||||
return true;
|
||||
}
|
||||
function defaultStructureHp10(typeId) {
|
||||
if (ITEM_TYPE_IDS[typeId] === "grass_bed") return 1000;
|
||||
if (ITEM_TYPE_IDS[typeId] === "plushie") return 10;
|
||||
return 10;
|
||||
}
|
||||
function defaultNeedsFromVitals(hunger = 0, energy = 0, sleepPressure = 0) {
|
||||
const food = Math.max(0, Math.min(100, Math.round(Number(hunger) || 0)));
|
||||
const sleep = Math.max(0, Math.min(100, Math.round(Math.max(Number(sleepPressure) || 0, 100 - (Number(energy) || 0)))));
|
||||
|
|
@ -584,147 +482,19 @@
|
|||
return [flags, birthSeed, x, y, age, generation, hunger, energy, sleepPressure, needs, current, parents, relationships, timers, modes, pinIdx, nestIdx, stateInfo, genetics, fightStats];
|
||||
}
|
||||
|
||||
function writeItemExtra(writer, typeId, extra = []) {
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) {
|
||||
const defaultHp = defaultStructureHp10(typeId);
|
||||
const hp = toSafeInt(extra[0] || 0);
|
||||
const maxHp = toSafeInt(extra[1] || 0);
|
||||
let mask = 0;
|
||||
if (valuesDiffer(hp, defaultHp)) mask |= 1;
|
||||
if (valuesDiffer(maxHp, defaultHp)) mask |= 2;
|
||||
if ((extra[2] ?? -1) >= 0) mask |= 4;
|
||||
if ((extra[3] ?? -1) >= 0) mask |= 8;
|
||||
if (extra[4]) mask |= 16;
|
||||
if ((extra[5] ?? -1) >= 0) mask |= 32;
|
||||
if (valuesDiffer(extra[6], 0)) mask |= 64;
|
||||
writeBitFields(writer, [mask], [7]);
|
||||
if (mask & 1) writer.s(hp);
|
||||
if (mask & 2) writer.s(maxHp);
|
||||
if (mask & 4) writer.u(extra[2]);
|
||||
if (mask & 8) writer.u(extra[3]);
|
||||
if (mask & 32) writer.u(extra[5]);
|
||||
if (mask & 64) writer.s(extra[6] || 0);
|
||||
} else if (typeId === 0) { // grass
|
||||
writer.s(extra[0] || 0);
|
||||
let mask = 0;
|
||||
if (valuesDiffer(extra[1], 100)) mask |= 1;
|
||||
if (valuesDiffer(extra[2], 0)) mask |= 2;
|
||||
if (extra[3]) mask |= 4;
|
||||
writeBitFields(writer, [mask], [3]);
|
||||
if (mask & 1) writer.s(extra[1] || 0);
|
||||
if (mask & 2) writer.s(extra[2] || 0);
|
||||
} else if (typeId === 1) { // zunchi
|
||||
writeBitFields(writer, [extra[0] || 0], [2]);
|
||||
let mask = 0;
|
||||
if (valuesDiffer(extra[1], 100)) mask |= 1;
|
||||
if (valuesDiffer(extra[2], 0)) mask |= 2;
|
||||
writeBitFields(writer, [mask], [2]);
|
||||
if (mask & 1) writer.s(extra[1] || 0);
|
||||
if (mask & 2) writer.s(extra[2] || 0);
|
||||
} else if (typeId === 22) { // signboard
|
||||
writeSaveString(writer, extra[0] || "");
|
||||
} else if (typeId === 6) { // duplicator
|
||||
writer.s(extra[0] ?? -1);
|
||||
} else if (typeId === 42) { // robot_cleaner
|
||||
writeBitFields(writer, [Math.max(0, Math.min(0x3f, Number(extra[0] ?? 3) | 0)), extra[1] ? 1 : 0], [6, 1]);
|
||||
} else if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
|
||||
writeSaveString(writer, JSON.stringify(extra || []));
|
||||
} else if (typeId === 21 || typeId === BALLOON_TYPE_ID) { // ball/balloon
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
|
||||
} else if (typeId === 23) { // ant_nest
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] || 0);
|
||||
} else if (typeId === 24) { // ant_corpse
|
||||
writer.s(extra[0] || 0);
|
||||
} else if (typeId === FIRECRACKER_TYPE_ID || typeId === FLAME_FIRECRACKER_TYPE_ID || typeId === FIRE_TYPE_ID) { // firecracker/flame/fire
|
||||
writer.s(extra[0] || 0);
|
||||
} else if (typeId === STICKY_BOMB_TYPE_ID) { // sticky bomb
|
||||
writer.s(extra[0] || 0); writer.s(extra[1] ?? -1);
|
||||
} else if (PIN_TYPE_IDS.has(typeId)) {
|
||||
const target = extra[1] ?? -1;
|
||||
let mask = 0;
|
||||
if (target >= 0) mask |= 1;
|
||||
if (valuesDiffer(extra[2], 0)) mask |= 2;
|
||||
if (valuesDiffer(extra[3], 0)) mask |= 4;
|
||||
if (valuesDiffer(extra[4], 0)) mask |= 8;
|
||||
writeBitFields(writer, [extra[0] || 0, mask], [2, 4]);
|
||||
if (mask & 1) writer.u(target);
|
||||
if (mask & 2) writer.s(extra[2] || 0);
|
||||
if (mask & 4) writer.s(extra[3] || 0);
|
||||
if (mask & 8) writer.s(extra[4] || 0);
|
||||
} else if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
|
||||
let mask = 0;
|
||||
if (valuesDiffer(extra[0], 0)) mask |= 1;
|
||||
if (extra[1]) mask |= 2;
|
||||
writeBitFields(writer, [mask], [2]);
|
||||
if (mask & 1) writer.s(extra[0] || 0);
|
||||
} else if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
|
||||
writer.s(extra[0] || 0);
|
||||
if (valuesDiffer(extra[1], extra[0])) { writer.u(1); writer.s(extra[1] || 0); }
|
||||
else writer.u(0);
|
||||
function writeCurrentItemExtra(writer, extra = []) {
|
||||
const value = extra == null ? [] : extra;
|
||||
writeSaveString(writer, JSON.stringify(value));
|
||||
}
|
||||
function readCurrentItemExtra(reader) {
|
||||
try {
|
||||
const parsed = JSON.parse(readSaveString(reader) || "[]");
|
||||
return (Array.isArray(parsed) || (parsed && typeof parsed === "object")) ? parsed : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function readItemExtra(reader, typeId) {
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) {
|
||||
const defaultHp = defaultStructureHp10(typeId);
|
||||
const [mask] = readBitFields(reader, [7]);
|
||||
const hp = (mask & 1) ? reader.s() : defaultHp;
|
||||
const maxHp = (mask & 2) ? reader.s() : defaultHp;
|
||||
return [
|
||||
hp,
|
||||
maxHp,
|
||||
(mask & 4) ? reader.u() : -1,
|
||||
(mask & 8) ? reader.u() : -1,
|
||||
(mask & 16) ? 1 : 0,
|
||||
(mask & 32) ? reader.u() : -1,
|
||||
(mask & 64) ? reader.s() : 0,
|
||||
];
|
||||
}
|
||||
if (typeId === 0) {
|
||||
const growth = reader.s();
|
||||
const [mask] = readBitFields(reader, [3]);
|
||||
return [growth, (mask & 1) ? reader.s() : 100, (mask & 2) ? reader.s() : 0, (mask & 4) ? 1 : 0];
|
||||
}
|
||||
if (typeId === 1) {
|
||||
const [stage] = readBitFields(reader, [2]);
|
||||
const [mask] = readBitFields(reader, [2]);
|
||||
return [stage, (mask & 1) ? reader.s() : 100, (mask & 2) ? reader.s() : 0];
|
||||
}
|
||||
if (typeId === 22) return [readSaveString(reader)];
|
||||
if (typeId === 6) return [reader.s()];
|
||||
if (typeId === 42) return readBitFields(reader, [6, 1]);
|
||||
if (JSON_EXTRA_ITEM_TYPE_IDS.has(typeId)) {
|
||||
try {
|
||||
const parsed = JSON.parse(readSaveString(reader) || "[]");
|
||||
return (Array.isArray(parsed) || (parsed && typeof parsed === "object")) ? parsed : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (typeId === 21 || typeId === BALLOON_TYPE_ID) return [reader.s(), reader.s()];
|
||||
if (typeId === 23) return [reader.s(), reader.s()];
|
||||
if (typeId === 24) return [reader.s()];
|
||||
if (typeId === FIRECRACKER_TYPE_ID || typeId === FLAME_FIRECRACKER_TYPE_ID || typeId === FIRE_TYPE_ID) return [reader.s()];
|
||||
if (typeId === STICKY_BOMB_TYPE_ID) return [reader.s(), reader.s()];
|
||||
if (PIN_TYPE_IDS.has(typeId)) {
|
||||
const [state, mask] = readBitFields(reader, [2, 4]);
|
||||
return [
|
||||
state,
|
||||
(mask & 1) ? reader.u() : -1,
|
||||
(mask & 2) ? reader.s() : 0,
|
||||
(mask & 4) ? reader.s() : 0,
|
||||
(mask & 8) ? reader.s() : 0,
|
||||
];
|
||||
}
|
||||
if (ROTATABLE_ITEM_TYPE_IDS.has(typeId)) {
|
||||
const [mask] = readBitFields(reader, [2]);
|
||||
return [(mask & 1) ? reader.s() : 0, (mask & 2) ? 1 : 0];
|
||||
}
|
||||
if (SERVING_FOOD_TYPE_IDS.has(typeId)) {
|
||||
const remaining = reader.s();
|
||||
return [remaining, reader.u() ? reader.s() : remaining];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function itemCoordKey(row = []) {
|
||||
const x = Math.max(0, Math.min(65535, toSafeUInt(row[1] || 0)));
|
||||
const y = Math.max(0, Math.min(65535, toSafeUInt(row[2] || 0)));
|
||||
|
|
@ -741,19 +511,16 @@
|
|||
writer.s(x - (coordState?.x || 0));
|
||||
writer.s(y - (coordState?.y || 0));
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
if (commonItemAmountNeeded(typeId)) writer.s(row[3] || 0);
|
||||
writeItemExtra(writer, typeId, row[4] || []);
|
||||
writer.s(row[3] || 0);
|
||||
writeCurrentItemExtra(writer, row[4] || []);
|
||||
}
|
||||
function readItemPayload(reader, typeId = 0, coordState = null) {
|
||||
const x = (coordState?.x || 0) + reader.s();
|
||||
const y = (coordState?.y || 0) + reader.s();
|
||||
if (coordState) { coordState.x = x; coordState.y = y; }
|
||||
const amount = commonItemAmountNeeded(typeId) ? reader.s() : 0;
|
||||
const extra = readItemExtra(reader, typeId);
|
||||
let restoredAmount = amount;
|
||||
if (STRUCTURE_TYPE_IDS.has(typeId)) restoredAmount = extra[0] || 0;
|
||||
else if (SERVING_FOOD_TYPE_IDS.has(typeId)) restoredAmount = extra[0] || 0;
|
||||
return [typeId, x, y, restoredAmount, extra];
|
||||
const amount = reader.s();
|
||||
const extra = readCurrentItemExtra(reader);
|
||||
return [typeId, x, y, amount, extra];
|
||||
}
|
||||
function itemBlocksFromRows(rows = []) {
|
||||
const groups = new Map();
|
||||
|
|
@ -843,25 +610,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function packBytesForSave(bytes) {
|
||||
const candidates = [{ codec: RAW_CODEC, packed: bytes }];
|
||||
const dict = dictionaryTransform(bytes);
|
||||
if (dict.length < bytes.length) candidates.push({ codec: DICT_CODEC, packed: dict });
|
||||
try {
|
||||
const compressed = await compressBytes(bytes);
|
||||
if (compressed.length < bytes.length) candidates.push({ codec: DEFLATE_CODEC, packed: compressed });
|
||||
if (dict.length < bytes.length) {
|
||||
const dictCompressed = await compressBytes(dict);
|
||||
if (dictCompressed.length < bytes.length) candidates.push({ codec: DICT_DEFLATE_CODEC, packed: dictCompressed });
|
||||
}
|
||||
} catch (error) { console.warn("save compression fallback", error); }
|
||||
return candidates.sort((a, b) => a.packed.length - b.packed.length)[0];
|
||||
}
|
||||
|
||||
async function encodeSnapshot(snapshot) {
|
||||
const bytes = encodeBinarySnapshot(snapshot);
|
||||
const { codec, packed } = await packBytesForSave(bytes);
|
||||
return `${EXPORT_PREFIX}${saveHeader(codec, packed.length)}${jp4096Encode(packed)}`;
|
||||
return `${EXPORT_PREFIX}${saveHeader(bytes.length)}${jp4096Encode(bytes)}`;
|
||||
}
|
||||
|
||||
async function decodeSnapshot(text) {
|
||||
|
|
@ -869,11 +620,7 @@
|
|||
if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported Japanese save text");
|
||||
const header = parseSaveHeader(raw.charAt(EXPORT_PREFIX.length));
|
||||
const payload = raw.slice(EXPORT_PREFIX.length + 1);
|
||||
const packed = jp4096Decode(payload, header.remainder);
|
||||
const packedCodec = header.codec === DICT_DEFLATE_CODEC ? DEFLATE_CODEC : (header.codec === DICT_CODEC ? RAW_CODEC : header.codec);
|
||||
let bytes = await decompressBytes(packed, packedCodec);
|
||||
if (header.codec === DICT_CODEC || header.codec === DICT_DEFLATE_CODEC) bytes = dictionaryRestore(bytes);
|
||||
return decodeBinarySnapshot(bytes);
|
||||
return decodeBinarySnapshot(jp4096Decode(payload, header.remainder));
|
||||
}
|
||||
|
||||
global.TarinaiSaveCodec = {
|
||||
|
|
|
|||
|
|
@ -1,81 +1,47 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: persistence/schema
|
||||
// Owns stable save identifiers shared by snapshot_system.js and save_codec.js.
|
||||
// Current-format save identifiers only.
|
||||
// Current-format save identifiers only. Old save compatibility is intentionally not retained.
|
||||
(function (global) {
|
||||
const SNAPSHOT_VERSION = 20;
|
||||
const BINARY_SCHEMA_VERSION = 25;
|
||||
const ids = global.TarinaiDomainIds;
|
||||
const itemCatalog = global.TarinaiItemTypeCatalog;
|
||||
if (!ids) throw new Error("TarinaiDomainIds must be loaded before save_schema.js");
|
||||
if (!itemCatalog) throw new Error("TarinaiItemTypeCatalog must be loaded before save_schema.js");
|
||||
|
||||
const FIELD_IDS = Object.freeze(["garden", "cage", "park"]);
|
||||
const GROUND_IDS = Object.freeze(["soil", "dirt", "concrete", "blanket", "foot_massage", "ice", "laboratory"]);
|
||||
const WEATHER_IDS = Object.freeze(["sunny", "cloudy", "light_rain"]);
|
||||
const MOOD_IDS = Object.freeze(["relaxed", "devastation", "crisis", "confusion", "fearful", "disease_spread", "weakened", "breeding", "happy", "overcrowded", "isolated"]);
|
||||
const STATE_IDS = Object.freeze(["idle", "sleep"]);
|
||||
const POWER_MODE_IDS = Object.freeze(["", "protein", "niteropu"]);
|
||||
const SIZE_MODE_IDS = Object.freeze(["", "giant_drug", "dwarf_drug"]);
|
||||
const LIFE_MODE_IDS = Object.freeze(["", "mercury"]);
|
||||
const PIN_STATE_IDS = Object.freeze(["loose", "lodged", "falling", "stuck"]);
|
||||
const STAGE_IDS = Object.freeze(["fresh", "aging", "old", "dry"]);
|
||||
const ITEM_TYPE_IDS = Object.freeze([
|
||||
"grass", "zunchi", "water", "grass_bed", "plushie", "nest_box", "duplicator", "sweet", "love_mochi", "fight_mochi",
|
||||
"sleep_drug", "laxative", "protein", "niteropu", "ammo", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice",
|
||||
"stone", "ball", "signboard", "ant_nest", "ant_corpse", "firecracker", "pushpin", "oshibyo", "fence_v", "fence_h",
|
||||
"splat", "food", "bed", "genkotsu", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "reciprocator", "rope", "rod", "poison_block", "robot_cleaner", "dry_ice", "stove", "glass_wall", "slave_chain", "golden_crown", "fire", "flame_firecracker", "balloon", "fan", "pipe", "sticky_bomb"
|
||||
]);
|
||||
|
||||
function typeIds(names) {
|
||||
return Object.freeze(new Set((names || []).map(name => ITEM_TYPE_IDS.indexOf(name)).filter(id => id >= 0)));
|
||||
}
|
||||
|
||||
const STRUCTURE_TYPE_IDS = typeIds(["grass_bed", "plushie"]);
|
||||
const SERVING_FOOD_TYPE_IDS = typeIds(["sweet", "love_mochi", "fight_mochi", "sleep_drug", "laxative", "protein", "niteropu", "mystery_drug", "mercury", "giant_drug", "dwarf_drug", "zunda_juice", "food"]);
|
||||
const PIN_TYPE_IDS = typeIds(["pushpin", "oshibyo"]);
|
||||
const JSON_EXTRA_ITEM_TYPE_IDS = typeIds(["rotator", "poison_block", "reciprocator", "rope", "rod"]);
|
||||
const ROTATABLE_ITEM_TYPE_IDS = typeIds(["fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "fan"]);
|
||||
|
||||
function enumIndex(list, value, fallback = 0) {
|
||||
const i = list.indexOf(String(value ?? ""));
|
||||
return i >= 0 ? i : fallback;
|
||||
}
|
||||
|
||||
function canonicalMoodId(value = "relaxed") {
|
||||
const id = String(value || "relaxed");
|
||||
return MOOD_IDS.includes(id) ? id : "relaxed";
|
||||
}
|
||||
|
||||
function enumValue(list, index, fallback = "") {
|
||||
const i = Number(index) | 0;
|
||||
return list[i] ?? fallback ?? list[0] ?? "";
|
||||
}
|
||||
const SNAPSHOT_VERSION = 22;
|
||||
const BINARY_SCHEMA_VERSION = 27;
|
||||
|
||||
function moodValue(index, fallback = "relaxed") {
|
||||
const i = Number(index) | 0;
|
||||
return canonicalMoodId(MOOD_IDS[i] ?? fallback);
|
||||
return ids.canonicalMoodId(ids.enumValue(ids.MOOD_IDS, index, fallback));
|
||||
}
|
||||
|
||||
global.TarinaiSaveSchema = Object.freeze({
|
||||
SNAPSHOT_VERSION,
|
||||
BINARY_SCHEMA_VERSION,
|
||||
FIELD_IDS,
|
||||
GROUND_IDS,
|
||||
WEATHER_IDS,
|
||||
MOOD_IDS,
|
||||
canonicalMoodId,
|
||||
FIELD_IDS: ids.FIELD_IDS,
|
||||
GROUND_IDS: ids.GROUND_IDS,
|
||||
WEATHER_IDS: ids.WEATHER_IDS,
|
||||
MOOD_IDS: ids.MOOD_IDS,
|
||||
STATE_IDS: ids.STATE_IDS,
|
||||
POWER_MODE_IDS: ids.POWER_MODE_IDS,
|
||||
SIZE_MODE_IDS: ids.SIZE_MODE_IDS,
|
||||
LIFE_MODE_IDS: ids.LIFE_MODE_IDS,
|
||||
PIN_STATE_IDS: ids.PIN_STATE_IDS,
|
||||
STAGE_IDS: ids.STAGE_IDS,
|
||||
ITEM_TYPE_IDS: itemCatalog.ITEM_TYPE_IDS,
|
||||
FIELD_INDEX: ids.FIELD_INDEX,
|
||||
GROUND_INDEX: ids.GROUND_INDEX,
|
||||
WEATHER_INDEX: ids.WEATHER_INDEX,
|
||||
MOOD_INDEX: ids.MOOD_INDEX,
|
||||
ITEM_TYPE_INDEX: itemCatalog.ITEM_TYPE_INDEX,
|
||||
canonicalFieldId: ids.canonicalFieldId,
|
||||
canonicalGroundId: ids.canonicalGroundId,
|
||||
canonicalWeatherId: ids.canonicalWeatherId,
|
||||
canonicalMoodId: ids.canonicalMoodId,
|
||||
moodValue,
|
||||
STATE_IDS,
|
||||
POWER_MODE_IDS,
|
||||
SIZE_MODE_IDS,
|
||||
LIFE_MODE_IDS,
|
||||
PIN_STATE_IDS,
|
||||
STAGE_IDS,
|
||||
ITEM_TYPE_IDS,
|
||||
STRUCTURE_TYPE_IDS,
|
||||
SERVING_FOOD_TYPE_IDS,
|
||||
PIN_TYPE_IDS,
|
||||
JSON_EXTRA_ITEM_TYPE_IDS,
|
||||
ROTATABLE_ITEM_TYPE_IDS,
|
||||
enumIndex,
|
||||
enumValue,
|
||||
enumIndex: ids.enumIndex,
|
||||
enumValue: ids.enumValue,
|
||||
itemTypeIndex: itemCatalog.itemTypeIndex,
|
||||
itemTypeValue: itemCatalog.itemTypeValue,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@
|
|||
if (global.TarinaiWeatherSystem.shouldDropRainWater(worldRef, dt)) {
|
||||
const drop = global.TarinaiWeatherSystem.createRainWaterItem(worldRef);
|
||||
worldRef.addItem?.(drop, "rain-water") || worldRef.items.push(drop);
|
||||
worldRef.emit?.("rain:itemDropped", { item: drop, type: "water" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
// Layer: simulation/system-effects
|
||||
// Owns frame updates for transient visual/effect entities.
|
||||
(function (global) {
|
||||
function updateEffect(effect, dt, frameId = 0) {
|
||||
function updateEffect(effect, dt, frameId = 0, runScale = 1) {
|
||||
if (!effect || effect.dead) return false;
|
||||
const runDt = Math.min(0.24, Math.max(0, Number(effect._effectUpdateAccum || 0) + dt));
|
||||
effect._effectUpdateAccum = 0;
|
||||
const runDt = Math.min(0.24, Math.max(0, dt * Math.max(1, Number(runScale) || 1)));
|
||||
effect._effectUpdatedFrame = frameId;
|
||||
effect.update(runDt);
|
||||
return true;
|
||||
|
|
@ -18,11 +17,12 @@
|
|||
const frameId = (worldRef._effectUpdateFrameId || 0) + 1;
|
||||
worldRef._effectUpdateFrameId = frameId;
|
||||
if (total <= 56) {
|
||||
for (const ef of effects) updateEffect(ef, dt, frameId);
|
||||
for (const ef of effects) updateEffect(ef, dt, frameId, 1);
|
||||
worldRef._effectUpdateStats = { total, updated: total, skipped: 0, batched: false };
|
||||
return;
|
||||
}
|
||||
const budget = Math.max(40, Math.ceil(total * 0.42));
|
||||
const runScale = Math.min(3.0, Math.max(1, total / Math.max(1, budget)));
|
||||
let cursor = Math.max(0, Math.floor(worldRef._effectUpdateCursor || 0)) % total;
|
||||
let updated = 0;
|
||||
let scanned = 0;
|
||||
|
|
@ -31,15 +31,9 @@
|
|||
cursor = (cursor + 1) % total;
|
||||
scanned += 1;
|
||||
if (!ef || ef.dead) continue;
|
||||
updateEffect(ef, dt, frameId);
|
||||
updateEffect(ef, dt, frameId, runScale);
|
||||
updated += 1;
|
||||
}
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
const ef = effects[i];
|
||||
if (!ef || ef.dead || ef._effectUpdatedFrame === frameId) continue;
|
||||
if (Number.isFinite(ef._effectUpdateAccum)) ef._effectUpdateAccum = Math.min(0.24, ef._effectUpdateAccum + dt);
|
||||
else ef._effectUpdateAccum = dt;
|
||||
}
|
||||
worldRef._effectUpdateCursor = cursor;
|
||||
worldRef._effectUpdateStats = { total, updated, skipped: Math.max(0, total - updated), batched: true };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,7 @@
|
|||
worldRef.updateTemperature?.(dt);
|
||||
worldRef.updateTerrainDirtyState?.(dt);
|
||||
const now = worldRef.time || 0;
|
||||
if (now >= (worldRef.nextColonyMoodEvaluateAt || 0)) {
|
||||
worldRef.nextColonyMoodEvaluateAt = now + 0.65;
|
||||
worldRef.evaluateColonyMood?.();
|
||||
}
|
||||
if (now >= (worldRef.nextColonyMoodEvalAt || 0)) worldRef.evaluateColonyMood?.();
|
||||
}
|
||||
|
||||
function prepareSpatialFrame(worldRef) {
|
||||
|
|
@ -61,7 +58,9 @@
|
|||
}
|
||||
|
||||
function finalizeDiagnostics(worldRef) {
|
||||
worldRef.lastRuntimeDiagnostics = worldRef.runtimeDiagnostics?.() || null;
|
||||
if (!worldRef) return;
|
||||
const wantsDiagnostics = Boolean(global.__tarinaiDebugOverlayEnabled || global.__tarinaiForceRuntimeDiagnostics);
|
||||
worldRef.lastRuntimeDiagnostics = wantsDiagnostics ? (worldRef.runtimeDiagnostics?.() || null) : null;
|
||||
}
|
||||
|
||||
const phases = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@
|
|||
worldRef.nextGrassBedExpiryCheckAt = (worldRef.time || 0) + 5.0;
|
||||
const lifespan = Math.max(1, CONFIG.dayLength || 120); // \u30B2\u30FC\u30E0\u518524\u6642\u9593
|
||||
let removedBeds = 0;
|
||||
for (const it of worldRef.items || []) {
|
||||
const grassBeds = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType("grass_bed") : (worldRef.items || []);
|
||||
for (const it of grassBeds || []) {
|
||||
if (!it || it.dead || it.type !== "grass_bed") continue;
|
||||
if (!Number.isFinite(Number(it.createdAt))) it.createdAt = worldRef.time || 0;
|
||||
if ((worldRef.time || 0) - Number(it.createdAt || 0) < lifespan) continue;
|
||||
|
|
@ -75,13 +76,9 @@
|
|||
worldRef.sanitizeOutOfBounds();
|
||||
}
|
||||
|
||||
worldRef.drawSortTimer += dt;
|
||||
const visibleMoving = Boolean(worldRef.spatialTarinaiDirty || worldRef.spatialDynamicItemsDirty || worldRef.spatialAntDirty);
|
||||
const sortInterval = visibleMoving ? 0.25 : 0.55;
|
||||
if (worldRef.drawSortTimer >= sortInterval) {
|
||||
worldRef.drawListDirty = true;
|
||||
worldRef.drawSortTimer = 0;
|
||||
}
|
||||
// Render builds the visible stack from spatial cells every frame; the old
|
||||
// drawListDirty periodic invalidation is no longer a render input.
|
||||
worldRef.drawSortTimer = 0;
|
||||
}
|
||||
|
||||
global.TarinaiMaintenanceSimulationSystem = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -63,17 +63,11 @@
|
|||
return { left: cameraX - margin, top: cameraY - margin, right: cameraX + vw + margin, bottom: cameraY + vh + margin };
|
||||
}
|
||||
|
||||
function pointInRect(x, y, rect) {
|
||||
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
||||
}
|
||||
|
||||
const helpers = Object.freeze({
|
||||
updateItemsScheduled,
|
||||
markSpatialDirtyIfMoved,
|
||||
markScheduledItemsMoved,
|
||||
trackSpatialEntityMoved,
|
||||
updateVisibleWorldRect,
|
||||
pointInRect,
|
||||
});
|
||||
|
||||
global.TarinaiSimulationRuntime = Object.freeze({
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
PIN_STATE_IDS,
|
||||
STAGE_IDS,
|
||||
ITEM_TYPE_IDS,
|
||||
itemTypeIndex,
|
||||
enumIndex,
|
||||
enumValue,
|
||||
} = SaveSchema;
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
return NEED_KEYS.map(k => q(needs[k], 1));
|
||||
}
|
||||
function arrayToNeeds(arr = []) {
|
||||
const out = typeof global.createDefaultNeeds === "function" ? global.createDefaultNeeds() : { food: 0, sleep: 0, health: 0, safety: 0, social: 0, fulfill: 0 };
|
||||
const out = global.createDefaultNeeds();
|
||||
NEED_KEYS.forEach((k, i) => { out[k] = u(arr[i], 1); });
|
||||
return out;
|
||||
}
|
||||
|
|
@ -85,7 +86,7 @@
|
|||
return out;
|
||||
}
|
||||
function geneticsToArray(g = {}) {
|
||||
const normalized = typeof global.normalizeGenetics === "function" ? global.normalizeGenetics(g, "") : g;
|
||||
const normalized = global.normalizeGenetics(g, "");
|
||||
return [
|
||||
q(Number(normalized?.lifeSpanMul) || 1, 100, 100),
|
||||
q(Number(normalized?.attackMul) || 1, 100, 100),
|
||||
|
|
@ -102,7 +103,7 @@
|
|||
sizeMul: u(arr[3], 100, 1),
|
||||
temperatureOffset: u(arr[4], 10, 0),
|
||||
};
|
||||
return typeof global.normalizeGenetics === "function" ? global.normalizeGenetics(out, "") : out;
|
||||
return global.normalizeGenetics(out, "");
|
||||
}
|
||||
function flagPack(t = {}) {
|
||||
let flags = 0;
|
||||
|
|
@ -290,16 +291,17 @@
|
|||
if (packed) return packed;
|
||||
}
|
||||
if (global.TarinaiPhysicsBodySystem.isPhysicsType(type)) return { pf: 2, missing: true };
|
||||
if (type === "duplicator") return [enumIndex(ITEM_TYPE_IDS, item.storedFoodType || "", -1)];
|
||||
if (type === "duplicator") return [itemTypeIndex(item.storedFoodType || "", -1)];
|
||||
if (type === "ball" || type === "balloon") return [q(item.vx, 10), q(item.vy, 10)];
|
||||
if (type === "ant_nest") return [q(item.antCount, 1), q(item.queenSpawnAt, 10)];
|
||||
if (type === "ant_corpse") return [q(item.decayTimer, 10)];
|
||||
if (type === "firecracker" || type === "flame_firecracker") return [q(item.fuseTimer, 10)];
|
||||
if (type === "sticky_bomb") return [q(item.fuseTimer, 10), tarinaiIndex.get(item.stickyBombCarrierId) ?? -1];
|
||||
if (type === "fire") return [q(item.flameLife, 10)];
|
||||
if (typeof global.isPinType === "function" && global.isPinType(type)) return [enumIndex(PIN_STATE_IDS, item.pinState || "loose"), tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
|
||||
if (type === "nest_box") return [q(item.burnTimer, 10), q(item.nestFireEvacuationTimer, 10), item.burning ? 1 : 0, item.nestFireEvacuated ? 1 : 0];
|
||||
if (global.isPinType(type)) return [enumIndex(PIN_STATE_IDS, item.pinState || "loose"), tarinaiIndex.get(item.pinTargetId) ?? -1, q(item.pinAttachAngle, 1000), q(item.pinAttachDistance, 10), q(item.pinOffsetY, 10)];
|
||||
if (type === "fan") return [
|
||||
q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)),
|
||||
q(item.angle, 1000, q(global.defaultItemAngle(type), 1000)),
|
||||
item.fanSwingOn ? 1 : 0,
|
||||
q(item.fanSwingCenter ?? item.angle ?? 0, 1000),
|
||||
q(item.fanSwingRange ?? (35 * Math.PI / 180), 1000),
|
||||
|
|
@ -307,12 +309,12 @@
|
|||
q(item.fanSwingPhase || 0, 1000),
|
||||
q(item.fanBodyAngle ?? item.angle ?? 0, 1000),
|
||||
];
|
||||
if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) return [q(item.angle, 1000, q(typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0, 1000)), type === "gate_fence" && item.gateOpen ? 1 : 0];
|
||||
if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) return [q(item.foodServingsRemaining ?? item.amount, 10), q(item.foodServingsMax, 10)];
|
||||
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 [];
|
||||
}
|
||||
function compactItem(item, index, tarinaiIndex = new Map(), itemIndex = new Map()) {
|
||||
const typeId = enumIndex(ITEM_TYPE_IDS, item?.type || "", -1);
|
||||
const typeId = itemTypeIndex(item?.type || "", -1);
|
||||
if (typeId < 0 || item?.type === "trace") return null;
|
||||
return [typeId, q(item?.x, 1), q(item?.y, 1), q(item?.amount ?? item?.hp, 10), itemExtra(item, tarinaiIndex, itemIndex)];
|
||||
}
|
||||
|
|
@ -371,26 +373,35 @@
|
|||
} else if (type === "firecracker" || type === "flame_firecracker") {
|
||||
item.fuseTimer = u(extra[0], 10); item.fuseMax = Math.max(item.fuseMax || 5, item.fuseTimer || 0);
|
||||
} else if (type === "sticky_bomb") {
|
||||
item.fuseTimer = u(extra[0], 10, item.fuseTimer || 6); item.fuseMax = Math.max(item.fuseMax || 6, item.fuseTimer || 0); item.stickyBombCarrierId = tarinaiList[extra[1]]?.id || "";
|
||||
item.fuseTimer = u(extra[0], 10, item.fuseTimer || 12); item.fuseMax = Math.max(item.fuseMax || 12, item.fuseTimer || 0); item.stickyBombCarrierId = tarinaiList[extra[1]]?.id || "";
|
||||
} else if (type === "fire") {
|
||||
item.flameLife = u(extra[0], 10, item.flameLife || 6);
|
||||
} else if (typeof global.isPinType === "function" && global.isPinType(type)) {
|
||||
} else if (type === "nest_box") {
|
||||
item.burnTimer = u(extra[0], 10, 0);
|
||||
item.nestFireEvacuationTimer = u(extra[1], 10, 0);
|
||||
item.burning = Boolean(extra[2]) || item.burnTimer > 0.02 || item.nestFireEvacuationTimer > 0.02;
|
||||
item.nestFireEvacuated = Boolean(extra[3]);
|
||||
if (item.burning) {
|
||||
item.flameWaterCooldown = 0;
|
||||
item.nestFireEffectCooldown = 0;
|
||||
}
|
||||
} else if (global.isPinType(type)) {
|
||||
item.pinState = enumValue(PIN_STATE_IDS, extra[0], "loose"); item.pinTargetId = tarinaiList[extra[1]]?.id || ""; item.pinAttachAngle = u(extra[2], 1000); item.pinAttachDistance = u(extra[3], 10); item.pinOffsetY = u(extra[4], 10);
|
||||
} else if (type === "fan") {
|
||||
const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
|
||||
item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback) : u(extra[0], 1000, fallback);
|
||||
const fallback = global.defaultItemAngle(type);
|
||||
item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback);
|
||||
item.fanSwingOn = Boolean(extra[1]);
|
||||
item.fanSwingCenter = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[2], 1000, item.angle), item.angle) : u(extra[2], 1000, item.angle);
|
||||
item.fanSwingCenter = global.normalizedItemAngle(u(extra[2], 1000, item.angle), item.angle);
|
||||
item.fanSwingRange = Math.max(0, Math.min(150 * Math.PI / 180, u(extra[3], 1000, 35 * Math.PI / 180)));
|
||||
item.fanSwingSpeed = Math.max(0, Math.min(360 * Math.PI / 180, u(extra[4], 1000, 48 * Math.PI / 180)));
|
||||
item.fanSwingPhase = u(extra[5], 1000, item.fanSwingPhase || 0);
|
||||
item.fanBodyAngle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[6], 1000, item.angle), item.angle) : u(extra[6], 1000, item.angle);
|
||||
} else if (typeof global.isRotatableItemType === "function" && global.isRotatableItemType(type)) {
|
||||
const fallback = typeof global.defaultItemAngle === "function" ? global.defaultItemAngle(type) : 0;
|
||||
item.angle = typeof global.normalizedItemAngle === "function" ? global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback) : u(extra[0], 1000, fallback);
|
||||
item.fanBodyAngle = global.normalizedItemAngle(u(extra[6], 1000, item.angle), item.angle);
|
||||
} else if (global.isRotatableItemType(type)) {
|
||||
const fallback = global.defaultItemAngle(type);
|
||||
item.angle = global.normalizedItemAngle(u(extra[0], 1000, fallback), fallback);
|
||||
if (type === "gate_fence") item.gateOpen = !!extra[1];
|
||||
} else if (typeof global.isServingFoodType === "function" && global.isServingFoodType(type)) {
|
||||
item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining; (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false);
|
||||
} else if (global.isServingFoodType(type)) {
|
||||
item.foodServingsRemaining = u(extra[0], 10, item.amount || 1); item.foodServingsMax = u(extra[1], 10, item.foodServingsRemaining || item.amount || 1); item.amount = item.foodServingsRemaining; global.updateServingFoodVisualSize(item);
|
||||
}
|
||||
}
|
||||
function createSnapshot(worldRef = global.world) {
|
||||
|
|
@ -404,8 +415,9 @@
|
|||
tarinaiLive.forEach((t, i) => { if (t.id) tarinaiIndex.set(t.id, i); if (t.familyKey) familyIndex.set(t.familyKey, i); });
|
||||
const itemRecords = [];
|
||||
for (const it of (worldRef.items || [])) {
|
||||
if (!it || it.dead || it.type === "trace" || enumIndex(ITEM_TYPE_IDS, it.type || "", -1) < 0) continue;
|
||||
const typeId = enumIndex(ITEM_TYPE_IDS, it.type || "", -1);
|
||||
if (!it || it.dead || it.type === "trace") continue;
|
||||
const typeId = itemTypeIndex(it.type || "", -1);
|
||||
if (typeId < 0) continue;
|
||||
itemRecords.push({ item: it, sortTypeId: typeId, sortX: q(it.x, 1), sortY: q(it.y, 1) });
|
||||
}
|
||||
itemRecords.sort((a, b) => (a.sortTypeId || 0) - (b.sortTypeId || 0) || mortonKeyXY(a.sortX, a.sortY) - mortonKeyXY(b.sortX, b.sortY) || String(a.item?.id || "").localeCompare(String(b.item?.id || "")));
|
||||
|
|
@ -503,8 +515,9 @@
|
|||
t.relationships = expandRelationships(t.__savedRelationships || [], worldRef.tarinai, worldRef);
|
||||
delete t.__savedPinIdx; delete t.__savedNestIdx; delete t.__savedRelationships; delete t.__savedStateInfo;
|
||||
}
|
||||
if (typeof global.updateNeeds === "function") {
|
||||
for (const t of worldRef.tarinai) global.updateNeeds(t, worldRef, 0);
|
||||
const updateNeedsRuntime = global.TarinaiNeedsRuntime?.updateNeeds;
|
||||
if (typeof updateNeedsRuntime === "function") {
|
||||
for (const t of worldRef.tarinai) updateNeedsRuntime(t, worldRef, 0);
|
||||
}
|
||||
worldRef.liveIdNext = Math.max(1, worldRef.tarinai.length + 1);
|
||||
worldRef.liveIdSerial = Math.max(1, worldRef.tarinai.length + 1);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
function triggerOwnedStructureDestroyedPanic(worldRef, structure, breaker = null, reason = "structure-destroyed") {
|
||||
if (!worldRef || !shouldPanicOwnerOnStructureGone(structure, reason)) return false;
|
||||
const owner = (worldRef.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
||||
const owner = worldRef.liveTarinaiById?.(structure.ownerId) || (worldRef.tarinai || []).find(t => t && !t.dead && t.id === structure.ownerId);
|
||||
if (!owner) return false;
|
||||
const label = ownedStructureDestroyedLabel(structure);
|
||||
const detail = `\u81EA\u5206\u306E${label}\u304C\u58CA\u3055\u308C\u305F`;
|
||||
|
|
@ -172,7 +172,6 @@
|
|||
removeDependentLinks(worldRef, structure, reason);
|
||||
if (ownerPanic && opts.panicOwner !== false) triggerOwnedStructureDestroyedPanic(worldRef, structure, opts.breaker || null, reason);
|
||||
markWorldAfterStructureChange(worldRef, reason);
|
||||
worldRef?.emit?.("structure:deleted", { structure, type: structure.type, reason });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +183,6 @@
|
|||
const changed = markStructureGone(bed, "grass-bed-wake-used");
|
||||
if (!changed) return false;
|
||||
markWorldAfterStructureChange(worldRef, "grass-bed-wake-used");
|
||||
worldRef?.emit?.("structure:usedUp", { structure: bed, user, reason: "wake" });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +194,6 @@
|
|||
if (!changed) return false;
|
||||
removeDependentLinks(worldRef, item, reason);
|
||||
markWorldAfterStructureChange(worldRef, reason, item, Math.max(36, Number(item.r || item.radius || 24) + 18));
|
||||
worldRef?.emit?.("item:deleted", { item, type: item.type, reason });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
}
|
||||
|
||||
owner(worldRef = this.world) {
|
||||
return (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === this.ownerId) || null;
|
||||
return worldRef?.liveTarinaiById?.(this.ownerId) || (worldRef?.tarinai || []).find(t => t && !t.dead && t.id === this.ownerId) || null;
|
||||
}
|
||||
|
||||
update(dt, worldRef = this.world) {
|
||||
|
|
@ -345,7 +345,6 @@
|
|||
structure.amount = Math.max(0.001, structure.hp);
|
||||
worldRef?.markItemBucketsDirty?.("grass-bed-owner-adopted");
|
||||
worldRef?.markTerrainDirty?.("grass-bed-owner-adopted");
|
||||
worldRef?.emit?.("structure:ownerAdopted", { structure, owner: user });
|
||||
}
|
||||
const ownerUse = user?.id && user.id === structure.ownerId;
|
||||
if (ownerUse) structure.attachment = Math.min(100, (structure.attachment || 0) + 2);
|
||||
|
|
@ -363,7 +362,6 @@
|
|||
structure.amount = Math.max(0.001, structure.hp || structure.amount || 1);
|
||||
worldRef?.markItemBucketsDirty?.("grass-bed-sleep-reserved");
|
||||
worldRef?.markTerrainDirty?.("grass-bed-sleep-reserved");
|
||||
worldRef?.emit?.("structure:useReserved", { structure, user });
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
|
@ -403,7 +401,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
global.TarinaiStructure = TarinaiStructure;
|
||||
global.StructureRegistry = StructureRegistry;
|
||||
global.damageNearbyStructures = damageNearbyStructures;
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ function createTemperatureComfortActionSpec() {
|
|||
},
|
||||
weight: 42,
|
||||
selectTarget(t, world, ctx = {}) {
|
||||
return world?.findComfortTemperatureSpot?.(t, { force: !!ctx.force, requireComfort: true }) || null;
|
||||
return world?.findComfortTemperatureSpot?.(t, { force: !!ctx.force, requireComfort: false }) || null;
|
||||
},
|
||||
canStart(t, world, ctx = {}) {
|
||||
const felt = world?.feltTemperatureFor?.(t) ?? world?.temperatureAt?.(t.x, t.y, t);
|
||||
|
|
@ -239,7 +239,7 @@ function createTemperatureComfortActionSpec() {
|
|||
t.goIdle?.("\u5FEB\u9069\u306A\u6E29\u5EA6\u306B\u306A\u3063\u305F");
|
||||
return "finished";
|
||||
}
|
||||
const target = t.target && !t.target.dead ? t.target : (ctx.target ?? this.selectTarget(t, world, { force: true, requireComfort: true }));
|
||||
const target = t.target && !t.target.dead ? t.target : (ctx.target ?? this.selectTarget(t, world, { force: true, requireComfort: false }));
|
||||
if (!target) return false;
|
||||
t.target = target;
|
||||
if (distXY(t.x, t.y, target.x, target.y) < Math.max(24, (t.radius || 20) * 0.9)) {
|
||||
|
|
@ -252,7 +252,7 @@ function createTemperatureComfortActionSpec() {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
const next = this.selectTarget(t, world, { force: true, requireComfort: true });
|
||||
const next = this.selectTarget(t, world, { force: true, requireComfort: false });
|
||||
if (!next || distXY(t.x, t.y, next.x, next.y) < Math.max(28, (t.radius || 20))) return "finished";
|
||||
t.target = next;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@
|
|||
target.singleUsePending = true;
|
||||
target.singleUseConsumedById = this.id || target.singleUseConsumedById || "";
|
||||
}
|
||||
const targetIsNestContainer = Boolean(target && (typeof global.isNestContainerType === "function" ? global.isNestContainerType(target.type) : (target.type === "nest_box" || target.type === "pipe")));
|
||||
const targetIsNestContainer = Boolean(target && global.TarinaiToolRuntime?.isNestContainerItem?.(target, { alive: false }));
|
||||
if (target && !targetIsNestContainer) {
|
||||
this._sleepExternalMotionUntil = 0;
|
||||
this._sleepExternalWakeSuppressUntil = Math.max(Number(this._sleepExternalWakeSuppressUntil || 0) || 0, now + 2.4);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
function fulfillPrimaryReason(tarinai, action = null) {
|
||||
const behavior = currentTarinaiBehavior(tarinai);
|
||||
const actionId = action?.id || behavior?.actionId || "";
|
||||
|
|
@ -100,6 +101,9 @@ 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 "気分がいいので、日光浴している。";
|
||||
}
|
||||
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);
|
||||
const status = tarinai?.world?.temperatureStatusFor?.(felt, tarinai) || tarinai?.temperatureStatus || null;
|
||||
|
|
@ -200,5 +204,9 @@ function refreshBehaviorReasonText(tarinai, action = null) {
|
|||
return patchBehaviorTextFromComposer(tarinai, behavior, { action });
|
||||
}
|
||||
|
||||
|
||||
|
||||
Object.assign(global, {
|
||||
buildReasonText,
|
||||
composeTarinaiBehaviorText,
|
||||
refreshBehaviorReasonText,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -55,10 +55,6 @@ function forceBuiltStructureVisualRefresh(world, structure, reason = "structure-
|
|||
if (structure) {
|
||||
structure.world = world;
|
||||
}
|
||||
if (typeof globalThis.render === "function") {
|
||||
globalThis.render();
|
||||
globalThis.requestAnimationFrame?.(() => globalThis.render?.());
|
||||
}
|
||||
}
|
||||
|
||||
function findStructureBuildSpot(t, world, type) {
|
||||
|
|
@ -108,25 +104,42 @@ function grassMaterialAvailableStages(material) {
|
|||
return Math.max(0, Math.floor(Number(stage) || 0) + 1);
|
||||
}
|
||||
|
||||
function reservedBuildGrassStages(world, material, type, self = null) {
|
||||
if (!world || !material) return 0;
|
||||
const id = material.id || "";
|
||||
if (!id) return 0;
|
||||
let reserved = 0;
|
||||
function buildGrassReservationIndex(world, self = null) {
|
||||
const index = new Map();
|
||||
if (!world) return index;
|
||||
for (const other of world.tarinai || []) {
|
||||
if (!other || other.dead || other === self) continue;
|
||||
const plan = other.buildPlan || null;
|
||||
const planType = plan?.type || (/build_plushie/.test(other.behavior?.actionId || "") ? "plushie" : (/build_grass_bed/.test(other.behavior?.actionId || "") ? "grass_bed" : ""));
|
||||
if (type && planType && planType !== type) continue;
|
||||
const targetId = plan?.materialId || other.target?.id || "";
|
||||
if (targetId !== id) continue;
|
||||
if (!targetId) continue;
|
||||
const state = String(other.state || "");
|
||||
const actionId = String(other.behavior?.actionId || other.currentActionId || "");
|
||||
if (state === "seek_material" || state === "build" || /build_grass_bed|build_plushie|seek_material/.test(actionId)) {
|
||||
reserved += structureGrassStageCost(planType || type);
|
||||
if (state !== "seek_material" && state !== "build" && !/build_grass_bed|build_plushie|seek_material/.test(actionId)) continue;
|
||||
const cost = structureGrassStageCost(planType || "");
|
||||
if (cost <= 0) continue;
|
||||
let rec = index.get(targetId);
|
||||
if (!rec) {
|
||||
rec = { total: 0, byType: Object.create(null), unknownReservations: 0 };
|
||||
index.set(targetId, rec);
|
||||
}
|
||||
rec.total += cost;
|
||||
if (planType) rec.byType[planType] = (rec.byType[planType] || 0) + cost;
|
||||
else rec.unknownReservations += 1;
|
||||
}
|
||||
return reserved;
|
||||
return index;
|
||||
}
|
||||
|
||||
function reservedBuildGrassStages(world, material, type, self = null, reservationIndex = null) {
|
||||
if (!world || !material) return 0;
|
||||
const id = material.id || "";
|
||||
if (!id) return 0;
|
||||
const index = reservationIndex || buildGrassReservationIndex(world, self);
|
||||
const rec = index.get(id);
|
||||
if (!rec) return 0;
|
||||
if (!type) return rec.total || 0;
|
||||
const unknownCost = (rec.unknownReservations || 0) * structureGrassStageCost(type);
|
||||
return (rec.byType?.[type] || 0) + unknownCost;
|
||||
}
|
||||
|
||||
function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) {
|
||||
|
|
@ -134,13 +147,14 @@ function findNearbyBuildMaterial(world, tarinai, type, maxDist = 520) {
|
|||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
const candidates = world?.nearbyItems?.(tarinai.x, tarinai.y, maxDist + 40, true) || world?.items || [];
|
||||
const reservationIndex = buildGrassReservationIndex(world, tarinai);
|
||||
for (const item of candidates) {
|
||||
if (!item || item.dead || !roleMatches(item, "grassMaterial")) continue;
|
||||
if (tarinai?.shouldAvoidTarget?.(item)) continue;
|
||||
if (world?.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65))) continue;
|
||||
const available = grassMaterialAvailableStages(item);
|
||||
if (available < required) continue;
|
||||
const reserved = reservedBuildGrassStages(world, item, type, tarinai);
|
||||
const reserved = reservedBuildGrassStages(world, item, type, tarinai, reservationIndex);
|
||||
if (available - reserved < required) continue;
|
||||
const d = dist(tarinai, item);
|
||||
if (d > maxDist) continue;
|
||||
|
|
|
|||
|
|
@ -5,11 +5,37 @@
|
|||
const Tarinai = global.Tarinai;
|
||||
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_disease_nest.js");
|
||||
function isNestContainerItem(item) {
|
||||
return Boolean(item && !item.dead && (typeof global.isNestContainerType === "function" ? global.isNestContainerType(item.type) : (item.type === "nest_box" || item.type === "pipe")));
|
||||
return Boolean(global.TarinaiToolRuntime?.isNestContainerItem?.(item));
|
||||
}
|
||||
function containerLabel(item) { return item?.type === "pipe" ? "\u571f\u7ba1" : "\u5de3\u7bb1"; }
|
||||
function containerInsideSleepText(item, verb = "\u7720\u3063\u3066\u3044\u308b") { return `${containerLabel(item)}\u306e\u4e2d\u3067${verb}`; }
|
||||
function containerFullText(item) { return `${containerLabel(item)}\u304c\u3044\u3063\u3071\u3044\u3067\u5165\u308c\u306a\u3044`; }
|
||||
function targetShelterItem(target = null) {
|
||||
return isNestContainerItem(target?.shelterItem) ? target.shelterItem : (isNestContainerItem(target?.hostItem) ? target.hostItem : null);
|
||||
}
|
||||
function targetReferencesNestBox(target = null, box = null) {
|
||||
if (!box || !isNestContainerItem(box)) return false;
|
||||
return target === box || targetShelterItem(target) === box;
|
||||
}
|
||||
function zunchiDiseaseTemperatureInfectionMultiplier(actor = null) {
|
||||
const worldRef = actor?.world || global.world || null;
|
||||
if (!actor || !worldRef) return 1;
|
||||
const felt = worldRef.feltTemperatureFor?.(actor) ?? worldRef.temperatureAt?.(actor.x, actor.y, actor) ?? actor.feltTemperature ?? actor.tempComfort ?? (CONFIG.standardTemperature ?? 15);
|
||||
const status = worldRef.temperatureStatusFor?.(felt, actor) || null;
|
||||
if (!status || status.comfortable) return 1;
|
||||
const discomfort = Math.max(0, Number(status.discomfort || 0) || 0);
|
||||
if (status.direction === "hot") return clamp(1 + discomfort * 0.070, 1, 2.85);
|
||||
if (status.direction === "cold") return clamp(1 - discomfort * 0.030, 0.42, 1);
|
||||
return 1;
|
||||
}
|
||||
if (typeof global !== "undefined") global.zunchiDiseaseTemperatureInfectionMultiplier = zunchiDiseaseTemperatureInfectionMultiplier;
|
||||
function temperatureShelterStillUseful(t, box = null) {
|
||||
if (!t || !box || !t.world) return false;
|
||||
const now = Number(t.world.time || 0) || 0;
|
||||
if (now < Number(t._nestBoxShelterUntil || 0)) return true;
|
||||
const status = t.world.temperatureStatusFor?.(t.world.feltTemperatureFor?.(t) ?? t.tempComfort ?? t.feltTemperature ?? (CONFIG.standardTemperature ?? 15), t);
|
||||
return Boolean(status && !status.comfortable && (status.discomfort || 0) >= 1.2 && targetReferencesNestBox(t.target, box));
|
||||
}
|
||||
|
||||
Object.defineProperties(Tarinai.prototype, Object.getOwnPropertyDescriptors({
|
||||
infectZunchiDisease(source = null, force = false) {
|
||||
|
|
@ -23,7 +49,6 @@
|
|||
this.thought = "\u305a\u3093\u3061\u75c5\u306b\u304b\u304b\u3063\u305f";
|
||||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "\u3046\u3045\u2026", "rgba(38,82,38,0.82)");
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_zunchi", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u305a\u3093\u3061\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -38,19 +63,18 @@
|
|||
this.thought = reason || "\u305a\u3093\u3061\u75c5\u304c\u6cbb\u307e\u3063\u305f";
|
||||
audio.heal?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(62,104,62,0.78)");
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_zunchi", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u305a\u3093\u3061\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
||||
emitZunchiDiseaseEffect(dt) {
|
||||
if (!this.zunchiDisease || !this.world?.effects) return;
|
||||
if (deterministicChance(this.world, "zunchi-disease-miasma", dt * (1.2 + (this.zunchiDiseaseSeverity || 0) * 1.4), this)) {
|
||||
if (deterministicChance(this.world, "zunchi-disease-miasma", dt * (0.48 + (this.zunchiDiseaseSeverity || 0) * 0.62), this)) {
|
||||
this.world.effects.push(new Effect("zunchi_miasma", this.x + deterministicRange(this.world, "zunchi-miasma-x", -this.radius * 0.45, this.radius * 0.45, this), this.y - this.radius * deterministicRange(this.world, "zunchi-miasma-y", 0.05, 0.72, this), {
|
||||
vx: deterministicRange(this.world, "zunchi-miasma-vx", -5, 5, this),
|
||||
vy: deterministicRange(this.world, "zunchi-miasma-vy", -10, -3, this),
|
||||
life: deterministicRange(this.world, "zunchi-miasma-life", 1.4, 2.4, this),
|
||||
size: deterministicRange(this.world, "zunchi-miasma-size", 8, 18, this) * (0.8 + (this.zunchiDiseaseSeverity || 0) * 0.45),
|
||||
life: deterministicRange(this.world, "zunchi-miasma-life", 1.6, 2.8, this),
|
||||
size: deterministicRange(this.world, "zunchi-miasma-size", 12, 26, this) * (1.0 + (this.zunchiDiseaseSeverity || 0) * 0.55),
|
||||
color: "rgba(16,78,28,0.70)",
|
||||
}));
|
||||
}
|
||||
|
|
@ -63,7 +87,9 @@
|
|||
this.zunchiStain = clamp(this.zunchiStain - dt * 2.4, 0, 100);
|
||||
if (this.zunchiDisease) this.zunchiDiseaseSeverity = Math.max(0, (this.zunchiDiseaseSeverity || 1) - dt * 0.006);
|
||||
}
|
||||
const zunchiSelfInfectChance = this.diseaseChance ? this.diseaseChance(dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045) : dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045;
|
||||
const zunchiTempInfectMul = zunchiDiseaseTemperatureInfectionMultiplier(this);
|
||||
const zunchiSelfInfectBase = dt * clamp((this.zunchiStain - 82) / 18, 0, 1) * 0.045 * zunchiTempInfectMul;
|
||||
const zunchiSelfInfectChance = this.diseaseChance ? this.diseaseChance(zunchiSelfInfectBase) : zunchiSelfInfectBase;
|
||||
if (!this.zunchiDisease && this.zunchiStain > 82 && deterministicChance(this.world, "zunchi-self-infect", zunchiSelfInfectChance, this)) {
|
||||
this.infectZunchiDisease(null, true);
|
||||
this.world?.log?.(`${this.name}\u306f\u305a\u3093\u3061\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] });
|
||||
|
|
@ -93,7 +119,6 @@
|
|||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "Zzz...", "rgba(72,76,120,0.76)");
|
||||
this.world?.log?.(`${this.name}\u306f\u306d\u3080\u308a\u75c5\u306b\u304b\u304b\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_sleep", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u306d\u3080\u308a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -111,7 +136,6 @@
|
|||
this.surpriseTimer = Math.max(this.surpriseTimer || 0, 0.65);
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "!", "rgba(72,76,120,0.76)");
|
||||
this.world?.log?.(`${this.name}\u306e\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_sleep", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u306d\u3080\u308a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -125,7 +149,6 @@
|
|||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.2, "!", "rgba(178,78,42,0.82)");
|
||||
this.world?.log?.(`${this.name}\u306f\u7206\u767a\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_explosion", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u7206\u767a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -139,7 +162,6 @@
|
|||
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -22, safety: -8 });
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
|
||||
this.world?.log?.(`${this.name}\u306e\u7206\u767a\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "accident", { participants: [this] });
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_explosion", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u7206\u767a\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -157,7 +179,6 @@
|
|||
audio.disease?.();
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.20, "?", "rgba(150,78,44,0.80)");
|
||||
this.world?.log?.(`${this.name}\u306f\u304d\u305a\u3064\u304d\u75c5\u3092\u767a\u75c7\u3057\u305f\u3002`, "fight", { participants: [this] });
|
||||
this.world?.emit?.("disease:infected", { tarinai: this, diseaseId: "disease_fight", source });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u767a\u75c7", "\u304d\u305a\u3064\u304d\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -171,7 +192,6 @@
|
|||
if (typeof applyNeedRelief === "function") applyNeedRelief(this, { health: -24, safety: -10 });
|
||||
this.world?.spawnBubble?.(this.x, this.y - this.radius * 1.18, "\u306f\u3046", "rgba(82,126,88,0.76)");
|
||||
this.world?.log?.(`${this.name}\u306e\u304d\u305a\u3064\u304d\u75c5\u304c\u6cbb\u3063\u305f\u3002`, "fight", { participants: [this] });
|
||||
this.world?.emit?.("disease:cured", { tarinai: this, diseaseId: "disease_fight", reason });
|
||||
this.recordChangeCause?.("\u75c5\u6c17\u56de\u5fa9", "\u304d\u305a\u3064\u304d\u75c5");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -251,16 +271,22 @@
|
|||
if (!isNestContainerItem(box)) return false;
|
||||
const now = this.world?.time || 0;
|
||||
if (this._nestBoxAvoidId === box.id && now < (this._nestBoxAvoidUntil || -Infinity)) return false;
|
||||
if (this.insideNestBoxId === box.id && (this.world?.time || 0) < (this.nestBoxStayUntil || -Infinity) && (this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return true;
|
||||
if (!(this.state === "sleep" || this.state === "seek_bed" || this.sleeping)) return false;
|
||||
return this.target === box || this.insideNestBoxId === box.id;
|
||||
const sleepIntent = this.state === "sleep" || this.state === "seek_bed" || this.sleeping;
|
||||
const temperatureIntent = this.state === "seek_temperature" || targetReferencesNestBox(this.target, box) || temperatureShelterStillUseful(this, box);
|
||||
if (this.insideNestBoxId === box.id && now < (this.nestBoxStayUntil || -Infinity) && (sleepIntent || temperatureIntent)) return true;
|
||||
if (this.insideNestBoxId === box.id && temperatureShelterStillUseful(this, box)) return true;
|
||||
if (!(sleepIntent || temperatureIntent)) return false;
|
||||
return targetReferencesNestBox(this.target, box) || this.insideNestBoxId === box.id;
|
||||
},
|
||||
|
||||
enterNestBox(box, dt = 0.016, options = {}) {
|
||||
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;
|
||||
}
|
||||
const capacity = this.world?.nestBoxCapacity ? this.world.nestBoxCapacity(box) : 5;
|
||||
let occupants = this.world?.nestBoxOccupants ? this.world.nestBoxOccupants(box, Infinity) : [];
|
||||
const alreadyInside = this.insideNestBoxId === box.id;
|
||||
const now = this.world?.time || 0;
|
||||
if (!alreadyInside && occupants.length >= capacity) {
|
||||
const kingSleepDisplace = Boolean(this.isTarinaiChampion && box.type === "nest_box" && (this.state === "seek_bed" || this.state === "sleep" || this.sleeping));
|
||||
|
|
@ -279,11 +305,15 @@
|
|||
}
|
||||
if (occupants.length >= capacity) return this.abortNestBoxEntry?.(box, containerFullText(box)) || false;
|
||||
}
|
||||
const shelterPurpose = options?.purpose === "temperature" || this.state === "seek_temperature" || targetShelterItem(this.target) === box;
|
||||
if (shelterPurpose) {
|
||||
this._nestBoxShelterUntil = Math.max(Number(this._nestBoxShelterUntil || 0) || 0, now + 12);
|
||||
}
|
||||
if (!alreadyInside) {
|
||||
this._nestBoxSeekStartedAt = NaN;
|
||||
this._nestBoxSeekTargetId = "";
|
||||
const entry = this.world?.nestBoxEntryPoint ? this.world.nestBoxEntryPoint(box) : { x: box.x, y: box.y };
|
||||
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, now + 14);
|
||||
this.nestBoxStayUntil = Math.max(this.nestBoxStayUntil || -Infinity, now + (shelterPurpose ? 12 : 14));
|
||||
this.nestBoxSlotIndex = Math.min(occupants.length, Math.max(0, capacity - 1));
|
||||
this._nestBoxEntryStartX = Number.isFinite(this.x) ? this.x : entry.x;
|
||||
this._nestBoxEntryStartY = Number.isFinite(this.y) ? this.y : entry.y;
|
||||
|
|
@ -355,14 +385,40 @@
|
|||
this._nestBoxExitEndY = Number.isFinite(exit.y) ? exit.y : this._nestBoxExitDoorY;
|
||||
this._nestBoxExitDuration = 0.58;
|
||||
this._nestBoxExitSmoothUntil = now + this._nestBoxExitDuration;
|
||||
if (b?.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(b)) {
|
||||
this._burningNestExitBoxId = b.id || "";
|
||||
this._burningNestExitUntil = Math.max(Number(this._burningNestExitUntil || 0) || 0, now + this._nestBoxExitDuration + 1.2);
|
||||
}
|
||||
this.nestFade = Math.max(this.nestFade || 0, 0.96);
|
||||
return true;
|
||||
},
|
||||
|
||||
forceIgniteFromBurningNestExit(box = null) {
|
||||
const now = Number(this.world?.time || 0) || 0;
|
||||
const b = box || this.nestBoxById?.(this._burningNestExitBoxId);
|
||||
const active = Boolean(b?.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(b));
|
||||
const taggedExit = Boolean(this._burningNestExitBoxId && now < Number(this._burningNestExitUntil || 0));
|
||||
if (!active && !taggedExit) return false;
|
||||
if (!this.burning || !(this.burnTimer > 0.02)) {
|
||||
if (typeof this.igniteFire === "function") this.igniteFire(b || box || null, true);
|
||||
else {
|
||||
const text = globalThis.TarinaiItemDynamicToolSystem?.fireBurningText || "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002";
|
||||
this.burning = true;
|
||||
this.burnTimer = Math.max(Number(this.burnTimer || 0) || 0, 5.2);
|
||||
this.thought = text;
|
||||
this.sleeping = false;
|
||||
this.setActionState?.("panic", { target: null, reason: text, wake: true, sleeping: false });
|
||||
}
|
||||
}
|
||||
this._burningNestExitUntil = Math.max(Number(this._burningNestExitUntil || 0) || 0, now + 0.75);
|
||||
return true;
|
||||
},
|
||||
|
||||
updateNestBoxExitAnimation(dt = 0.016) {
|
||||
const now = this.world?.time || 0;
|
||||
const until = this._nestBoxExitSmoothUntil || -Infinity;
|
||||
if (!(now < until)) return false;
|
||||
if (this._burningNestExitBoxId && now < Number(this._burningNestExitUntil || 0)) this.forceIgniteFromBurningNestExit?.(this.nestBoxById?.(this._burningNestExitBoxId));
|
||||
const duration = Math.max(0.20, Number(this._nestBoxExitDuration || 0.58) || 0.58);
|
||||
const progress = clamp(1 - Math.max(0, until - now) / duration, 0, 1);
|
||||
const eased = progress * progress * (3 - 2 * progress);
|
||||
|
|
@ -416,6 +472,7 @@
|
|||
this.sleepSession = null;
|
||||
if (this.state === "sleep" || this.state === "seek_bed") this.goIdle?.(`${containerLabel(b)}\u304b\u3089\u51fa\u305f`);
|
||||
this.nestBoxStayUntil = -Infinity;
|
||||
this._nestBoxShelterUntil = 0;
|
||||
this.nestBoxSlotIndex = -1;
|
||||
this._nestBoxEntrySmoothUntil = -Infinity;
|
||||
this._nestBoxEntryStartX = NaN;
|
||||
|
|
@ -425,13 +482,20 @@
|
|||
this._nestBoxEntryDuration = 0;
|
||||
this._nestBoxSeekStartedAt = NaN;
|
||||
this._nestBoxSeekTargetId = "";
|
||||
if (b?.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(b)) {
|
||||
const now = Number(this.world?.time || 0) || 0;
|
||||
this._burningNestExitBoxId = b.id || this._burningNestExitBoxId || "";
|
||||
this._burningNestExitUntil = Math.max(Number(this._burningNestExitUntil || 0) || 0, now + 1.4);
|
||||
this.forceIgniteFromBurningNestExit?.(b);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
updateNestBoxPresence(dt = 0) {
|
||||
if (this.updateNestBoxExitAnimation?.(dt || 0.016)) return null;
|
||||
const currentBox = this.nestBoxById(this.insideNestBoxId);
|
||||
const targetBox = isNestContainerItem(this.target) ? this.target : null;
|
||||
const shelterBox = targetShelterItem(this.target);
|
||||
const targetBox = isNestContainerItem(this.target) ? this.target : shelterBox;
|
||||
const box = currentBox || targetBox;
|
||||
if (!box) {
|
||||
this.insideNestBoxId = null;
|
||||
|
|
@ -533,7 +597,6 @@
|
|||
this.nextBurnVoiceAt = now + 2.6;
|
||||
this.bubble?.("\u3042\u3061\u3085\u3044\uff01", 1.35, "rgba(214,82,38,0.90)", { force: true });
|
||||
}
|
||||
this.world?.emit?.("fire:ignite", { tarinai: this, source });
|
||||
this.recordChangeCause?.("\u708e\u4e0a", "\u708e\u4e0a");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -546,7 +609,6 @@
|
|||
this.hurtTimer = Math.max(this.hurtTimer || 0, 0.35);
|
||||
if (reason) this.thought = reason;
|
||||
this.world?.effects?.push(new Effect("ring", this.x, this.y, { size: Math.max(22, this.radius * 1.2), life: 0.22, color: "rgba(108,192,236,0.60)" }));
|
||||
this.world?.emit?.("fire:extinguish", { tarinai: this, reason });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
@ -562,8 +624,24 @@
|
|||
this.fearTimer = Math.max(this.fearTimer || 0, 0.8);
|
||||
this.sleeping = false;
|
||||
this.eatTimer = 0;
|
||||
const currentNest = this.nestBoxById?.(this.insideNestBoxId);
|
||||
if (currentNest) {
|
||||
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("水で消火された");
|
||||
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("自然に火が消えた");
|
||||
return;
|
||||
}
|
||||
if (this.burnTimer <= 0.01) this.extinguishFire("自然に火が消えた");
|
||||
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, "\u708e\u4e0a");
|
||||
if (typeof this.damage === "function") this.damage(dt * 3.8, "炎上");
|
||||
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));
|
||||
|
|
@ -578,12 +656,12 @@
|
|||
return;
|
||||
}
|
||||
for (const viewer of [...(this.world?.nearbyTarinai?.(this.x, this.y, 190, true) || [])]) {
|
||||
if (!viewer || viewer === this || viewer.dead) continue;
|
||||
if (!viewer || viewer === this || viewer.dead || viewer.insideNestBoxId || this.world?.isTarinaiHiddenInNestBox?.(viewer)) continue;
|
||||
globalThis.TarinaiItemDynamicToolSystem?.panicTarinaiFromFire?.(viewer, this, this.world);
|
||||
}
|
||||
const contactRadius = Math.max(20, (this.radius || 18) * 1.1);
|
||||
for (const other of [...(this.world?.nearbyTarinai?.(this.x, this.y, contactRadius + 28, true) || [])]) {
|
||||
if (!other || other === this || other.dead) continue;
|
||||
if (!other || other === this || other.dead || other.insideNestBoxId || this.world?.isTarinaiHiddenInNestBox?.(other)) continue;
|
||||
if (distXY(this.x, this.y, other.x, other.y) <= contactRadius + Math.max(other.radius || 18, 12) * 0.72) other.igniteFire?.(this, false);
|
||||
}
|
||||
for (const ant of [...(this.world?.nearbyAnts?.(this.x, this.y, contactRadius + 18, true) || [])]) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
if (!it.storedFoodType) return 0;
|
||||
const value = Number.isFinite(nutrition) ? nutrition : (window.TarinaiItemRegistry.food?.nutrition?.(it.storedFoodType, 0) ?? 42);
|
||||
it.usedCount = (it.usedCount || 0) + 1;
|
||||
this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.storedFoodType, sourceType: "duplicator", nutrition: value });
|
||||
return value;
|
||||
}
|
||||
this.ensureFoodServings(it);
|
||||
|
|
@ -51,7 +50,6 @@
|
|||
this.world?.markSpatialDirty?.("food-serving-resize");
|
||||
this.world && (this.world.drawListDirty = true);
|
||||
}
|
||||
this.world?.emit?.("item:eaten", { tarinai: this, item: it, type: it.type, nutrition: value });
|
||||
return value;
|
||||
},
|
||||
foodHungerReliefFor(type = "", nutrition = 0, fallbackMultiplier = 0) {
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ function setBehaviorFromAction(tarinai, action, choice = {}, reasonText = "", op
|
|||
lockSeconds: options.lockSeconds,
|
||||
minDuration: options.minDuration,
|
||||
phase: options.phase || "starting",
|
||||
data: options.leisure ? { leisure: true } : options.data,
|
||||
specId: action.id,
|
||||
specKind: action.kind || "need_action",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -382,7 +382,7 @@
|
|||
},
|
||||
|
||||
targetIgnoresFence(target = this.target) {
|
||||
const targetIsContainer = Boolean(target && (typeof isNestContainerType === "function" ? isNestContainerType(target.type) : (target.type === "nest_box" || target.type === "pipe")));
|
||||
const targetIsContainer = Boolean(target && globalThis.TarinaiToolRuntime?.isNestContainerItem?.(target, { alive: false }));
|
||||
return Boolean(target?.type === "sweet" || targetIsContainer || (target?.type === "bed" && (this.state === "seek_bed" || this.state === "sleep")) || this.state === "panic" || this.state === "fight" || this.state === "intimidate" || this.state === "birth_ritual" || this.state === "cursor_enemy");
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@
|
|||
if (!(id in this.itemEffectTimers)) this.itemEffectTimers[id] = 0;
|
||||
this.itemEffectTimers[id] = Math.max(this.itemEffectTimers[id] || 0, Number(seconds) || 0);
|
||||
if (id === "laxative") this.laxativePoopTimer = Math.min(Math.max(this.laxativePoopTimer || 3, 0.2), 3);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: id, duration: this.itemEffectTimers[id] || 0 });
|
||||
recordItemChange(this, id, "\u6642\u9593\u52b9\u679c");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -113,7 +112,6 @@
|
|||
this.powerEffectKind = mode;
|
||||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 3.2);
|
||||
this.spawnPowerItemEffect(mode, 1.5);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: mode, permanent: true });
|
||||
recordItemChange(this, mode, "\u80fd\u529b\u5909\u5316");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -127,7 +125,6 @@
|
|||
this.powerEffectKind = mode;
|
||||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 4.0);
|
||||
this.spawnPowerItemEffect(mode, mode === "giant_drug" ? 1.9 : 1.2);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: mode, permanent: true });
|
||||
recordItemChange(this, mode, "\u4f53\u683c\u5909\u5316");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -144,7 +141,6 @@
|
|||
this.powerEffectKind = "mercury";
|
||||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.7);
|
||||
this.spawnPowerItemEffect("mercury", 0.95);
|
||||
this.world?.emit?.("effect:applied", { tarinai: this, effectId: "mercury", permanent: true, multiplier: this.mercuryLifeMultiplier });
|
||||
recordItemChange(this, "mercury", "\u5bff\u547d\u5909\u5316");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -169,7 +165,6 @@
|
|||
this.powerEffectPulse = Math.max(this.powerEffectPulse || 0, 2.2);
|
||||
this.refreshEffectiveSize();
|
||||
this.spawnPowerItemEffect("zunda_juice", 1.4);
|
||||
this.world?.emit?.("effect:removed", { tarinai: this, effectId: "all_removable", source: "zunda_juice" });
|
||||
recordItemChange(this, "zunda_juice", "\u52b9\u679c\u89e3\u9664");
|
||||
return true;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -161,17 +161,24 @@ function findNearestItemWithRole(world, tarinai, role, maxDist = 520) {
|
|||
const primary = (foodLikeRole && world?.nearbyFood)
|
||||
? world.nearbyFood(tarinai.x, tarinai.y, searchDist, true)
|
||||
: (world?.nearbyItems?.(tarinai.x, tarinai.y, searchDist, true) || world?.items || []);
|
||||
const candidates = Array.isArray(primary) ? primary.slice() : [];
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (it) => {
|
||||
if (it && !it.dead && !candidates.includes(it)) candidates.push(it);
|
||||
if (it && !it.dead && !seen.has(it)) {
|
||||
seen.add(it);
|
||||
candidates.push(it);
|
||||
}
|
||||
};
|
||||
if (Array.isArray(primary)) for (const it of primary) addCandidate(it);
|
||||
|
||||
// \u98DF\u6599\u30FB\u6C34\u30FB\u85AC\u306F\u3001\u901A\u5E38\u6642\u304B\u3089\u7A7A\u9593\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u3060\u3051\u306B\u4F9D\u5B58\u3057\u306A\u3044\u3002
|
||||
// \u751F\u6210\u76F4\u5F8C/\u79FB\u52D5\u76F4\u5F8C\u306E\u30A2\u30A4\u30C6\u30E0\u304C nearbyFood \u306E\u30AD\u30E3\u30C3\u30B7\u30E5\u304B\u3089\u6F0F\u308C\u3066\u3082\u3001\u8DDD\u96E2\u3068 roleMatches \u3067\u6700\u7D42\u5224\u5B9A\u3059\u308B\u3002
|
||||
// 食料・水・薬は、生成直後/移動直後の spatial cache 漏れにも耐える。
|
||||
// item type buckets が使える場合は role ごとの型だけを見ることで、
|
||||
// crowded world で毎回 world.items 全体を足す経路を避ける。
|
||||
if (foodLikeRole) {
|
||||
if (Array.isArray(world?.items)) for (const it of world.items) addCandidate(it);
|
||||
if (world?.itemsOfType) {
|
||||
for (const type of foodRoleSearchTypes(role)) for (const it of world.itemsOfType(type) || []) addCandidate(it);
|
||||
} else if (Array.isArray(world?.items)) {
|
||||
for (const it of world.items) addCandidate(it);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@
|
|||
|
||||
// Need planning runtime and Tarinai prototype extensions.
|
||||
// Targeting, ActionSpec definitions, consumables, social ticks, and building are split into dedicated modules.
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, { createDefaultNeeds, quantizeNeed, updateNeeds, chooseTopNeedRandom, buildReasonText, formatCauseActionText, forcedCausePhrase, refreshBehaviorReasonText, fulfillPrimaryReason, getNeedDisplayValue, findNearestItemWithRole, findNearestSleepPlace, findPrioritySleepPlaceBeforeGrassBedBuild, findNearbyDanger, findNearbyMaterial, evaluateConflictUrge, conflictTargetFor, calculateStressFromNeeds, applyGroundStressModifier, applyNeedShock, applyNeedRelief, triggerNeedShockReaction, applyNeedSatisfaction, synchronizeActionTextFromState, setBehaviorText, setBehavior, clearBehavior, behaviorText, queueForcedTarinaiBehavior, clearForcedBehaviorQueue, processForcedBehaviorQueue, forcedBehaviorQueueOf, currentForcedBehaviorRequest, actionPriority, forcedBehaviorSourcePriority, completeForcedBehavior, retryForcedBehavior, isLiveTarinaiEntity, actionById });
|
||||
}
|
||||
// This file keeps its local needs-loop helpers private; cross-file helpers come
|
||||
// from their owning modules loaded earlier in index.html.
|
||||
(function (global) {
|
||||
|
||||
function decayNeedSatisfaction(tarinai, dt = 0) {
|
||||
if (!tarinai) return;
|
||||
|
|
@ -32,24 +31,43 @@ function decayNeedShock(tarinai, dt = 0) {
|
|||
|
||||
function circadianSleepPhase(world) {
|
||||
const p = world?.dayProgress?.() || 0;
|
||||
const night = p >= 0.66 || p < 0.18;
|
||||
const afternoon = p >= 0.46 && p < 0.58;
|
||||
return { p, night, afternoon };
|
||||
// Sleep pressure should visibly rise before dusk and fade around dawn.
|
||||
// Keep the behavior controlled by need deltas rather than forcing sleep/wake
|
||||
// at a fixed clock time. The steep increase begins before the evening
|
||||
// phase, and sleeping reduces the pressure slowly enough to settle near dawn.
|
||||
const sunsetHour = 17;
|
||||
const sunriseHour = 5;
|
||||
const highGrowthStart = Math.max(0, (sunsetHour - 2) / 24);
|
||||
const decayStart = sunriseHour / 24;
|
||||
const night = p >= highGrowthStart || p < decayStart;
|
||||
const eveningRamp = p >= Math.max(0, highGrowthStart - 2 / 24) && p < highGrowthStart;
|
||||
const afternoon = p >= 0.46 && p < Math.max(0.46, highGrowthStart - 2 / 24);
|
||||
return { p, night, eveningRamp, afternoon, highGrowthStart, decayStart };
|
||||
}
|
||||
|
||||
function updateCircadianSleepPressure(tarinai, world, dt = 0) {
|
||||
const phase = circadianSleepPhase(world);
|
||||
const sleeping = Boolean(tarinai.state === "sleep" || tarinai.sleeping);
|
||||
if (!Number.isFinite(tarinai.circadianSleepPressure)) {
|
||||
tarinai.circadianSleepPressure = phase.night ? rand(68, 88) : (phase.afternoon ? rand(8, 18) : rand(0, 5));
|
||||
tarinai.circadianSleepPressure = phase.night ? rand(46, 64) : (phase.afternoon || phase.eveningRamp ? rand(8, 18) : rand(0, 5));
|
||||
}
|
||||
const step = Math.max(0, Number(dt) || 0);
|
||||
if (phase.night) {
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 1.85, 0, 94);
|
||||
if (sleeping) {
|
||||
const target = phase.night ? 18 : 0;
|
||||
const current = Number(tarinai.circadianSleepPressure || 0) || 0;
|
||||
const extra = Math.max(0, current - target) * (phase.night ? 0.002 : 0.018);
|
||||
const bedLike = tarinai.target && typeof tarinai.isSleepFurniture === "function" && tarinai.isSleepFurniture(tarinai.target);
|
||||
const sleepDecay = (phase.night ? (bedLike ? 0.45 : 0.36) : (bedLike ? 2.15 : 1.45)) + extra;
|
||||
tarinai.circadianSleepPressure = clamp(current - step * sleepDecay, 0, 94);
|
||||
} else if (phase.night) {
|
||||
const duskBoost = phase.p >= phase.highGrowthStart && phase.p < 0.66 ? 1.0 : 0;
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * (2.35 + duskBoost), 0, 94);
|
||||
} else if (phase.eveningRamp) {
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 0.82, 0, 60);
|
||||
} else if (phase.afternoon) {
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 0.16, 0, 26);
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure + step * 0.10, 0, 22);
|
||||
} else {
|
||||
const decay = (tarinai.state === "sleep" || tarinai.sleeping) ? 1.85 : 0.42;
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure - step * decay, 0, 94);
|
||||
tarinai.circadianSleepPressure = clamp(tarinai.circadianSleepPressure - step * 0.74, 0, 94);
|
||||
}
|
||||
return { ...phase, pressure: tarinai.circadianSleepPressure };
|
||||
}
|
||||
|
|
@ -451,6 +469,9 @@ function startNeedAction(tarinai, world, choice, action, reasonText, options = {
|
|||
return Boolean(ran);
|
||||
}
|
||||
|
||||
// Needed by tarinai_needs_core.js and tarinai_forced_behavior.js, which run outside
|
||||
// this module's private IIFE but start need-driven actions through the same runtime path.
|
||||
global.startNeedAction = startNeedAction;
|
||||
|
||||
function continueCurrentBehavior(tarinai, world, needs, dt) {
|
||||
const behavior = currentTarinaiBehavior(tarinai);
|
||||
|
|
@ -566,9 +587,14 @@ function shouldWakeFromSleep(tarinai, world, needs) {
|
|||
const foodNeed = Number(needs?.food ?? tarinai.needRaw?.food ?? tarinai.needs?.food ?? 0) || 0;
|
||||
if ((tarinai.hunger || 0) >= 84 || foodNeed >= needThreshold("food", "start")) return true;
|
||||
const sleepNeed = Number(needs?.sleep ?? tarinai.needRaw?.sleep ?? tarinai.needs?.sleep ?? 0) || 0;
|
||||
if (sleepNeed <= ((typeof needThreshold === "function" ? needThreshold("sleep", "continue") : 34) + 4)) return true;
|
||||
if ((tarinai.energy || 0) >= (Number(session.targetEnergy) || 74)) return true;
|
||||
if (elapsed >= (Number(session.maxDuration) || 28)) return true;
|
||||
const sleepContinue = (typeof needThreshold === "function" ? needThreshold("sleep", "continue") : 34);
|
||||
const restedEnough = sleepNeed <= sleepContinue + 4;
|
||||
if (restedEnough) return true;
|
||||
// Energy can recover faster than the explicit sleep need, especially inside
|
||||
// nest containers. Do not wake and immediately re-enter while the individual
|
||||
// is still classified as sleepy.
|
||||
if ((tarinai.energy || 0) >= (Number(session.targetEnergy) || 74) && sleepNeed <= sleepContinue + 14) return true;
|
||||
if (elapsed >= (Number(session.maxDuration) || 28) && sleepNeed <= sleepContinue + 18) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -588,7 +614,7 @@ function protectSleepSession(tarinai, world, needs) {
|
|||
const comfort = bedLike ? Math.max(0.5, Math.min(1.4, Number(world?.bedComfort?.(tarinai.target) || tarinai.target?.comfort || 1) || 1)) : 0.55;
|
||||
const energyMax = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(tarinai) : (Number(tarinai.maxEnergy) || 100));
|
||||
tarinai.energy = Math.min(energyMax, (Number(tarinai.energy || 0) || 0) + step * (bedLike ? (1.2 + comfort * 1.35) : 0.65));
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { sleep: -step * (bedLike ? (5.2 + comfort * 4.5) : 3.2), safety: bedLike ? -step * 0.9 : 0 });
|
||||
if (typeof applyNeedRelief === "function") applyNeedRelief(tarinai, { sleep: -step * (bedLike ? (0.16 + comfort * 0.18) : 0.18), safety: bedLike ? -step * 0.9 : 0 });
|
||||
tarinai.thought = tarinai.thought || "\u7720\u3063\u3066\u3044\u308b";
|
||||
setBehavior(tarinai, { id: "sleep", need: "sleep", label: "\u7720\u3063\u3066\u3044\u308b", reason: "\u306d\u3080\u3044\u306e\u3067\u3001\u7720\u3063\u3066\u3044\u308b\u3002", text: "\u7720\u3063\u3066\u3044\u308b", state: "sleep", tiedNeeds: ["sleep"], startedAt: tarinai.sleepSession.startedAt, minDuration: tarinai.sleepSession.minDuration, lockSeconds: tarinai.sleepSession.minDuration, phase: "perform" });
|
||||
tarinai.behaviorLockTimer = Math.max(tarinai.behaviorLockTimer || 0, 0.8);
|
||||
|
|
@ -1013,11 +1039,13 @@ function resolveNeedsCore(dt) {
|
|||
this.tryRecoverBySunbath();
|
||||
this.sunbathCooldown = Math.max(this.sunbathCooldown || 0, rand(20, 38));
|
||||
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0)) this.bubble("\u307d\u304b\u3063", 3.4, "rgba(112,86,36,0.78)");
|
||||
this.sunbathLeisure = false;
|
||||
return true;
|
||||
},
|
||||
|
||||
startSunbath(options = {}) {
|
||||
if (!this.canStartSunbath(options)) return false;
|
||||
this.sunbathLeisure = Boolean(options.leisure);
|
||||
const pad = Math.max(28, CONFIG.worldPadding || 30);
|
||||
const safeW = Math.max(pad * 2 + 1, Number(this.world?.w || 1000) || 1000);
|
||||
const safeH = Math.max(pad * 2 + 1, Number(this.world?.h || 720) || 720);
|
||||
|
|
@ -1046,7 +1074,7 @@ function resolveNeedsCore(dt) {
|
|||
this.sunbathRecoveryChecked = false;
|
||||
this.vx = 0;
|
||||
this.vy = 0;
|
||||
setBehaviorText(this, { need: options.leisure ? "fulfill" : "health", subNeed: "sunbath", actionId: "sunbath", actionLabel: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b", reason: options.leisure ? "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002" : "\u8abf\u5b50\u304c\u60aa\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", target: null, phase: "perform", source: "behavior" });
|
||||
setBehaviorText(this, { need: options.leisure ? "fulfill" : "health", subNeed: "sunbath", actionId: "sunbath", actionLabel: "\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b", reason: options.leisure ? "\u4f59\u88d5\u304c\u3042\u308b\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002" : "\u8abf\u5b50\u304c\u60aa\u3044\u306e\u3067\u3001\u65e5\u5149\u6d74\u3057\u3066\u3044\u308b\u3002", target: null, phase: "perform", source: "behavior", causeText: options.leisure ? "\u4f59\u88d5\u304c\u3042\u308b" : "" });
|
||||
if ((this.world?.time || 0) >= (this.nextBubbleAt || 0) && Math.random() < 0.45) this.bubble("\u307d\u304b\u3063", 4.0, "rgba(112,86,36,0.78)");
|
||||
return true;
|
||||
},
|
||||
|
|
@ -1158,3 +1186,8 @@ function resolveNeedsCore(dt) {
|
|||
}
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
||||
global.TarinaiNeedsRuntime = Object.freeze({
|
||||
updateNeeds,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
const clampFn = typeof clamp === "function" ? clamp : ((v, a, b) => Math.max(a, Math.min(b, v)));
|
||||
|
||||
function isNestContainer(item) {
|
||||
return Boolean(item && (typeof global.isNestContainerType === "function" ? global.isNestContainerType(item.type) : (item.type === "nest_box" || item.type === "pipe")));
|
||||
return Boolean(global.TarinaiToolRuntime?.isNestContainerItem?.(item, { alive: false }));
|
||||
}
|
||||
|
||||
function isSleepFurniture(item) {
|
||||
|
|
@ -23,13 +23,27 @@
|
|||
return Boolean(worldRef.targetBlockedByObstacle?.(tarinai, item, Math.max(18, (tarinai.radius || 20) * 0.65)));
|
||||
}
|
||||
|
||||
function pipeItems(worldRef) {
|
||||
return (worldRef?.items || []).filter(it => it && !it.dead && it.type === "pipe");
|
||||
}
|
||||
|
||||
function tarinaiInsidePipeNetwork(worldRef, t) {
|
||||
if (!worldRef || !t || t.dead || !t.insideNestBoxId) return false;
|
||||
const holder = typeof worldRef.nestBoxById === "function" ? worldRef.nestBoxById(t.insideNestBoxId) : (worldRef.items || []).find(it => it && !it.dead && it.id === t.insideNestBoxId);
|
||||
return holder?.type === "pipe";
|
||||
}
|
||||
|
||||
function nestBoxCapacity(worldRef, box = null) {
|
||||
return typeof worldRef?.nestBoxCapacity === "function" ? worldRef.nestBoxCapacity(box) : (box?.type === "pipe" ? 3 : 5);
|
||||
if (typeof worldRef?.nestBoxCapacity === "function") return worldRef.nestBoxCapacity(box);
|
||||
if (box?.type === "pipe") return Math.max(3, pipeItems(worldRef).length * 3);
|
||||
return 5;
|
||||
}
|
||||
|
||||
function nestBoxOccupants(worldRef, box, limit = 5) {
|
||||
if (!worldRef || !box || box.dead || !isNestContainer(box)) return [];
|
||||
const occupants = (worldRef.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||||
const occupants = box.type === "pipe"
|
||||
? (worldRef.tarinai || []).filter(t => tarinaiInsidePipeNetwork(worldRef, t))
|
||||
: (worldRef.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id);
|
||||
return Number.isFinite(limit) ? occupants.slice(0, Math.max(0, limit)) : occupants;
|
||||
}
|
||||
|
||||
|
|
@ -41,11 +55,21 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function tarinaiTargetContainer(t) {
|
||||
const target = t?.target || null;
|
||||
return isNestContainer(target) ? target : (isNestContainer(target?.shelterItem) ? target.shelterItem : (isNestContainer(target?.hostItem) ? target.hostItem : null));
|
||||
}
|
||||
|
||||
function tarinaiIsReservingNest(t, box, now = 0) {
|
||||
if (!t || t.dead || !box || t.insideNestBoxId === box.id) return false;
|
||||
if (t.target !== box) return false;
|
||||
if (!t || t.dead || !box) return false;
|
||||
if (box.type === "pipe" && t.insideNestBoxId && tarinaiInsidePipeNetwork(t.world || null, t)) return false;
|
||||
if (box.type !== "pipe" && t.insideNestBoxId === box.id) return false;
|
||||
const target = tarinaiTargetContainer(t);
|
||||
if (box.type === "pipe") {
|
||||
if (target?.type !== "pipe") return false;
|
||||
} else if (target !== box) return false;
|
||||
if (t._nestBoxAvoidId === box.id && now < (t._nestBoxAvoidUntil || -Infinity)) return false;
|
||||
const seekingNest = t.state === "seek_bed" || t.state === "sleep" || t.sleeping || t.behavior?.actionId === "sleep_in_bed";
|
||||
const seekingNest = t.state === "seek_bed" || t.state === "sleep" || t.sleeping || t.state === "seek_temperature" || t.behavior?.actionId === "sleep_in_bed";
|
||||
if (!seekingNest) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -83,6 +107,7 @@
|
|||
function sleepPlaceAvailableFor(worldRef, tarinai, item) {
|
||||
if (!worldRef || !tarinai || !item || item.dead || !isSleepFurniture(item)) return false;
|
||||
if (Number(item.unreachableUntil || 0) > Number(worldRef.time || 0)) return false;
|
||||
if (item.type === "nest_box" && globalThis.TarinaiItemDynamicToolSystem?.nestBoxFireActive?.(item)) return false;
|
||||
if (item.abandonedById && item.abandonedById === tarinai?.id && Number(item.abandonedUntil || 0) > Number(worldRef.time || 0)) return false;
|
||||
if (tarinai.shouldAvoidTarget?.(item)) return false;
|
||||
if (worldRef?.sleepTargetTemperatureBlocked?.(tarinai, item)) return false;
|
||||
|
|
@ -155,14 +180,12 @@
|
|||
}
|
||||
|
||||
global.TarinaiNestSleepSystem = Object.freeze({
|
||||
isNestContainer,
|
||||
isSleepFurniture,
|
||||
isTarinaiHiddenInNestBox,
|
||||
nestBoxOccupants,
|
||||
bedOccupancy,
|
||||
bedComfort,
|
||||
sleepPlaceAvailableFor,
|
||||
findStructure,
|
||||
findOwnedStructure,
|
||||
findUnownedStructure,
|
||||
nearestSleepPlaceOfType,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
"use strict";
|
||||
|
||||
function tarinaiSpriteReversesFacing(sprite) {
|
||||
const id = String(sprite || "");
|
||||
return id === "back" || id === "tarinai12_back" || /(^|_)12_back$/i.test(id);
|
||||
}
|
||||
|
||||
(function (global) {
|
||||
const Tarinai = global.Tarinai;
|
||||
if (!Tarinai) throw new Error("Tarinai is not available for mixin: tarinai_render.js");
|
||||
|
|
@ -279,9 +284,10 @@
|
|||
const squish = this.state === "sleep"
|
||||
? 1 + breathing * 0.018
|
||||
: 1 + Math.sin(t * (4.5 + gait * 1.6) + this.age) * (0.012 + gait * 0.018) + headbuttPulse * 0.03;
|
||||
const faceRight = this.state === "sleep"
|
||||
let faceRight = this.state === "sleep"
|
||||
? this.sleepFacing > 0
|
||||
: this.facingDir() > 0;
|
||||
if (tarinaiSpriteReversesFacing(renderSprite || sprite)) faceRight = !faceRight;
|
||||
const angle = (faceRight ? 1 : -1) * (Math.PI / 12) + motionTilt;
|
||||
const nestHideAlpha = this.nestFade ? clamp(1 - this.nestFade, 0, 1) : 1;
|
||||
if (nestHideAlpha <= 0.01) return;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
const TARINAI_KING_FLEE_REASON = "\u305F\u308A\u306A\u3044\u738B\u304C\u6016\u304F\u3066\u9003\u3052\u3066\u3044\u308B\u3002";
|
||||
|
||||
function isNestContainerItem(item) {
|
||||
return Boolean(item && !item.dead && (typeof global.isNestContainerType === "function" ? global.isNestContainerType(item.type) : (item.type === "nest_box" || item.type === "pipe")));
|
||||
return Boolean(global.TarinaiToolRuntime?.isNestContainerItem?.(item));
|
||||
}
|
||||
|
||||
function sleepContainerText(item, verb = "\u4f11\u3093\u3067\u3044\u308b") {
|
||||
|
|
@ -167,11 +167,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
const zunchiSpreadChanceA = o.diseaseChance ? o.diseaseChance(dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) : dt * 0.065 * clamp(1 - d / 58, 0.12, 1);
|
||||
const zunchiTempMulA = typeof global.zunchiDiseaseTemperatureInfectionMultiplier === "function" ? global.zunchiDiseaseTemperatureInfectionMultiplier(o) : 1;
|
||||
const zunchiSpreadBaseA = dt * 0.065 * clamp(1 - d / 58, 0.12, 1) * zunchiTempMulA;
|
||||
const zunchiSpreadChanceA = o.diseaseChance ? o.diseaseChance(zunchiSpreadBaseA) : zunchiSpreadBaseA;
|
||||
if (this.zunchiDisease && !o.zunchiDisease && (o.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && deterministicChance(this.world, "social-zunchi-spread-a", zunchiSpreadChanceA, this, o)) {
|
||||
if (o.infectZunchiDisease(this)) this.world.log(`${this.name}\u306e\u305a\u3093\u3061\u75c5\u304c${o.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
|
||||
}
|
||||
const zunchiSpreadChanceB = this.diseaseChance ? this.diseaseChance(dt * 0.065 * clamp(1 - d / 58, 0.12, 1)) : dt * 0.065 * clamp(1 - d / 58, 0.12, 1);
|
||||
const zunchiTempMulB = typeof global.zunchiDiseaseTemperatureInfectionMultiplier === "function" ? global.zunchiDiseaseTemperatureInfectionMultiplier(this) : 1;
|
||||
const zunchiSpreadBaseB = dt * 0.065 * clamp(1 - d / 58, 0.12, 1) * zunchiTempMulB;
|
||||
const zunchiSpreadChanceB = this.diseaseChance ? this.diseaseChance(zunchiSpreadBaseB) : zunchiSpreadBaseB;
|
||||
if (o.zunchiDisease && !this.zunchiDisease && (this.zunchiDiseaseCooldown || 0) <= 0 && d < 54 && deterministicChance(this.world, "social-zunchi-spread-b", zunchiSpreadChanceB, this, o)) {
|
||||
if (this.infectZunchiDisease(o)) this.world.log(`${o.name}\u306e\u305a\u3093\u3061\u75c5\u304c${this.name}\u306b\u3046\u3064\u3063\u305f\u3002`, "accident", { participants: [o, this] });
|
||||
}
|
||||
|
|
@ -238,8 +242,6 @@
|
|||
if (typeof applyNeedShock === "function") applyNeedShock(o, { safety: 12, health: 6 });
|
||||
const damageToThis = fightHeadbuttDamage(o, this, this.world, "to-this");
|
||||
const damageToOther = fightHeadbuttDamage(this, o, this.world, "to-other");
|
||||
this.world.emit?.("fight:hit", { attacker: o, target: this, amount: damageToThis, cause: "\u55a7\u5629" });
|
||||
this.world.emit?.("fight:hit", { attacker: this, target: o, amount: damageToOther, cause: "\u55a7\u5629" });
|
||||
this.damage(damageToThis, "\u55a7\u5629");
|
||||
o.damage(damageToOther, "\u55a7\u5629");
|
||||
this.world.maybeDefeatFromFightDamage?.(this, o, damageToThis);
|
||||
|
|
@ -469,7 +471,7 @@
|
|||
if (this.target && !this.target.dead) {
|
||||
const baseMoveTarget = this.state === "seek_bed" && this.isSleepFurniture(this.target) ? this.sleepSpotFor(this.target) : this.target;
|
||||
const routeStates = new Set(["seek_food", "seek_water", "seek_bed", "seek_material", "build", "follow_parent", "seek_friend", "seek_enemy", "fight", "play_ball", "ant_attack", "seek_temperature"]);
|
||||
const routeTargetItem = this.target?.hostItem || this.target;
|
||||
const routeTargetItem = this.target?.shelterItem || this.target?.hostItem || this.target;
|
||||
const pathPadding = Math.max(18, (this.radius || 20) * 0.82);
|
||||
const targetFoodType = this.target?.type === "duplicator" ? this.target?.storedFoodType : this.target?.type;
|
||||
const zundaFoodTarget = this.state === "seek_food" && (targetFoodType === "sweet" || targetFoodType === "zunda_juice");
|
||||
|
|
@ -700,7 +702,8 @@
|
|||
if (!Array.isArray(this.world.recentDeathTimes)) this.world.recentDeathTimes = [];
|
||||
this.world.recentDeathTimes.push(this.world.lastDeathAt);
|
||||
if (this.world.recentDeathTimes.length > 24) this.world.recentDeathTimes = this.world.recentDeathTimes.slice(-24);
|
||||
for (const item of this.world.items || []) {
|
||||
const plushieSource = this.world.itemsOfType?.("plushie") || this.world.items || [];
|
||||
for (const item of plushieSource) {
|
||||
if (!item || item.dead || !item.isStructure || item.type !== "plushie" || item.ownerId !== this.id) continue;
|
||||
item.hp = 0;
|
||||
item.amount = 0;
|
||||
|
|
@ -724,7 +727,6 @@
|
|||
this.releasedLiveToken = this.liveToken || 0;
|
||||
this.world.releaseTarinaiLiveId?.(this);
|
||||
audio.death();
|
||||
this.world.emit?.("tarinai:died", { tarinai: this, reason: finalReason });
|
||||
this.world.log(`${this.name}\u306f\u9759\u304b\u306a\u75d5\u8de1\u3092\u6b8b\u3057\u305f\u3002\u6b7b\u56e0: ${finalReason}`, "death", { participants: [this] });
|
||||
if (this.world.selected === this) this.world.selected = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
// every frame via the smooth-motion pass.
|
||||
(function (global) {
|
||||
const helpers = global.TarinaiSimulationRuntime.helpers;
|
||||
const Geometry = global.TarinaiGeometry;
|
||||
|
||||
function behaviorOf(tarinai) {
|
||||
return currentTarinaiBehavior(tarinai);
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
}
|
||||
|
||||
function visible(worldRef, tarinai, visibleRect) {
|
||||
return Boolean(helpers.pointInRect(tarinai.x || 0, tarinai.y || 0, visibleRect));
|
||||
return Boolean(Geometry.pointInRect(tarinai.x || 0, tarinai.y || 0, visibleRect));
|
||||
}
|
||||
|
||||
function hasMotionNeed(tarinai) {
|
||||
|
|
@ -59,7 +60,7 @@
|
|||
const lockedPassive = sleeping || state === "eat" || /eat|sunbath|birth_ritual|breed|repro/i.test(id) || /sunbath|birth_ritual|breed|repro/.test(state) || (tarinai.birthRitualTimer || 0) > 0.04 || (tarinai.eatTimer || 0) > 0.04 || (tarinai.sunbathTimer || 0) > 0.04;
|
||||
if (sleeping && isVisible && !worldRef?.isTarinaiHiddenInNestBox?.(tarinai)) return true;
|
||||
if (lockedPassive && isVisible && activeMechanicalObstacleNear(tarinai, worldRef)) return true;
|
||||
if (sleeping && tarinai.target && !tarinai.target.dead && tarinai.isSleepFurniture?.(tarinai.target) && !(typeof isNestContainerType === "function" ? isNestContainerType(tarinai.target.type) : (tarinai.target.type === "nest_box" || tarinai.target.type === "pipe"))) return true;
|
||||
if (sleeping && tarinai.target && !tarinai.target.dead && tarinai.isSleepFurniture?.(tarinai.target) && !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(tarinai.target, { alive: false })) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +75,7 @@
|
|||
const mobile = Boolean(global.TarinaiInputMode.snapshot().touchFirst || (global.innerWidth || 9999) <= 760);
|
||||
const isVisible = visible(worldRef, tarinai, visibleRect);
|
||||
const farRect = isVisible ? visibleRect : (nearVisibleRect || helpers.updateVisibleWorldRect(worldRef, 520));
|
||||
const isNear = isVisible || Boolean(helpers.pointInRect(tarinai.x || 0, tarinai.y || 0, farRect));
|
||||
const isNear = isVisible || Boolean(Geometry.pointInRect(tarinai.x || 0, tarinai.y || 0, farRect));
|
||||
const motionNeed = hasMotionNeed(tarinai);
|
||||
|
||||
const nestEntryActive = Boolean(tarinai.insideNestBoxId && (typeof tarinai.nestBoxEntryActive === "function" ? tarinai.nestBoxEntryActive() : ((worldRef.time || 0) < (tarinai._nestBoxEntrySmoothUntil || -Infinity) || ((tarinai.nestFade || 0) > 0.01 && (tarinai.nestFade || 0) < 0.985))));
|
||||
|
|
|
|||
|
|
@ -174,8 +174,16 @@
|
|||
if ((tempStatus.harmExcess || 0) > 0) {
|
||||
const harm = tempStatus.harmExcess || 0;
|
||||
t.energy -= dt * (0.10 + harm * 0.075 + harm * harm * 0.006);
|
||||
t.lastDamageCause = tempStatus.direction === "cold" ? "\u51CD\u7D50" : "\u71B1\u4E2D\u75C7";
|
||||
t.lastTemperatureDamageAt = t.world?.time || 0;
|
||||
const tempDamageCause = tempStatus.direction === "cold" ? "\u51CD\u7D50" : "\u71B1\u4E2D\u75C7";
|
||||
const tempDamageAt = t.world?.time || 0;
|
||||
t.lastDamageCause = tempDamageCause;
|
||||
t.lastDamageAmount = Math.max(Number(t.lastDamageAmount || 0) || 0, harm);
|
||||
t.lastDamageAt = tempDamageAt;
|
||||
if (harm >= 1.5 || t.energy <= 30 || t.stress >= 104) {
|
||||
t.lastMajorDamageCause = tempDamageCause;
|
||||
t.lastMajorDamageAt = tempDamageAt;
|
||||
}
|
||||
t.lastTemperatureDamageAt = tempDamageAt;
|
||||
}
|
||||
|
||||
const phase = t.world.lightLevel();
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@
|
|||
t._sleepExternalWakeBubbleAt = now;
|
||||
t.bubble("!", 0.55, "rgba(88,82,118,0.78)");
|
||||
}
|
||||
world?.emit?.("tarinai:woke", { tarinai: t, reason: "external-motion" });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +119,7 @@
|
|||
const state = String(t.state || "");
|
||||
|
||||
const sleepExternal = markSleepExternalMotion(t, dt, "passive-physical");
|
||||
if ((state === "sleep" || t.sleeping) && t.target && !t.target.dead && t.isSleepFurniture?.(t.target) && !(typeof isNestContainerType === "function" ? isNestContainerType(t.target.type) : (t.target.type === "nest_box" || t.target.type === "pipe"))) {
|
||||
if ((state === "sleep" || t.sleeping) && t.target && !t.target.dead && t.isSleepFurniture?.(t.target) && !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(t.target, { alive: false })) {
|
||||
const comfort = world?.bedComfort ? world.bedComfort(t.target) : 0.72;
|
||||
const spot = t.sleepSpotFor ? t.sleepSpotFor(t.target) : t.target;
|
||||
const dx = numeric(spot?.x, beforeX) - beforeX;
|
||||
|
|
|
|||
3
js/ui.js
3
js/ui.js
|
|
@ -36,6 +36,9 @@ const ui = {
|
|||
soundCategoryControls: document.getElementById("soundPanel"),
|
||||
toast: document.getElementById("toast"),
|
||||
statPop: document.getElementById("statPop"),
|
||||
statSick: document.getElementById("statSick"),
|
||||
statJuvenile: document.getElementById("statJuvenile"),
|
||||
statElder: document.getElementById("statElder"),
|
||||
statDead: document.getElementById("statDead"),
|
||||
statColonyMood: document.getElementById("statColonyMood"),
|
||||
temperatureValue: document.getElementById("temperatureValue"),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ function bindUI() {
|
|||
syncLogPushControls();
|
||||
syncAudioControls();
|
||||
window.TarinaiTooltips.applyToolPalette();
|
||||
window.bindGroundUI?.(world);
|
||||
window.TarinaiSaveSystem.bindSaveSystem();
|
||||
uiCache.toolButtons = Array.from(ui.toolPalette?.querySelectorAll(".tool") || []);
|
||||
function isActiveToolMode() {
|
||||
|
|
@ -379,6 +378,7 @@ function bindUI() {
|
|||
if (!uiCache.archiveAutoRefreshTimer && typeof window !== "undefined") {
|
||||
uiCache.archiveAutoRefreshTimer = window.setInterval(() => {
|
||||
if (!ui.archiveContent || !archiveAutoUpdateEnabled()) return;
|
||||
if (typeof lineageArchiveNeedsRender === "function" && !lineageArchiveNeedsRender()) return;
|
||||
scheduleArchiveWindowRender();
|
||||
}, 5000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,20 @@ function colonyChartObjectKeys() {
|
|||
|
||||
function renderStats() {
|
||||
const alive = world.tarinai;
|
||||
const isDiseased = (t) => Boolean(t?.zunchiDisease || t?.sleepDisease || t?.explosionDisease || t?.fightDisease);
|
||||
const lifeRatio = (t) => Math.max(0, Math.min(1, (Number(t?.age || 0) || 0) / Math.max(1, Number(t?.lifeSpan || 1) || 1)));
|
||||
const sickCount = alive.reduce((n, t) => n + (isDiseased(t) ? 1 : 0), 0);
|
||||
const juvenileCount = alive.reduce((n, t) => n + (lifeRatio(t) <= 0.10 ? 1 : 0), 0);
|
||||
const elderCount = alive.reduce((n, t) => n + (lifeRatio(t) >= 0.80 ? 1 : 0), 0);
|
||||
const avg = (fn) => alive.length ? alive.reduce((a, t) => a + fn(t), 0) / alive.length : 0;
|
||||
const needKeys = typeof TARINAI_NEED_KEYS !== "undefined" ? TARINAI_NEED_KEYS : ["food", "sleep", "health", "safety", "social", "fulfill"];
|
||||
const needAverages = Object.fromEntries(needKeys.map(key => [`need_${key}`, avg(t => Number(t.needs?.[key] || 0) || 0)]));
|
||||
const temp = world.updateTemperature?.(0) ?? world.currentTemperature ?? (CONFIG.standardTemperature ?? 15);
|
||||
const values = {
|
||||
pop: alive.length,
|
||||
sick: sickCount,
|
||||
juvenile: juvenileCount,
|
||||
elder: elderCount,
|
||||
dead: world.deadCount,
|
||||
stress: avg(t => t.stress),
|
||||
temperature: Number(temp) || 0,
|
||||
|
|
@ -33,6 +41,9 @@ function renderStats() {
|
|||
objects: { ...(world.itemCounts || {}) },
|
||||
};
|
||||
setTextIfChanged(ui.statPop, "pop", values.pop);
|
||||
setTextIfChanged(ui.statSick, "sick", values.sick);
|
||||
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 || "安定");
|
||||
|
|
@ -73,6 +84,9 @@ function updateColonyHistory(values) {
|
|||
count: 0,
|
||||
sums: {
|
||||
pop: 0,
|
||||
sick: 0,
|
||||
juvenile: 0,
|
||||
elder: 0,
|
||||
stress: 0,
|
||||
temperature: 0,
|
||||
...Object.fromEntries(needKeys.map(k => [`need_${k}`, 0])),
|
||||
|
|
@ -90,6 +104,9 @@ function updateColonyHistory(values) {
|
|||
const row = {
|
||||
t: bucket.end,
|
||||
pop: bucket.sums.pop / bucket.count,
|
||||
sick: bucket.sums.sick / bucket.count,
|
||||
juvenile: bucket.sums.juvenile / bucket.count,
|
||||
elder: bucket.sums.elder / bucket.count,
|
||||
stress: bucket.sums.stress / bucket.count,
|
||||
temperature: bucket.sums.temperature / bucket.count,
|
||||
...Object.fromEntries(needKeys.map(k => [`need_${k}`, (bucket.sums[`need_${k}`] || 0) / bucket.count])),
|
||||
|
|
@ -108,6 +125,9 @@ function updateColonyHistory(values) {
|
|||
const bucket = uiCache.chartBucketAccum;
|
||||
bucket.count += 1;
|
||||
bucket.sums.pop += values.pop || 0;
|
||||
bucket.sums.sick += values.sick || 0;
|
||||
bucket.sums.juvenile += values.juvenile || 0;
|
||||
bucket.sums.elder += values.elder || 0;
|
||||
bucket.sums.stress += values.stress || 0;
|
||||
bucket.sums.temperature += Number(values.temperature || 0) || 0;
|
||||
for (const k of needKeys) bucket.sums[`need_${k}`] += values[`need_${k}`] || 0;
|
||||
|
|
@ -124,6 +144,9 @@ function chartRows(values) {
|
|||
return [{
|
||||
t: Math.max(0, Number(uiCache.chartBucketStart || 0) || 0),
|
||||
pop: 0,
|
||||
sick: 0,
|
||||
juvenile: 0,
|
||||
elder: 0,
|
||||
stress: 0,
|
||||
temperature: 0,
|
||||
...Object.fromEntries(needKeys.map(k => [`need_${k}`, 0])),
|
||||
|
|
@ -251,7 +274,12 @@ function chartSeries(mode = "population") {
|
|||
{ key: "obj_water", label: "雨滴", color: "#5aa8d6" },
|
||||
{ key: "temperature", label: "気温", color: "#c46f3c" },
|
||||
];
|
||||
return [{ key: "pop", label: "個体", color: "#5d8ee6" }];
|
||||
return [
|
||||
{ key: "pop", label: "総数", color: "#5d8ee6" },
|
||||
{ key: "sick", label: "病気", color: "#d96262" },
|
||||
{ key: "juvenile", label: "幼体", color: "#78a642" },
|
||||
{ key: "elder", label: "老体", color: "#8a7054" },
|
||||
];
|
||||
}
|
||||
|
||||
function formatChartValue(key, values) {
|
||||
|
|
|
|||
|
|
@ -158,27 +158,6 @@ function familyDeathReasonLabel(rawReason, entity = null) {
|
|||
if (!text) return "";
|
||||
// The event log already records the normalized final death reason. Use the
|
||||
// same stored value here so the family tree and event UI cannot diverge.
|
||||
void entity;
|
||||
return text;
|
||||
const collisionText = text.replace(/\u3067\u8870\u5f31.*$/, "");
|
||||
const collisionWith = collisionText.match(/([^\u3001\u3002\s]+?)(?:\u306b|\u3068\u306e)\u885d\u7a81/);
|
||||
if (collisionWith) {
|
||||
const label = `${collisionWith[1]}\u3068\u306e\u885d\u7a81${text.includes("\u3067\u8870\u5f31") ? "\u3067\u8870\u5f31" : ""}`;
|
||||
return label.length > 14 ? `${label.slice(0, 14)}\u2026` : label;
|
||||
}
|
||||
if (text.includes("\u3067\u8870\u5f31")) return text.length > 12 ? `${text.slice(0, 12)}\u2026` : text;
|
||||
if (/\u55a7\u5629|fight/.test(text)) return "\u55a7\u5629";
|
||||
if (/\u7206\u7af9|firecracker|explosion/.test(text)) return "\u7206\u7af9";
|
||||
if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664";
|
||||
if (/\u885d\u7a81|collision/.test(text)) return "\u885d\u7a81";
|
||||
if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return "\u3064\u3064\u304b\u308c\u3059\u304e";
|
||||
if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return "\u305a\u3093\u3061\u75c5";
|
||||
if (/\u75c5/.test(text)) {
|
||||
const disease = text.match(/([^\u3001\u3002\s]+\u75c5)/);
|
||||
return disease ? disease[1] : "\u305a\u3093\u3061\u75c5";
|
||||
}
|
||||
if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return "\u98e2\u9913";
|
||||
if (/\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return "\u30b9\u30c8\u30ec\u30b9\u904e\u591a";
|
||||
if (/\u5bff\u547d|\u5929\u5bff/.test(text)) return "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f";
|
||||
if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u77f3|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|stone|accident/.test(text)) return "\u4e8b\u6545";
|
||||
return "\u4e8b\u6545";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,14 +21,6 @@ function lineageSignedPercentText(value = 0) {
|
|||
return `${n >= 0 ? "+" : ""}${n}%`;
|
||||
}
|
||||
|
||||
function lineageFightRateText(n) {
|
||||
const entity = n?.id ? world.tarinai.find(t => (t.familyKey || t.id) === n.id) : null;
|
||||
const wins = Number(entity?.totalFightWins ?? n?.totalFightWins ?? 0) || 0;
|
||||
const losses = Number(entity?.totalFightLosses ?? n?.totalFightLosses ?? 0) || 0;
|
||||
const total = wins + losses;
|
||||
if (!total) return "--";
|
||||
return `${Math.round((wins / total) * 100)}%/${total}`;
|
||||
}
|
||||
|
||||
function lineageTreeNodeHtml(n, x, y) {
|
||||
const entity = n?.id ? world.tarinai.find(t => (t.familyKey || t.id) === n.id) : null;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,227 @@ function lineageSegmentsFromPoints(points) {
|
|||
return segments;
|
||||
}
|
||||
|
||||
function lineageRoutePointKey(x, y) {
|
||||
return `${Math.round(Number(x || 0) * 10) / 10},${Math.round(Number(y || 0) * 10) / 10}`;
|
||||
}
|
||||
|
||||
function lineageRouteSamePoint(a, b) {
|
||||
return Math.abs(Number(a?.[0]) - Number(b?.[0])) < 0.05 && Math.abs(Number(a?.[1]) - Number(b?.[1])) < 0.05;
|
||||
}
|
||||
|
||||
function lineageCleanRoutePoints(points) {
|
||||
const raw = (points || [])
|
||||
.map(p => [Number(p?.[0]), Number(p?.[1])])
|
||||
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
||||
const deduped = [];
|
||||
for (const p of raw) {
|
||||
if (!deduped.length || !lineageRouteSamePoint(deduped[deduped.length - 1], p)) deduped.push(p);
|
||||
}
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (let i = 1; i < deduped.length - 1; i++) {
|
||||
const a = deduped[i - 1];
|
||||
const b = deduped[i];
|
||||
const c = deduped[i + 1];
|
||||
const sameX = Math.abs(a[0] - b[0]) < 0.05 && Math.abs(b[0] - c[0]) < 0.05;
|
||||
const sameY = Math.abs(a[1] - b[1]) < 0.05 && Math.abs(b[1] - c[1]) < 0.05;
|
||||
const reverse = lineageRouteSamePoint(a, c);
|
||||
if (sameX || sameY || reverse) {
|
||||
deduped.splice(i, 1);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return deduped;
|
||||
}
|
||||
|
||||
function lineagePathFromPoints(points) {
|
||||
if (!points.length) return "";
|
||||
return `M ${points[0][0].toFixed(1)} ${points[0][1].toFixed(1)} ` + points.slice(1).map(p => `L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' ');
|
||||
const clean = lineageForceOrthogonalPoints(points);
|
||||
if (!clean.length) return "";
|
||||
return `M ${clean[0][0].toFixed(1)} ${clean[0][1].toFixed(1)}` + clean.slice(1).map(p => ` L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join('');
|
||||
}
|
||||
|
||||
function lineagePointInsideRouteRect(x, y, rects = []) {
|
||||
return (rects || []).some(r => x > r.left && x < r.right && y > r.top && y < r.bottom);
|
||||
}
|
||||
|
||||
function lineageRouteSegmentClear(x1, y1, x2, y2, rects = []) {
|
||||
if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) return false;
|
||||
if (Math.abs(x1 - x2) < 0.05 && Math.abs(y1 - y2) < 0.05) return true;
|
||||
const vertical = Math.abs(x1 - x2) < 0.05;
|
||||
const horizontal = Math.abs(y1 - y2) < 0.05;
|
||||
if (!vertical && !horizontal) return false;
|
||||
for (const r of rects || []) {
|
||||
if (vertical) {
|
||||
const top = Math.min(y1, y2);
|
||||
const bottom = Math.max(y1, y2);
|
||||
if (x1 > r.left && x1 < r.right && Math.max(top, r.top) < Math.min(bottom, r.bottom)) return false;
|
||||
} else {
|
||||
const left = Math.min(x1, x2);
|
||||
const right = Math.max(x1, x2);
|
||||
if (y1 > r.top && y1 < r.bottom && Math.max(left, r.left) < Math.min(right, r.right)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function lineageOrthogonalFallbackPoints(x1, y1, x2, y2, rects = []) {
|
||||
const candidates = [
|
||||
[[x1, y1], [x2, y1], [x2, y2]],
|
||||
[[x1, y1], [x1, y2], [x2, y2]],
|
||||
];
|
||||
const score = points => {
|
||||
let bad = 0;
|
||||
let length = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const a = points[i - 1];
|
||||
const b = points[i];
|
||||
length += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
||||
if (!lineageRouteSegmentClear(a[0], a[1], b[0], b[1], rects)) bad += 100000;
|
||||
}
|
||||
return bad + length;
|
||||
};
|
||||
candidates.sort((a, b) => score(a) - score(b));
|
||||
return lineageCleanRoutePoints(candidates[0]);
|
||||
}
|
||||
|
||||
function lineageForceOrthogonalPoints(points) {
|
||||
const clean = lineageCleanRoutePoints(points);
|
||||
if (clean.length <= 1) return clean;
|
||||
const out = [clean[0]];
|
||||
for (let i = 1; i < clean.length; i++) {
|
||||
const prev = out[out.length - 1];
|
||||
const cur = clean[i];
|
||||
const diagonal = Math.abs(prev[0] - cur[0]) >= 0.05 && Math.abs(prev[1] - cur[1]) >= 0.05;
|
||||
if (diagonal) {
|
||||
const via = [cur[0], prev[1]];
|
||||
if (!lineageRouteSamePoint(prev, via)) out.push(via);
|
||||
}
|
||||
if (!lineageRouteSamePoint(out[out.length - 1], cur)) out.push(cur);
|
||||
}
|
||||
return lineageCleanRoutePoints(out);
|
||||
}
|
||||
|
||||
|
||||
function lineageRelevantRouteRects(x1, y1, x2, y2, rects = [], margin = 92) {
|
||||
const left = Math.min(x1, x2) - margin;
|
||||
const right = Math.max(x1, x2) + margin;
|
||||
const top = Math.min(y1, y2) - margin;
|
||||
const bottom = Math.max(y1, y2) + margin;
|
||||
return (rects || []).filter(r => r.right >= left && r.left <= right && r.bottom >= top && r.top <= bottom);
|
||||
}
|
||||
|
||||
function lineageOrthogonalRoutePoints(x1, y1, x2, y2, rects = [], context = {}) {
|
||||
if (!Number.isFinite(x1) || !Number.isFinite(y1) || !Number.isFinite(x2) || !Number.isFinite(y2)) return [];
|
||||
if (Math.abs(x1 - x2) < 0.05 && Math.abs(y1 - y2) < 0.05) return [[x1, y1]];
|
||||
const relevant = lineageRelevantRouteRects(x1, y1, x2, y2, rects, Number(context.routeMargin || 110) || 110);
|
||||
if (lineageRouteSegmentClear(x1, y1, x2, y2, relevant)) return [[x1, y1], [x2, y2]];
|
||||
|
||||
const width = Number(context.width || 0) || 0;
|
||||
const height = Number(context.height || 0) || 0;
|
||||
const minX = 8;
|
||||
const minY = Math.max(8, Number(context.minRouteY ?? 8) || 8);
|
||||
const maxX = width > 24 ? width - 8 : Math.max(x1, x2, ...relevant.map(r => r.right + 30), 240);
|
||||
const maxY = height > 24 ? height - 8 : Math.max(y1, y2, ...relevant.map(r => r.bottom + 30), 240);
|
||||
const xs = [x1, x2, minX, maxX];
|
||||
const ys = [y1, y2, minY, maxY];
|
||||
if (Number.isFinite(Number(context.preferredX))) xs.push(Number(context.preferredX));
|
||||
if (Number.isFinite(Number(context.preferredY))) ys.push(Number(context.preferredY));
|
||||
for (const r of relevant) {
|
||||
xs.push(r.left - 8, r.left, r.right, r.right + 8);
|
||||
ys.push(r.top - 8, r.top, r.bottom, r.bottom + 8);
|
||||
}
|
||||
const norm = values => Array.from(new Set(values
|
||||
.map(v => Math.round(Number(v) * 10) / 10)
|
||||
.filter(Number.isFinite)))
|
||||
.sort((a, b) => a - b);
|
||||
let ux = norm(xs).filter(v => v >= minX && v <= maxX);
|
||||
let uy = norm(ys).filter(v => v >= minY && v <= maxY);
|
||||
if (!ux.some(v => Math.abs(v - x1) < 0.05)) ux.push(x1);
|
||||
if (!ux.some(v => Math.abs(v - x2) < 0.05)) ux.push(x2);
|
||||
if (!uy.some(v => Math.abs(v - y1) < 0.05)) uy.push(y1);
|
||||
if (!uy.some(v => Math.abs(v - y2) < 0.05)) uy.push(y2);
|
||||
ux = Array.from(new Set(ux.map(v => Math.round(v * 10) / 10))).sort((a, b) => a - b);
|
||||
uy = Array.from(new Set(uy.map(v => Math.round(v * 10) / 10))).sort((a, b) => a - b);
|
||||
|
||||
const passable = new Set();
|
||||
const keyFor = (i, j) => `${i}:${j}`;
|
||||
for (let i = 0; i < ux.length; i++) {
|
||||
for (let j = 0; j < uy.length; j++) {
|
||||
if (!lineagePointInsideRouteRect(ux[i], uy[j], relevant)) passable.add(keyFor(i, j));
|
||||
}
|
||||
}
|
||||
const ix1 = ux.findIndex(v => Math.abs(v - x1) < 0.05);
|
||||
const iy1 = uy.findIndex(v => Math.abs(v - y1) < 0.05);
|
||||
const ix2 = ux.findIndex(v => Math.abs(v - x2) < 0.05);
|
||||
const iy2 = uy.findIndex(v => Math.abs(v - y2) < 0.05);
|
||||
const start = keyFor(ix1, iy1);
|
||||
const goal = keyFor(ix2, iy2);
|
||||
passable.add(start);
|
||||
passable.add(goal);
|
||||
|
||||
const dist = new Map([[start, 0]]);
|
||||
const prev = new Map();
|
||||
const open = new Set([start]);
|
||||
const score = key => {
|
||||
const [i, j] = key.split(':').map(Number);
|
||||
return (dist.get(key) || 0) + Math.abs(ux[i] - x2) + Math.abs(uy[j] - y2) * 1.08;
|
||||
};
|
||||
let guard = 0;
|
||||
while (open.size && guard++ < 20000) {
|
||||
let cur = null;
|
||||
for (const k of open) if (cur === null || score(k) < score(cur)) cur = k;
|
||||
if (cur === goal) break;
|
||||
open.delete(cur);
|
||||
const [i, j] = cur.split(':').map(Number);
|
||||
const neighbors = [[i - 1, j], [i + 1, j], [i, j - 1], [i, j + 1]];
|
||||
for (const [ni, nj] of neighbors) {
|
||||
if (ni < 0 || nj < 0 || ni >= ux.length || nj >= uy.length) continue;
|
||||
const nk = keyFor(ni, nj);
|
||||
if (!passable.has(nk)) continue;
|
||||
if (!lineageRouteSegmentClear(ux[i], uy[j], ux[ni], uy[nj], relevant)) continue;
|
||||
const turnPenalty = (() => {
|
||||
const pk = prev.get(cur);
|
||||
if (!pk) return 0;
|
||||
const [pi, pj] = pk.split(':').map(Number);
|
||||
const wasH = pi !== i;
|
||||
const nowH = ni !== i;
|
||||
return wasH === nowH ? 0 : 5;
|
||||
})();
|
||||
const step = Math.abs(ux[i] - ux[ni]) + Math.abs(uy[j] - uy[nj]) + turnPenalty;
|
||||
const nd = (dist.get(cur) || 0) + step;
|
||||
if (nd + 0.001 < (dist.get(nk) ?? Infinity)) {
|
||||
dist.set(nk, nd);
|
||||
prev.set(nk, cur);
|
||||
open.add(nk);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dist.has(goal)) {
|
||||
const keys = [];
|
||||
let cur = goal;
|
||||
while (cur) {
|
||||
keys.push(cur);
|
||||
if (cur === start) break;
|
||||
cur = prev.get(cur);
|
||||
}
|
||||
keys.reverse();
|
||||
return lineageCleanRoutePoints(keys.map(k => {
|
||||
const [i, j] = k.split(':').map(Number);
|
||||
return [ux[i], uy[j]];
|
||||
}));
|
||||
}
|
||||
|
||||
// Last-resort route: prefer an L shape that is at least connected and then
|
||||
// simplify it. This should rarely be used, but avoids emitting orphaned SVG
|
||||
// fragments when the obstacle grid is over-constrained.
|
||||
const viaA = [[x1, y1], [x1, y2], [x2, y2]];
|
||||
if (lineageRouteSegmentClear(x1, y1, x1, y2, relevant) && lineageRouteSegmentClear(x1, y2, x2, y2, relevant)) return lineageCleanRoutePoints(viaA);
|
||||
const viaB = [[x1, y1], [x2, y1], [x2, y2]];
|
||||
if (lineageRouteSegmentClear(x1, y1, x2, y1, relevant) && lineageRouteSegmentClear(x2, y1, x2, y2, relevant)) return lineageCleanRoutePoints(viaB);
|
||||
return lineageOrthogonalFallbackPoints(x1, y1, x2, y2, relevant);
|
||||
}
|
||||
|
||||
function lineageNodeRectsBetween(positions, nodeW, nodeH, y1, y2, excludeIds = new Set(), pad = 10) {
|
||||
|
|
@ -63,6 +281,71 @@ function lineageHorizontalLaneClear(y, x1, x2, rects) {
|
|||
return !(rects || []).some(r => y > r.top && y < r.bottom && Math.max(left, r.left) < Math.min(right, r.right));
|
||||
}
|
||||
|
||||
|
||||
function lineageRouteVerticalClear(x, y1, y2, rects) {
|
||||
const top = Math.min(y1, y2);
|
||||
const bottom = Math.max(y1, y2);
|
||||
return !(rects || []).some(r => x > r.left && x < r.right && Math.max(top, r.top) < Math.min(bottom, r.bottom));
|
||||
}
|
||||
|
||||
function lineageRouteHorizontalClear(y, x1, x2, rects) {
|
||||
const left = Math.min(x1, x2);
|
||||
const right = Math.max(x1, x2);
|
||||
return !(rects || []).some(r => y > r.top && y < r.bottom && Math.max(left, r.left) < Math.min(right, r.right));
|
||||
}
|
||||
|
||||
function lineageChooseVerticalDetourX(x, y1, y2, rects, context = {}) {
|
||||
const top = Math.min(y1, y2);
|
||||
const bottom = Math.max(y1, y2);
|
||||
const blockers = (rects || []).filter(r => Math.max(top, r.top) < Math.min(bottom, r.bottom));
|
||||
const width = Number(context.width || 0) || 0;
|
||||
const minX = 8;
|
||||
const maxX = width > 16 ? width - 8 : Math.max(x + 120, ...blockers.map(r => r.right + 18), 240);
|
||||
const candidates = [x - 34, x + 34, x - 58, x + 58];
|
||||
for (const r of blockers) {
|
||||
candidates.push(r.left - 12, r.right + 12, r.left - 22, r.right + 22);
|
||||
}
|
||||
const unique = Array.from(new Set(candidates
|
||||
.map(v => clamp(Math.round(v * 10) / 10, minX, maxX))))
|
||||
.filter(v => Number.isFinite(v) && lineageRouteVerticalClear(v, y1, y2, rects));
|
||||
const pool = unique.length ? unique : Array.from(new Set(candidates.map(v => clamp(Math.round(v * 10) / 10, minX, maxX)))).filter(Number.isFinite);
|
||||
if (!pool.length) return clamp(x, minX, maxX);
|
||||
return pool.sort((a, b) => Math.abs(a - x) - Math.abs(b - x))[0];
|
||||
}
|
||||
|
||||
function lineageChooseChildDetourY(y, x1, x2, rects, context = {}) {
|
||||
const height = Number(context.height || 0) || 0;
|
||||
const minY = 8;
|
||||
const maxY = height > 16 ? height - 8 : Math.max(y + 120, ...((rects || []).map(r => r.bottom + 18)), 240);
|
||||
const blockers = (rects || []).filter(r => Math.max(Math.min(x1, x2), r.left) < Math.min(Math.max(x1, x2), r.right));
|
||||
const candidates = [y - 18, y + 18, y - 32, y + 32, y - 52, y + 52];
|
||||
for (const r of blockers) {
|
||||
candidates.push(r.top - 12, r.bottom + 12, r.top - 22, r.bottom + 22);
|
||||
}
|
||||
const unique = Array.from(new Set(candidates
|
||||
.map(v => clamp(Math.round(v * 10) / 10, minY, maxY))))
|
||||
.filter(v => Number.isFinite(v) && lineageRouteHorizontalClear(v, x1, x2, rects));
|
||||
const pool = unique.length ? unique : Array.from(new Set(candidates.map(v => clamp(Math.round(v * 10) / 10, minY, maxY)))).filter(Number.isFinite);
|
||||
if (!pool.length) return clamp(y, minY, maxY);
|
||||
return pool.sort((a, b) => Math.abs(a - y) - Math.abs(b - y))[0];
|
||||
}
|
||||
|
||||
function lineageVerticalPathAvoidingNodes(x, y1, y2, bridges = [], rects = [], context = {}) {
|
||||
if (lineageRouteVerticalClear(x, y1, y2, rects)) return lineageVerticalBridgePath(x, y1, y2, bridges);
|
||||
return lineagePathFromPoints(lineageOrthogonalRoutePoints(x, y1, x, y2, rects, context));
|
||||
}
|
||||
|
||||
function lineageHorizontalConnectorAvoidingNodes(x1, y, x2, rects = [], context = {}) {
|
||||
if (Math.abs(x2 - x1) <= 0.5) return '';
|
||||
if (lineageRouteHorizontalClear(y, x1, x2, rects)) return ` L ${x2.toFixed(1)} ${y.toFixed(1)}`;
|
||||
const points = lineageOrthogonalRoutePoints(x1, y, x2, y, rects, context).slice(1);
|
||||
return points.map(p => ` L ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join('');
|
||||
}
|
||||
|
||||
function lineageHorizontalPathAvoidingNodes(x1, y, x2, rects = [], context = {}) {
|
||||
return lineagePathFromPoints(lineageOrthogonalRoutePoints(x1, y, x2, y, rects, context));
|
||||
}
|
||||
|
||||
function lineageLanePenalty(y, x1, x2, rects) {
|
||||
let penalty = 0;
|
||||
for (const r of rects || []) {
|
||||
|
|
@ -132,8 +415,14 @@ function lineageCrossGenerationPartnerPath(a, b, nodeW, nodeH, unionY, context =
|
|||
const bMidY = b.y + nodeH / 2;
|
||||
const aSideX = railX >= a.x + nodeW / 2 ? a.x + nodeW : a.x;
|
||||
const bSideX = railX >= b.x + nodeW / 2 ? b.x + nodeW : b.x;
|
||||
const points = [[aSideX, aMidY], [railX, aMidY], [railX, bMidY], [bSideX, bMidY]];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: railX, unionY: Math.max(aMidY, bMidY) };
|
||||
const exclude = new Set([a.node?.id, b.node?.id].filter(Boolean));
|
||||
const rects = lineageRouteNodeRects(context.positions, nodeW, nodeH, exclude, 10);
|
||||
const points = lineageOrthogonalRoutePoints(aSideX, aMidY, bSideX, bMidY, rects, { ...context, preferredX: railX, routeMargin: 180 });
|
||||
const clean = lineageCleanRoutePoints(points);
|
||||
const segments = lineageSegmentsFromPoints(clean);
|
||||
const verticals = segments.filter(seg => seg.kind === "v").sort((sa, sb) => Math.abs(sb.y2 - sb.y1) - Math.abs(sa.y2 - sa.y1));
|
||||
const rail = verticals[0];
|
||||
return { d: lineagePathFromPoints(clean), segments, unionX: rail ? rail.x1 : railX, unionY: Math.max(aMidY, bMidY) };
|
||||
}
|
||||
|
||||
function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0, context = {}) {
|
||||
|
|
@ -153,8 +442,10 @@ function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffs
|
|||
const direct = directClear && left.rowIndex === right.rowIndex && x2 - x1 <= 156 && Math.abs(midYLeft - midYRight) < 10
|
||||
&& (!Number.isFinite(unionY) || unionY <= Math.max(midYLeft, midYRight) + 16);
|
||||
if (direct) {
|
||||
const y = directY;
|
||||
return { d: `M ${x1.toFixed(1)} ${y.toFixed(1)} L ${x2.toFixed(1)} ${y.toFixed(1)}`, segments: [{ kind: "h", x1, y1: y, x2, y2: y }] };
|
||||
const points = Math.abs(midYLeft - midYRight) < 0.5
|
||||
? [[x1, midYLeft], [x2, midYRight]]
|
||||
: [[x1, midYLeft], [(x1 + x2) / 2, midYLeft], [(x1 + x2) / 2, midYRight], [x2, midYRight]];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(lineageCleanRoutePoints(points)), unionX: (x1 + x2) / 2, unionY: (midYLeft + midYRight) / 2 };
|
||||
}
|
||||
const gap = x2 - x1;
|
||||
const bend = Math.max(10, Math.min(24, gap * 0.20));
|
||||
|
|
@ -172,12 +463,17 @@ function lineageSameGenerationPartnerPath(a, b, nodeW, nodeH, unionY, attachOffs
|
|||
midYLeft = left.y + nodeH / 2 + leftOffset;
|
||||
midYRight = right.y + nodeH / 2 + rightOffset;
|
||||
}
|
||||
const xLeftBend = Math.min(x1 + bend, x2 - 4);
|
||||
const xRightBend = Math.max(x2 - bend, x1 + 4);
|
||||
const points = [
|
||||
[x1, midYLeft], [xLeftBend, midYLeft], [xLeftBend, y], [xRightBend, y], [xRightBend, midYRight], [x2, midYRight]
|
||||
];
|
||||
return { d: lineagePathFromPoints(points), segments: lineageSegmentsFromPoints(points), unionX: (xLeftBend + xRightBend) / 2, unionY: y };
|
||||
const routeContext = { ...context, preferredY: y, routeMargin: Math.max(140, Math.abs(x2 - x1) + nodeW) };
|
||||
const points = lineageOrthogonalRoutePoints(x1, midYLeft, x2, midYRight, rects, routeContext);
|
||||
const clean = lineageCleanRoutePoints(points);
|
||||
const horizontals = lineageSegmentsFromPoints(clean).filter(seg => seg.kind === "h");
|
||||
const mainH = horizontals.sort((sa, sb) => Math.abs(sb.x2 - sb.x1) - Math.abs(sa.x2 - sa.x1))[0];
|
||||
return {
|
||||
d: lineagePathFromPoints(clean),
|
||||
segments: lineageSegmentsFromPoints(clean),
|
||||
unionX: mainH ? (mainH.x1 + mainH.x2) / 2 : (x1 + x2) / 2,
|
||||
unionY: mainH ? mainH.y1 : y
|
||||
};
|
||||
}
|
||||
|
||||
function lineagePartnerPathFromPositions(a, b, nodeW, nodeH, unionY, attachOffsetA = 0, attachOffsetB = 0, context = {}) {
|
||||
|
|
@ -235,36 +531,37 @@ function lineageIntersectionYsForVertical(x, y1, y2, horizontalSegments) {
|
|||
return ys;
|
||||
}
|
||||
|
||||
function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals) {
|
||||
function lineageBuildChildPathGroup(group, nodeW, nodeH, bridgeHorizontals, context = {}) {
|
||||
const children = group.children;
|
||||
if (!children.length) return { d: '' };
|
||||
const joinX = group.joinX;
|
||||
const fromY = group.fromY;
|
||||
const laneY = group.busY;
|
||||
const routeRects = group.routeRects || [];
|
||||
const childPoints = children
|
||||
.map(c => ({ x: c.x + nodeW / 2, y: c.y }))
|
||||
.sort((a, b) => a.x - b.x || a.y - b.y);
|
||||
if (childPoints.length === 1 && Math.abs(childPoints[0].x - joinX) <= 0.5) {
|
||||
const p = childPoints[0];
|
||||
const bridges = lineageIntersectionYsForVertical(p.x, fromY, p.y, bridgeHorizontals);
|
||||
return { d: lineageVerticalBridgePath(p.x, fromY, p.y, bridges) };
|
||||
return { d: lineageVerticalPathAvoidingNodes(p.x, fromY, p.y, bridges, routeRects, context) };
|
||||
}
|
||||
if (childPoints.length === 1) {
|
||||
const p = childPoints[0];
|
||||
const parentBridges = lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals);
|
||||
const childBridges = lineageIntersectionYsForVertical(p.x, laneY, p.y, bridgeHorizontals);
|
||||
const parentLane = lineageVerticalBridgePath(joinX, fromY, laneY, parentBridges);
|
||||
const horizontal = ` L ${p.x.toFixed(1)} ${laneY.toFixed(1)}`;
|
||||
const childLane = lineageVerticalBridgePath(p.x, laneY, p.y, childBridges).replace(/^M\s+[-\d.]+\s+[-\d.]+/, "");
|
||||
const parentLane = lineageVerticalPathAvoidingNodes(joinX, fromY, laneY, parentBridges, routeRects, context);
|
||||
const horizontal = lineageHorizontalConnectorAvoidingNodes(joinX, laneY, p.x, routeRects, context);
|
||||
const childLane = lineageVerticalPathAvoidingNodes(p.x, laneY, p.y, childBridges, routeRects, context).replace(/^M\s+[-\d.]+\s+[-\d.]+/, "");
|
||||
return { d: `${parentLane}${horizontal}${childLane}` };
|
||||
}
|
||||
const minX = Math.min(joinX, ...childPoints.map(p => p.x));
|
||||
const maxX = Math.max(joinX, ...childPoints.map(p => p.x));
|
||||
let d = lineageVerticalBridgePath(joinX, fromY, laneY, lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals));
|
||||
d += ` M ${minX.toFixed(1)} ${laneY.toFixed(1)} L ${maxX.toFixed(1)} ${laneY.toFixed(1)}`;
|
||||
let d = lineageVerticalPathAvoidingNodes(joinX, fromY, laneY, lineageIntersectionYsForVertical(joinX, fromY, laneY, bridgeHorizontals), routeRects, context);
|
||||
d += ` ${lineageHorizontalPathAvoidingNodes(minX, laneY, maxX, routeRects, context)}`;
|
||||
for (const p of childPoints) {
|
||||
const bridges = lineageIntersectionYsForVertical(p.x, laneY, p.y, bridgeHorizontals);
|
||||
const branch = lineageVerticalBridgePath(p.x, laneY, p.y, bridges);
|
||||
const branch = lineageVerticalPathAvoidingNodes(p.x, laneY, p.y, bridges, routeRects, context);
|
||||
d += ` ${branch}`;
|
||||
}
|
||||
return { d };
|
||||
|
|
@ -419,7 +716,7 @@ function lineageAssignPartnerAttachOffsets(linkGroups) {
|
|||
for (const [key, groups] of buckets.entries()) {
|
||||
groups.sort((a, b) => a.joinX - b.joinX || a.childTop - b.childTop);
|
||||
const n = groups.length;
|
||||
const step = 14;
|
||||
const step = 18;
|
||||
groups.forEach((group, i) => {
|
||||
const offset = n <= 1 ? 0 : (i - (n - 1) / 2) * step;
|
||||
const [nodeId] = key.split(':');
|
||||
|
|
@ -505,7 +802,12 @@ function lineageBuildLinks(nodes, idSet, layout) {
|
|||
if (group.children.length > 1 || (childCenters.length === 1 && Math.abs(childCenters[0] - group.joinX) > 0.5)) {
|
||||
horizontalSegments.push({ kind: 'h', x1: Math.min(group.joinX, ...childCenters), y1: group.busY, x2: Math.max(group.joinX, ...childCenters), y2: group.busY });
|
||||
}
|
||||
const child = lineageBuildChildPathGroup(group, nodeW, nodeH, horizontalSegments);
|
||||
const excludeIds = new Set([
|
||||
...group.parents.map(p => p?.node?.id).filter(Boolean),
|
||||
...group.children.map(c => c?.node?.id).filter(Boolean),
|
||||
]);
|
||||
group.routeRects = lineageRouteNodeRects(positions, nodeW, nodeH, excludeIds, 12);
|
||||
const child = lineageBuildChildPathGroup(group, nodeW, nodeH, horizontalSegments, layout);
|
||||
if (child.d) childLinks.push(child.d);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,45 +75,6 @@ function lineageLayoutRowsFast(rows, idSet) {
|
|||
return { positions, width, height, nodeW: NODE_W, nodeH: NODE_H, topPad: TOP_PAD, rowGap: ROW_GAP, fastMode: true };
|
||||
}
|
||||
|
||||
function lineageBuildLinksFast(nodes, idSet, layout) {
|
||||
const { positions, nodeW, nodeH } = layout;
|
||||
const partnerLinks = [];
|
||||
const childLinks = [];
|
||||
const seenPartners = new Set();
|
||||
let childLinkGroups = 0;
|
||||
let childLinkEdges = 0;
|
||||
for (const child of nodes) {
|
||||
const childPos = positions.get(child.id);
|
||||
if (!childPos) continue;
|
||||
const parents = lineageParentIds(child, idSet).map(id => positions.get(id)).filter(Boolean).sort((a, b) => a.x - b.x);
|
||||
if (!parents.length) continue;
|
||||
childLinkGroups += 1;
|
||||
childLinkEdges += parents.length;
|
||||
const childX = childPos.x + nodeW / 2;
|
||||
const childY = childPos.y;
|
||||
let fromY = Math.max(...parents.map(p => p.y + nodeH));
|
||||
let joinX = parents.reduce((sum, p) => sum + p.x + nodeW / 2, 0) / parents.length;
|
||||
if (parents.length >= 2) {
|
||||
const left = parents[0];
|
||||
const right = parents[parents.length - 1];
|
||||
const partnerKey = parents.map(p => p.node?.id).filter(Boolean).sort().join("+");
|
||||
const y = ((left.y + nodeH / 2) + (right.y + nodeH / 2)) / 2;
|
||||
const x1 = left.x + nodeW;
|
||||
const x2 = right.x;
|
||||
if (x2 > x1) {
|
||||
joinX = clamp(joinX, x1 + 8, x2 - 8);
|
||||
fromY = y;
|
||||
if (partnerKey && !seenPartners.has(partnerKey)) {
|
||||
seenPartners.add(partnerKey);
|
||||
partnerLinks.push(`M ${x1.toFixed(1)} ${y.toFixed(1)} L ${x2.toFixed(1)} ${y.toFixed(1)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const laneY = Math.min(childY - 22, fromY + Math.max(36, (childY - fromY) * 0.54));
|
||||
childLinks.push(`M ${joinX.toFixed(1)} ${fromY.toFixed(1)} L ${joinX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${laneY.toFixed(1)} L ${childX.toFixed(1)} ${childY.toFixed(1)}`);
|
||||
}
|
||||
return { partnerLinks, childLinks, childLinkGroups, childLinkEdges };
|
||||
}
|
||||
|
||||
function buildLineageTreeLayout(component, family) {
|
||||
family = lineageEdgeFamily(family);
|
||||
|
|
@ -125,7 +86,11 @@ function buildLineageTreeLayout(component, family) {
|
|||
const rows = lineageRowsForLayout(nodes, generations, idToNode, idSet);
|
||||
const perf = lineageFastLayoutNeeded(nodes, idSet, family);
|
||||
const layout = perf.fast ? lineageLayoutRowsFast(rows, idSet) : lineageLayoutRows(rows, idSet);
|
||||
const links = perf.fast ? lineageBuildLinksFast(nodes, idSet, layout) : lineageBuildLinks(nodes, idSet, layout);
|
||||
// Keep the compact fast layout for very large families, but still use the
|
||||
// robust routed edge builder. The old fast edge builder emitted straight
|
||||
// child and partner lines, which could be masked behind unrelated nodes and
|
||||
// look like disconnected line fragments in complex trees.
|
||||
const links = lineageBuildLinks(nodes, idSet, layout);
|
||||
|
||||
const labels = generations.map((g, rowIndex) => ({ generation: g, y: layout.topPad + rowIndex * (layout.nodeH + layout.rowGap) + 18 }));
|
||||
const renderedNodes = nodes.map(n => ({ node: n, ...(layout.positions.get(n.id) || {}) })).filter(n => Number.isFinite(n.x) && Number.isFinite(n.y));
|
||||
|
|
@ -149,7 +114,7 @@ function lineageFamilyHtml(component, index, family, signature = "") {
|
|||
const nodeHtml = layout.nodes.map(p => lineageTreeNodeHtml(p.node, p.x, p.y)).join("");
|
||||
const empty = !layout.childLinks.length ? `<div class="lineage-empty-family">\u89aa\u5b50\u95a2\u4fc2\u306e\u7dda\u306f\u307e\u3060\u3042\u308a\u307e\u305b\u3093\u3002</div>` : "";
|
||||
|
||||
const html = `<section class="lineage-family lineage-family-tree" data-lineage-index="{{LINEAGE_INDEX}}" data-lineage-key="${escapeHtml(cacheKey)}" data-lineage-child-edges="${childEdgeCount}" data-lineage-rendered-child-edges="${layout.childLinkEdges || 0}" data-lineage-child-groups="${layout.childLinkGroups || 0}" data-lineage-child-paths="${layout.childLinks.length}">
|
||||
const html = `<section class="lineage-family lineage-family-tree" data-lineage-key="${escapeHtml(cacheKey)}">
|
||||
<h3>\u5bb6\u7cfb {{LINEAGE_INDEX}} / ${uniqueComponent.length}\u5339 / \u751f\u5b58 ${aliveCount}</h3>
|
||||
<div class="lineage-tree-panel" style="width:${Math.ceil(layout.width)}px;height:${Math.ceil(layout.height)}px">
|
||||
<svg class="lineage-links" viewBox="0 0 ${Math.ceil(layout.width)} ${Math.ceil(layout.height)}" aria-hidden="true">
|
||||
|
|
@ -163,6 +128,32 @@ function lineageFamilyHtml(component, index, family, signature = "") {
|
|||
return lineageHydrateFamilyHtml(html, index);
|
||||
}
|
||||
|
||||
|
||||
function lineageArchiveNeedsRender() {
|
||||
const familyVersion = world.familyVersion || 0;
|
||||
const familyDirty = Boolean(world.familyTreeDirty);
|
||||
return !uiCache.archiveVersion || uiCache.archiveFamilyVersion !== familyVersion || familyDirty;
|
||||
}
|
||||
|
||||
function lineageDeferArchiveIfOffscreen(hiddenInterval = 30000) {
|
||||
if (!uiCache.archiveVersion || lineageArchiveNearViewport()) return false;
|
||||
uiCache.archivePendingOffscreen = true;
|
||||
const now = performance.now();
|
||||
if (now - (uiCache.archiveLastHiddenCheckAt || -Infinity) < hiddenInterval) return true;
|
||||
uiCache.archiveLastHiddenCheckAt = now;
|
||||
return false;
|
||||
}
|
||||
|
||||
function lineageScheduleArchiveIdle(timeout = 1200) {
|
||||
if (uiCache.archiveRenderTimer || uiCache.archiveScheduled) return false;
|
||||
uiCache.archiveScheduled = true;
|
||||
const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
|
||||
schedule(() => {
|
||||
renderArchive();
|
||||
}, { timeout });
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderArchive() {
|
||||
if (!ui.archiveContent) return;
|
||||
// Auto update OFF now means "do not keep refreshing", not "hide the tree".
|
||||
|
|
@ -193,14 +184,8 @@ function renderArchive() {
|
|||
if (familyDirty) uiCache.archivePendingDirtyAfterRun = true;
|
||||
return;
|
||||
}
|
||||
if (uiCache.archiveVersion && uiCache.archiveFamilyVersion === familyVersion && !familyDirty) return;
|
||||
if (uiCache.archiveVersion && !lineageArchiveNearViewport()) {
|
||||
uiCache.archivePendingOffscreen = true;
|
||||
const hiddenInterval = 30000;
|
||||
const hiddenNow = performance.now();
|
||||
if (hiddenNow - (uiCache.archiveLastHiddenCheckAt || -Infinity) < hiddenInterval) return;
|
||||
uiCache.archiveLastHiddenCheckAt = hiddenNow;
|
||||
}
|
||||
if (!lineageArchiveNeedsRender()) return;
|
||||
if (lineageDeferArchiveIfOffscreen()) return;
|
||||
const now = performance.now();
|
||||
const minUpdateGap = 4800;
|
||||
if (uiCache.archiveVersion && !uiCache.archiveRenderForce && now - (uiCache.archiveLastRenderedAt || 0) < minUpdateGap) {
|
||||
|
|
@ -315,10 +300,13 @@ function resetArchiveRenderState() {
|
|||
}
|
||||
uiCache.archiveRenderToken = "";
|
||||
uiCache.archiveRenderRunning = false;
|
||||
uiCache.archiveRenderForce = false;
|
||||
uiCache.archiveScheduled = false;
|
||||
uiCache.archivePendingOffscreen = false;
|
||||
uiCache.archivePendingDirtyAfterRun = false;
|
||||
uiCache.archiveLastHiddenCheckAt = -Infinity;
|
||||
uiCache.archiveLastRenderedAt = 0;
|
||||
uiCache.deferArchiveUntil = 0;
|
||||
uiCache.archiveVersion = "";
|
||||
uiCache.archiveFamilyVersion = null;
|
||||
uiCache.archiveHtmlCache?.clear?.();
|
||||
|
|
@ -331,24 +319,13 @@ function scheduleArchiveWindowRender() {
|
|||
return;
|
||||
}
|
||||
const familyDirty = Boolean(world.familyTreeDirty);
|
||||
if (uiCache.archiveVersion && uiCache.archiveFamilyVersion === (world.familyVersion || 0) && !familyDirty) return;
|
||||
if (!lineageArchiveNeedsRender()) return;
|
||||
if (uiCache.archiveRenderRunning) {
|
||||
if (familyDirty) uiCache.archivePendingDirtyAfterRun = true;
|
||||
return;
|
||||
}
|
||||
if (uiCache.archiveVersion && !lineageArchiveNearViewport()) {
|
||||
uiCache.archivePendingOffscreen = true;
|
||||
const now = performance.now();
|
||||
if (now - (uiCache.archiveLastHiddenCheckAt || -Infinity) < 30000) return;
|
||||
uiCache.archiveLastHiddenCheckAt = now;
|
||||
}
|
||||
if (uiCache.archiveRenderTimer) return;
|
||||
if (uiCache.archiveScheduled) return;
|
||||
uiCache.archiveScheduled = true;
|
||||
const schedule = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
|
||||
schedule(() => {
|
||||
renderArchive();
|
||||
}, { timeout: 1200 });
|
||||
if (lineageDeferArchiveIfOffscreen()) return;
|
||||
lineageScheduleArchiveIdle(1200);
|
||||
}
|
||||
|
||||
function schedulePendingArchiveRender() {
|
||||
|
|
@ -356,7 +333,7 @@ function schedulePendingArchiveRender() {
|
|||
if (!uiCache.archivePendingOffscreen) return;
|
||||
if (!lineageArchiveNearViewport()) return;
|
||||
uiCache.archivePendingOffscreen = false;
|
||||
scheduleArchiveWindowRender();
|
||||
if (lineageArchiveNeedsRender()) scheduleArchiveWindowRender();
|
||||
}
|
||||
|
||||
window.resetArchiveRenderState = resetArchiveRenderState;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@
|
|||
}
|
||||
|
||||
global.TarinaiGroundUI = Object.freeze({
|
||||
bind: bindGroundUI,
|
||||
update: updateGroundUI,
|
||||
});
|
||||
global.bindGroundUI = bindGroundUI;
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ function selectedInfoRowHtml(label, valueHtml, metric = "") {
|
|||
function selectedInventoryItems(t) {
|
||||
const worldRef = t?.world || world;
|
||||
const labels = { grass_bed: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", plushie: "\u306c\u3044\u3050\u308b\u307f" };
|
||||
const items = (worldRef?.items || []).filter(it => it && !it.dead && it.isStructure && it.ownerId === t?.id);
|
||||
const items = (worldRef?.ownedItemsFor?.(t?.id) || worldRef?.items || []).filter(it => it && !it.dead && it.isStructure && it.ownerId === t?.id);
|
||||
const counts = new Map();
|
||||
for (const it of items) {
|
||||
const label = labels[it.type] || toolLabel(it.type) || it.type;
|
||||
|
|
|
|||
|
|
@ -10,9 +10,27 @@ function applyToolTextIcon(btn, def, toolId) {
|
|||
return true;
|
||||
}
|
||||
|
||||
const SIZE_ORDER = typeof TOOL_SIZE_ORDER !== "undefined" ? TOOL_SIZE_ORDER : ["small", "medium", "large"];
|
||||
function cssToolIconPath(path) {
|
||||
const raw = String(path || "").trim();
|
||||
if (!raw) return "";
|
||||
if (/^(?:[a-z]+:|data:|\/)/i.test(raw)) return raw;
|
||||
if (raw.startsWith("../")) return raw;
|
||||
if (raw.startsWith("./assets/")) return `../${raw.slice(2)}`;
|
||||
if (raw.startsWith("assets/")) return `../${raw}`;
|
||||
return raw;
|
||||
}
|
||||
|
||||
const SIZE_LABELS = typeof TOOL_SIZE_LABELS !== "undefined" ? TOOL_SIZE_LABELS : { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
||||
function applyToolImageIcon(btn, def) {
|
||||
if (!btn || !def?.icon) return false;
|
||||
const iconPath = cssToolIconPath(def.icon);
|
||||
if (!iconPath) return false;
|
||||
btn.style.setProperty("--tool-icon", `url(${JSON.stringify(iconPath)})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const SIZE_ORDER = window.TarinaiItemToolMetadata?.TOOL_SIZE_ORDER || ["small", "medium", "large"];
|
||||
|
||||
const SIZE_LABELS = window.TarinaiItemToolMetadata?.TOOL_SIZE_LABELS || { small: "\u5c0f", medium: "\u4e2d", large: "\u5927" };
|
||||
|
||||
function renderToolPalette() {
|
||||
if (!ui?.toolPalette || typeof toolCategories !== "function") return;
|
||||
|
|
@ -37,7 +55,8 @@ function renderToolPalette() {
|
|||
if (!def || def.simulationOnly || toolId === "observe") continue;
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "tool";
|
||||
applyToolTextIcon(btn, def, toolId);
|
||||
const hasTextIcon = applyToolTextIcon(btn, def, toolId);
|
||||
if (!hasTextIcon) applyToolImageIcon(btn, def);
|
||||
btn.dataset.tool = toolId;
|
||||
btn.dataset.toolCategory = category.id || "";
|
||||
const label = document.createElement("span");
|
||||
|
|
@ -108,12 +127,10 @@ function syncToolSizeBadges() {
|
|||
const tool = btn.dataset.tool || "";
|
||||
const old = btn.querySelector(".tool-size-badge");
|
||||
if (!SCALABLE_TOOLS.has(tool)) {
|
||||
btn.removeAttribute("data-size");
|
||||
if (old) old.remove();
|
||||
continue;
|
||||
}
|
||||
const size = toolSizeFor(tool);
|
||||
btn.dataset.size = size;
|
||||
let badge = old;
|
||||
if (!badge) {
|
||||
badge = document.createElement("b");
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
(function () {
|
||||
const APP_VERSION = "39.15.136";
|
||||
const APP_BUILD = "tarinai-pipe-v39-15-136";
|
||||
const APP_VERSION = "39.15.164";
|
||||
const APP_BUILD = "tarinai-sleep-family-tooltip-v39-15-164";
|
||||
const APP_CACHE_NAME = `tarinai-colony-${APP_VERSION}`;
|
||||
const STATIC_VERSION_PARAM = `v=${APP_VERSION}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
"use strict";
|
||||
|
||||
(function (global) {
|
||||
const DomainIds = global.TarinaiDomainIds;
|
||||
if (!DomainIds) throw new Error("TarinaiDomainIds must be loaded before weather_system.js");
|
||||
const WEATHER_IDS = DomainIds.WEATHER_IDS;
|
||||
function canonicalWeather(value = "sunny") {
|
||||
return DomainIds.canonicalWeatherId(value);
|
||||
}
|
||||
|
||||
function chooseNext(current = "sunny") {
|
||||
const choices = current === "light_rain"
|
||||
? ["sunny", "cloudy"]
|
||||
: ["sunny", "cloudy", "light_rain"];
|
||||
const id = canonicalWeather(current);
|
||||
const choices = id === "light_rain"
|
||||
? WEATHER_IDS.filter(weather => weather !== "light_rain")
|
||||
: WEATHER_IDS;
|
||||
return pick(choices);
|
||||
}
|
||||
|
||||
|
|
@ -20,12 +28,11 @@
|
|||
function enforceFixedWeather(worldRef) {
|
||||
const fixed = fixedWeatherFor(worldRef);
|
||||
if (!fixed || !worldRef) return false;
|
||||
const previous = worldRef.weather || "sunny";
|
||||
worldRef.weather = fixed;
|
||||
const previous = canonicalWeather(worldRef.weather || "sunny");
|
||||
worldRef.weather = canonicalWeather(fixed);
|
||||
worldRef.nextWeatherChange = resetTimer();
|
||||
if (previous !== fixed) {
|
||||
worldRef.emit?.("weather:changed", { previous, next: fixed });
|
||||
worldRef.log?.(`\u5929\u6C17: ${weatherLabel(fixed)}`);
|
||||
if (previous !== worldRef.weather) {
|
||||
worldRef.log?.(`\u5929\u6C17: ${weatherLabel(worldRef.weather)}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -33,7 +40,7 @@
|
|||
function shouldDropRainWater(worldRef, dt = 0) {
|
||||
return Boolean(
|
||||
worldRef?.weather === "light_rain" &&
|
||||
Math.random() < Math.max(0, Number(dt) || 0) * 0.12
|
||||
Math.random() < Math.max(0, Number(dt) || 0) * 0.24
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -66,12 +73,11 @@
|
|||
function applyNextWeather(worldRef) {
|
||||
if (!worldRef) return null;
|
||||
if (enforceFixedWeather(worldRef)) return worldRef.weather || "sunny";
|
||||
const previous = worldRef.weather || "sunny";
|
||||
const previous = canonicalWeather(worldRef.weather || "sunny");
|
||||
const next = chooseNext(previous);
|
||||
worldRef.weather = next;
|
||||
worldRef.weather = canonicalWeather(next);
|
||||
worldRef.nextWeatherChange = resetTimer();
|
||||
if (next !== previous) {
|
||||
worldRef.emit?.("weather:changed", { previous, next });
|
||||
worldRef.log?.(`\u5929\u6C17: ${weatherLabel(next)}`);
|
||||
}
|
||||
return next;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@
|
|||
maxHp: ANT_WORKER_HP || 32,
|
||||
});
|
||||
this.ants.push(a);
|
||||
this.emit?.("ant:spawned", { ant: a, nest });
|
||||
if (typeof syncAntNestCount === "function") syncAntNestCount(this, nest);
|
||||
nest.antSpawnTimer = rand(2.5, 5.5) + outside * 0.65;
|
||||
this.drawListDirty = true;
|
||||
|
|
@ -72,7 +71,6 @@
|
|||
target.y = home.y;
|
||||
audio.antNest?.();
|
||||
target.die("\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f");
|
||||
this.emit?.("ant:carried", { nest: home, target, complete: true });
|
||||
this.drawListDirty = true;
|
||||
},
|
||||
|
||||
|
|
@ -95,7 +93,6 @@
|
|||
foundingDelayUntil: (this.time || 0) + 0.35,
|
||||
});
|
||||
this.ants.push(queen);
|
||||
this.emit?.("ant:spawned", { ant: queen, nest: home });
|
||||
audio.queenAnt?.();
|
||||
this.drawListDirty = true;
|
||||
return queen;
|
||||
|
|
|
|||
|
|
@ -533,7 +533,6 @@
|
|||
silentLog: !!options.silentBirthCancel,
|
||||
});
|
||||
}
|
||||
this.emit?.(String(normalized).includes("\u55a7\u5629") ? "fight:hit" : "impact:hit", { target, amount, cause: normalized, source: options.source || null });
|
||||
if (physicalHit) {
|
||||
target.lastPhysicalCollisionDamageAt = this.time || 0;
|
||||
}
|
||||
|
|
@ -559,7 +558,6 @@
|
|||
attacker.defeatedById = null;
|
||||
victim.fightTimer = Math.min(victim.fightTimer || 0, deterministicRange(this, "fight-defeat-victim-timer", 0.20, 0.42, victim, attacker));
|
||||
attacker.fightTimer = Math.min(attacker.fightTimer || 0, deterministicRange(this, "fight-defeat-attacker-timer", 0.20, 0.42, victim, attacker));
|
||||
this.emit?.("fight:lost", { winner: attacker, loser: victim, damage });
|
||||
if (victim.enterPanic) victim.enterPanic({ target: attacker, threat: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.1, wake: true, cause: "fight_lost" });
|
||||
else { victim.setActionState?.("panic", { target: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1)); }
|
||||
const dx = victim.x - attacker.x;
|
||||
|
|
@ -591,7 +589,6 @@
|
|||
this.maybeApplyTarinaiChampionByFightRecord?.(winner, loser);
|
||||
this.recordFamily?.(winner);
|
||||
this.recordFamily?.(loser);
|
||||
this.emit?.("fight:ended", { winner, loser });
|
||||
this.markFightPairCooldown?.(winner, loser, CONFIG.fightCooldown + 8);
|
||||
for (const t of [winner, loser]) {
|
||||
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this, "fight-outcome-cooldown", 4, 8, t, winner, loser));
|
||||
|
|
@ -692,7 +689,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)" }));
|
||||
this.log(isBalloon ? "水風船をつついて飛ばした。" : "ボールをつついて転がした。", "observe", { eventType: "ball_poke" });
|
||||
if (!isBalloon) this.log("ボールをつついて転がした。", "observe", { eventType: "ball_poke" });
|
||||
return true;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,12 @@
|
|||
const World = global.World;
|
||||
if (!World) throw new Error("World is not available for mixin: world_environment.js");
|
||||
|
||||
const Geometry = global.TarinaiGeometry;
|
||||
|
||||
function angleForItemLocal(it) {
|
||||
return itemAngleFor(it);
|
||||
}
|
||||
|
||||
|
||||
function orientedRectAabb(cx, cy, halfW, halfH, angle) {
|
||||
return global.TarinaiGeometry.orientedRectAabb(cx, cy, halfW, halfH, angle);
|
||||
}
|
||||
|
||||
function obstacleQueryDistanceLocal(x, y, it) {
|
||||
if (!it) return Infinity;
|
||||
const type = String(it.type || "");
|
||||
|
|
@ -104,16 +101,6 @@
|
|||
|
||||
|
||||
|
||||
function rectLocalPoint(r, x, y) {
|
||||
return global.TarinaiCollisionFootprints.rectLocalPoint(r, x, y);
|
||||
}
|
||||
|
||||
function rectWorldNormal(r, nx, ny) {
|
||||
return global.TarinaiGeometry.rectWorldNormal(r, nx, ny);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function segmentAabbHitLocal(x1, y1, x2, y2, left, top, right, bottom) {
|
||||
let t0 = 0;
|
||||
let t1 = 1;
|
||||
|
|
@ -151,11 +138,11 @@
|
|||
if (!r) return null;
|
||||
const rr = Math.max(0, Number(radius || 0) || 0) + Math.max(0, Number(padding || 0) || 0);
|
||||
if (r.oriented) {
|
||||
const a = rectLocalPoint(r, x1, y1);
|
||||
const b = rectLocalPoint(r, x2, y2);
|
||||
const a = Geometry.rectLocalPoint(r, x1, y1);
|
||||
const b = Geometry.rectLocalPoint(r, x2, y2);
|
||||
const hit = segmentAabbHitLocal(a.x, a.y, b.x, b.y, -(r.halfW || 0) - rr, -(r.halfH || 0) - rr, (r.halfW || 0) + rr, (r.halfH || 0) + rr);
|
||||
if (!hit) return null;
|
||||
const n = rectWorldNormal(r, hit.nx, hit.ny);
|
||||
const n = Geometry.rectWorldNormal(r, hit.nx, hit.ny);
|
||||
return { t: hit.t, nx: n.x, ny: n.y, rect: r };
|
||||
}
|
||||
const hit = segmentAabbHitLocal(x1, y1, x2, y2, (r.left || 0) - rr, (r.top || 0) - rr, (r.right || 0) + rr, (r.bottom || 0) + rr);
|
||||
|
|
@ -184,6 +171,66 @@
|
|||
return global.TarinaiGeometry.segmentIntersectsOrientedRect(x1, y1, x2, y2, r, padding);
|
||||
}
|
||||
|
||||
function nearbyPoisonBlockHazardRectsLocal(worldRef, x, y, radius, { maxChecks = 24 } = {}) {
|
||||
if ((worldRef?.itemCounts?.poison_block || 0) <= 0) return [];
|
||||
const rects = [];
|
||||
const stamp = (worldRef._poisonHazardQueryStamp = (worldRef._poisonHazardQueryStamp || 0) + 1);
|
||||
let checked = 0;
|
||||
const addRectsFor = (it) => {
|
||||
if (!it || it.dead || it.type !== "poison_block" || it._poisonHazardQueryStamp === stamp) return false;
|
||||
const reach = global.TarinaiMechanicalShapeSystem.reach(it) || Math.max(60, it.r || 64);
|
||||
if (distXY(x, y, it.x, it.y) > radius + reach + 36) return false;
|
||||
const partRects = global.TarinaiMechanicalSystem.poisonHazardRects(it) || [];
|
||||
if (!partRects.length) return false;
|
||||
it._poisonHazardQueryStamp = stamp;
|
||||
checked += 1;
|
||||
for (const rect of partRects) rects.push(rect);
|
||||
return true;
|
||||
};
|
||||
const candidates = worldRef.nearbyHazards?.(x, y, radius + 48, false) || [];
|
||||
for (const it of candidates) {
|
||||
addRectsFor(it);
|
||||
if (checked >= maxChecks) break;
|
||||
}
|
||||
if (checked < maxChecks && !worldRef.nearbyHazards) {
|
||||
for (const it of worldRef.itemsOfType?.("poison_block") || []) {
|
||||
if (addRectsFor(it) && checked >= maxChecks) break;
|
||||
}
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
function applyPoisonBlockContactDamageLocal(worldRef, t, rect, rr) {
|
||||
if (!worldRef || !t || t.dead || !rect?.poisonBlock || !rect.item || rect.item.dead) return false;
|
||||
if (!Geometry.circleOverlapsRect(t.x, t.y, rr, rect, 1.0)) return false;
|
||||
const now = worldRef.time || 0;
|
||||
const key = rect.item.id || "poison";
|
||||
t._poisonBlockHitAt = t._poisonBlockHitAt || Object.create(null);
|
||||
const body = global.TarinaiPhysicsBodySystem.ensureBody(rect.item, worldRef, { syncFromLegacy: false });
|
||||
const solid = body?.collision ? body.collision.solid !== false : true;
|
||||
const cooldown = solid ? 0.30 : 0.42;
|
||||
if ((t._poisonBlockHitAt[key] || -999) + cooldown > now) return false;
|
||||
t._poisonBlockHitAt[key] = now;
|
||||
const velocity = body?.velocity || {};
|
||||
const vx = Number((velocity.x ?? rect.item.vx) || 0) || 0;
|
||||
const vy = Number((velocity.y ?? rect.item.vy) || 0) || 0;
|
||||
const omega = Number((velocity.angular ?? global.TarinaiPhysicsBodySystem.scalar(rect.item, "spin", 0)) || 0) || 0;
|
||||
const speed = Math.hypot(vx, vy) + Math.abs(omega) * Math.max(28, rect.item.r || 64) * 0.35;
|
||||
const base = Math.max(1, Number((body?.hazard?.damage ?? global.TarinaiPhysicsBodySystem.scalar(rect.item, "damage", 7)) || 7) || 7);
|
||||
const damage = clamp(base * (solid ? 1.0 : 0.72) + speed * 0.018, 2.0, 18.0);
|
||||
if (typeof worldRef.applyImpactDamage === "function") worldRef.applyImpactDamage(t, damage, "\u6bd2\u30d6\u30ed\u30c3\u30af", { source: rect.item, x: t.x, y: t.y });
|
||||
else if (typeof t.damage === "function") t.damage(damage, "\u6bd2\u30d6\u30ed\u30c3\u30af");
|
||||
t.showHpBar?.(4.8);
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.82);
|
||||
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.28);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24);
|
||||
if (typeof t.bubble === "function" && now >= (t.lastPoisonBlockBubbleAt || -999) + 0.85) {
|
||||
t.lastPoisonBlockBubbleAt = now;
|
||||
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
nearest(entity, types, maxDist = Infinity) {
|
||||
let best = null, bestD = maxDist;
|
||||
|
|
@ -221,7 +268,7 @@
|
|||
const angle = angleForItemLocal(it);
|
||||
const halfW = len / 2;
|
||||
const halfH = thick / 2;
|
||||
const aabb = orientedRectAabb(it.x || 0, it.y || 0, halfW, halfH, angle);
|
||||
const aabb = Geometry.orientedRectAabb(it.x || 0, it.y || 0, halfW, halfH, angle);
|
||||
return {
|
||||
left: aabb.left, right: aabb.right, top: aabb.top, bottom: aabb.bottom,
|
||||
cx: it.x || 0, cy: it.y || 0, halfW, halfH, angle, cos: aabb.cos, sin: aabb.sin, oriented: true,
|
||||
|
|
@ -233,11 +280,20 @@
|
|||
|
||||
|
||||
nestBoxCapacity(box = null) {
|
||||
return box?.type === "pipe" ? 3 : 5;
|
||||
if (box?.type === "pipe") {
|
||||
const count = (this.items || []).reduce((n, it) => n + (it && !it.dead && it.type === "pipe" ? 1 : 0), 0);
|
||||
return Math.max(3, count * 3);
|
||||
}
|
||||
return 5;
|
||||
},
|
||||
|
||||
isNestContainerItem(it) {
|
||||
return Boolean(it && !it.dead && (typeof globalThis.isNestContainerType === "function" ? globalThis.isNestContainerType(it.type) : (it.type === "nest_box" || it.type === "pipe")));
|
||||
return Boolean(it && !it.dead && globalThis.TarinaiToolRuntime?.isNestContainerItem?.(it));
|
||||
},
|
||||
|
||||
nestBoxById(id) {
|
||||
if (!id) return null;
|
||||
return (this.items || []).find(it => it && !it.dead && it.id === id && this.isNestContainerItem?.(it)) || null;
|
||||
},
|
||||
|
||||
nestBoxBaseRect(it) {
|
||||
|
|
@ -373,9 +429,10 @@
|
|||
},
|
||||
|
||||
shouldIgnoreNestBoxCollisionFor(t, box) {
|
||||
if (!t || !box || box.dead || !(typeof globalThis.isNestContainerType === "function" ? globalThis.isNestContainerType(box.type) : (box.type === "nest_box" || box.type === "pipe"))) return false;
|
||||
if (!t || !box || box.dead || !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(box)) return false;
|
||||
if (t.insideNestBoxId) return true;
|
||||
if (t.target !== box || (t.state !== "seek_bed" && t.state !== "sleep")) return false;
|
||||
const targetBox = t.target === box || t.target?.shelterItem === box || t.target?.hostItem === box;
|
||||
if (!targetBox || !(t.state === "seek_bed" || t.state === "sleep" || t.state === "seek_temperature")) return false;
|
||||
const entry = this.nestBoxEntryPoint(box);
|
||||
const base = this.nestBoxBaseRect(box);
|
||||
const r = box.r || 42;
|
||||
|
|
@ -475,86 +532,10 @@
|
|||
},
|
||||
|
||||
pointInRect(x, y, r, padding = 0) {
|
||||
return global.TarinaiCollisionFootprints.pointInRect(x, y, r, padding) ?? false;
|
||||
return Geometry.pointInRect(x, y, r, padding) ?? false;
|
||||
},
|
||||
|
||||
|
||||
nearbyPoisonBlockHazardRects(x, y, radius, { maxChecks = 24 } = {}) {
|
||||
if ((this.itemCounts?.poison_block || 0) <= 0) return [];
|
||||
const rects = [];
|
||||
const stamp = (this._poisonHazardQueryStamp = (this._poisonHazardQueryStamp || 0) + 1);
|
||||
let checked = 0;
|
||||
const addRectsFor = (it) => {
|
||||
if (!it || it.dead || it.type !== "poison_block" || it._poisonHazardQueryStamp === stamp) return false;
|
||||
const reach = global.TarinaiMechanicalShapeSystem.reach(it) || Math.max(60, it.r || 64);
|
||||
if (distXY(x, y, it.x, it.y) > radius + reach + 36) return false;
|
||||
const partRects = global.TarinaiMechanicalSystem.poisonHazardRects(it) || [];
|
||||
if (!partRects.length) return false;
|
||||
it._poisonHazardQueryStamp = stamp;
|
||||
checked += 1;
|
||||
for (const rect of partRects) rects.push(rect);
|
||||
return true;
|
||||
};
|
||||
const candidates = this.nearbyHazards?.(x, y, radius + 48, false) || [];
|
||||
for (const it of candidates) {
|
||||
addRectsFor(it);
|
||||
if (checked >= maxChecks) break;
|
||||
}
|
||||
if (checked < maxChecks && !this.nearbyHazards) {
|
||||
for (const it of this.itemsOfType?.("poison_block") || []) {
|
||||
if (addRectsFor(it) && checked >= maxChecks) break;
|
||||
}
|
||||
}
|
||||
return rects;
|
||||
},
|
||||
|
||||
circleOverlapsObstacleRect(x, y, radius, r, padding = 0) {
|
||||
if (!r) return false;
|
||||
const rr = Math.max(0, Number(radius || 0) || 0) + Math.max(0, Number(padding || 0) || 0);
|
||||
if (r.oriented) {
|
||||
const local = rectLocalPoint(r, x, y);
|
||||
const clx = clamp(local.x, -(r.halfW || 0), r.halfW || 0);
|
||||
const cly = clamp(local.y, -(r.halfH || 0), r.halfH || 0);
|
||||
return Math.hypot(local.x - clx, local.y - cly) < rr;
|
||||
}
|
||||
const cx = clamp(x, r.left, r.right);
|
||||
const cy = clamp(y, r.top, r.bottom);
|
||||
return Math.hypot(x - cx, y - cy) < rr;
|
||||
},
|
||||
|
||||
applyPoisonBlockContactDamage(t, rect, rr) {
|
||||
if (!t || t.dead || !rect?.poisonBlock || !rect.item || rect.item.dead) return false;
|
||||
if (!this.circleOverlapsObstacleRect(t.x, t.y, rr, rect, 1.0)) return false;
|
||||
const now = this.time || 0;
|
||||
const key = rect.item.id || "poison";
|
||||
t._poisonBlockHitAt = t._poisonBlockHitAt || Object.create(null);
|
||||
const body = global.TarinaiPhysicsBodySystem.ensureBody(rect.item, this, { syncFromLegacy: false });
|
||||
const solid = body?.collision ? body.collision.solid !== false : true;
|
||||
const cooldown = solid ? 0.30 : 0.42;
|
||||
if ((t._poisonBlockHitAt[key] || -999) + cooldown > now) return false;
|
||||
t._poisonBlockHitAt[key] = now;
|
||||
const velocity = body?.velocity || {};
|
||||
const vx = Number((velocity.x ?? rect.item.vx) || 0) || 0;
|
||||
const vy = Number((velocity.y ?? rect.item.vy) || 0) || 0;
|
||||
const omega = Number((velocity.angular ?? global.TarinaiPhysicsBodySystem.scalar(rect.item, "spin", 0)) || 0) || 0;
|
||||
const speed = Math.hypot(vx, vy) + Math.abs(omega) * Math.max(28, rect.item.r || 64) * 0.35;
|
||||
const base = Math.max(1, Number((body?.hazard?.damage ?? global.TarinaiPhysicsBodySystem.scalar(rect.item, "damage", 7)) || 7) || 7);
|
||||
const damage = clamp(base * (solid ? 1.0 : 0.72) + speed * 0.018, 2.0, 18.0);
|
||||
if (typeof this.applyImpactDamage === "function") this.applyImpactDamage(t, damage, "\u6bd2\u30d6\u30ed\u30c3\u30af", { source: rect.item, x: t.x, y: t.y });
|
||||
else if (typeof t.damage === "function") t.damage(damage, "\u6bd2\u30d6\u30ed\u30c3\u30af");
|
||||
// HP\u30d0\u30fc\u306f\u63cf\u753b\u5074\u3067\u300c\u30c0\u30e1\u30fc\u30b8\u4e2d\u300d\u306e\u8868\u793a\u6761\u4ef6\u3092\u898b\u308b\u305f\u3081\u3001\u6bd2\u30d6\u30ed\u30c3\u30af\u3067\u3082\u660e\u793a\u7684\u306b\u50b7\u307f\u72b6\u614b\u3092\u7acb\u3066\u308b\u3002
|
||||
t.showHpBar?.(4.8);
|
||||
t.hurtTimer = Math.max(t.hurtTimer || 0, 0.82);
|
||||
t.pokeFlashTimer = Math.max(t.pokeFlashTimer || 0, 0.28);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24);
|
||||
if (typeof t.bubble === "function" && now >= (t.lastPoisonBlockBubbleAt || -999) + 0.85) {
|
||||
t.lastPoisonBlockBubbleAt = now;
|
||||
t.bubble("!", 0.48, "rgba(94,166,76,0.78)");
|
||||
}
|
||||
// \u7269\u7406\u30D6\u30ED\u30C3\u30AF\u885D\u7A81\u306E\u8996\u899A\u30A8\u30D5\u30A7\u30AF\u30C8\u306F\u51FA\u3055\u306A\u3044\u3002\u63A5\u89E6\u6642\u306E\u72B6\u614B\u5909\u5316\u306FHP\u30D0\u30FC\u3068\u5439\u304D\u51FA\u3057\u3060\u3051\u306B\u9650\u5B9A\u3059\u308B\u3002
|
||||
return true;
|
||||
},
|
||||
|
||||
resolvePoisonBlockContactDamage(t, rr) {
|
||||
if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) return false;
|
||||
if ((this.itemCounts?.poison_block || 0) <= 0) return false;
|
||||
|
|
@ -563,8 +544,8 @@
|
|||
if ((t._nextPoisonBlockProbeAt || 0) > now) return false;
|
||||
t._nextPoisonBlockProbeAt = now + interval;
|
||||
let hit = false;
|
||||
for (const rect of this.nearbyPoisonBlockHazardRects(t.x, t.y, rr + 150, { maxChecks: 32 })) {
|
||||
hit = this.applyPoisonBlockContactDamage(t, rect, rr) || hit;
|
||||
for (const rect of nearbyPoisonBlockHazardRectsLocal(this, t.x, t.y, rr + 150, { maxChecks: 32 })) {
|
||||
hit = applyPoisonBlockContactDamageLocal(this, t, rect, rr) || hit;
|
||||
}
|
||||
return hit;
|
||||
},
|
||||
|
|
@ -598,7 +579,7 @@
|
|||
const rr = Math.max(5, Number(target.r || target.radius || itemRadiusFor?.(target.type, 10) || 10) || 10) * (target.type === "grass_bed" ? 1.15 : 0.92);
|
||||
let touching = false;
|
||||
for (const rect of rects) {
|
||||
if (this.circleOverlapsObstacleRect(target.x, target.y, rr, rect, 1.5)) { touching = true; break; }
|
||||
if (Geometry.circleOverlapsRect(target.x, target.y, rr, rect, 1.5)) { touching = true; break; }
|
||||
}
|
||||
if (!touching) continue;
|
||||
const wasLive = !target.dead;
|
||||
|
|
@ -631,7 +612,7 @@
|
|||
let cx, cy, dx, dy, d, nx, ny;
|
||||
let insidePenetration = 0;
|
||||
if (r.oriented) {
|
||||
const local = rectLocalPoint(r, t.x, t.y);
|
||||
const local = Geometry.rectLocalPoint(r, t.x, t.y);
|
||||
const clx = clamp(local.x, -(r.halfW || 0), r.halfW || 0);
|
||||
const cly = clamp(local.y, -(r.halfH || 0), r.halfH || 0);
|
||||
dx = local.x - clx;
|
||||
|
|
@ -650,7 +631,7 @@
|
|||
else if (m === top) { dx = 0; dy = -1; d = 1; }
|
||||
else { dx = 0; dy = 1; d = 1; }
|
||||
}
|
||||
const n = rectWorldNormal(r, dx / d, dy / d);
|
||||
const n = Geometry.rectWorldNormal(r, dx / d, dy / d);
|
||||
nx = n.x; ny = n.y;
|
||||
cx = t.x - dx; cy = t.y - dy;
|
||||
} else {
|
||||
|
|
@ -852,6 +833,13 @@
|
|||
const type = rect?.item?.type || rect?.type || "";
|
||||
if (!type || type === "glass_wall") return false;
|
||||
if (type === "gate_fence" && rect?.item?.gateOpen) return false;
|
||||
// Path routing uses this predicate even though the queried solid rects are
|
||||
// not limited to fences. Nest containers have real collision footprints;
|
||||
// include them here so tarinai route around a non-target nest instead of
|
||||
// walking straight into its body. The target item is still excluded by
|
||||
// findTarinaiPathWaypoint/targetBlockedByObstacle, so sleeping in that
|
||||
// nest remains reachable through the door.
|
||||
if (type === "nest_box" || type === "pipe") return true;
|
||||
return Boolean(this.isFenceType?.(type));
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,80 @@
|
|||
const groundForResetPreset = resetPresets.groundForResetPreset || (() => "soil");
|
||||
const resetPresetLabel = resetPresets.label || (() => "\u30C7\u30D5\u30A9\u30EB\u30C8");
|
||||
|
||||
function pointerLargeTarinaiTooltipCandidatesLocal(worldRef, now = worldRef?.time || 0) {
|
||||
const cache = worldRef._pointerTooltipLargeTarinaiCache;
|
||||
if (cache && worldRef._pointerTooltipLargeTarinaiSerial === worldRef.liveIdSerial && now < (worldRef._pointerTooltipLargeTarinaiUntil || 0)) return cache;
|
||||
const out = [];
|
||||
for (const t of worldRef.tarinai || []) {
|
||||
if (!t || t.dead) continue;
|
||||
if ((Number(t.radius) || 20) > 64) out.push(t);
|
||||
}
|
||||
worldRef._pointerTooltipLargeTarinaiCache = out;
|
||||
worldRef._pointerTooltipLargeTarinaiSerial = worldRef.liveIdSerial;
|
||||
worldRef._pointerTooltipLargeTarinaiUntil = now + 0.35;
|
||||
return out;
|
||||
}
|
||||
|
||||
function pointerTarinaiTooltipCandidatesLocal(worldRef, p) {
|
||||
const all = Array.isArray(worldRef.tarinai) ? worldRef.tarinai : [];
|
||||
if (!p?.inside || !all.length) return all;
|
||||
if (all.length < 72 || typeof worldRef.nearbyTarinai !== "function") return all;
|
||||
const local = worldRef.nearbyTarinai(p.x, p.y, 128, false) || [];
|
||||
const large = pointerLargeTarinaiTooltipCandidatesLocal(worldRef, worldRef.time || 0) || [];
|
||||
if (!large.length) return local;
|
||||
const stamp = (worldRef._pointerTooltipCandidateStamp = (worldRef._pointerTooltipCandidateStamp || 0) + 1);
|
||||
for (const t of local) if (t) t._pointerTooltipCandidateStamp = stamp;
|
||||
for (const t of large) {
|
||||
if (!t || t.dead || t._pointerTooltipCandidateStamp === stamp) continue;
|
||||
t._pointerTooltipCandidateStamp = stamp;
|
||||
local.push(t);
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
function pointerTarinaiTooltipInfoLocal(worldRef) {
|
||||
const p = worldRef.pointer;
|
||||
if (!p?.inside || !Array.isArray(worldRef.tarinai)) return null;
|
||||
let best = null, bestD = Infinity;
|
||||
for (const t of pointerTarinaiTooltipCandidatesLocal(worldRef, p) || worldRef.tarinai) {
|
||||
if (!t || t.dead) continue;
|
||||
if (worldRef.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const d = distXY(p.x, p.y, t.x, t.y);
|
||||
const limit = Math.max(46, (t.radius || 20) * 1.35);
|
||||
if (d <= limit && d < bestD) { best = t; bestD = d; }
|
||||
}
|
||||
if (!best) return null;
|
||||
const nameLine = String(best.name || "たりない");
|
||||
const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "")
|
||||
|| window.TEXT_CATALOG?.stateLabel?.(best.state, best)
|
||||
|| best.thought
|
||||
|| "様子を見ている";
|
||||
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, "体感気温") || "体感気温: --℃";
|
||||
return { target: best, lines: [nameLine, String(behavior), statusLine, tempLine], kind: "tarinai" };
|
||||
}
|
||||
|
||||
function pointerOwnedBedInfoLocal(worldRef) {
|
||||
const p = worldRef.pointer;
|
||||
if (!p?.inside) return null;
|
||||
let best = null, bestD = Infinity;
|
||||
for (const it of worldRef.nearbyItems(p.x, p.y, 140)) {
|
||||
if (!it || it.dead || it.type !== "grass_bed") continue;
|
||||
const d = distXY(p.x, p.y, it.x, it.y);
|
||||
if (d < bestD && d <= Math.max(60, (it.r || 24) * 2.0)) { best = it; bestD = d; }
|
||||
}
|
||||
if (!best) return null;
|
||||
const owner = worldRef.liveTarinaiById?.(best.ownerId) || null;
|
||||
return { bed: best, owner };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
reset(seedPopulation = null, fieldType = this.fieldType || "garden", presetId = "default") {
|
||||
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
|
||||
|
|
@ -433,12 +507,25 @@
|
|||
}
|
||||
}
|
||||
const t = new Tarinai(this, opts);
|
||||
const isSeededSpawn = !Number.isFinite(Number(opts.age))
|
||||
&& opts.birthTime == null
|
||||
&& !opts.dead
|
||||
&& !(Array.isArray(opts.parents) && opts.parents.length)
|
||||
&& !(Array.isArray(opts.seedParents) && opts.seedParents.length)
|
||||
&& (Number(opts.generation ?? 1) <= 1);
|
||||
if (isSeededSpawn) {
|
||||
const span = Math.max(1, Number(t.lifeSpan || 1) || 1);
|
||||
const ratio = typeof deterministicRange === "function"
|
||||
? deterministicRange(this, "initial-life-spent-ratio", 0, 0.30, t.familyKey || t.birthSeed || t.id || this.birthSerial || Math.random())
|
||||
: Math.random() * 0.30;
|
||||
t.age = Math.max(Number(t.age || 0) || 0, span * ratio);
|
||||
t.birthTime = (this.time || 0) - t.age;
|
||||
}
|
||||
this.assignTarinaiLiveId(t);
|
||||
this.tarinai.push(t);
|
||||
this.drawListDirty = true;
|
||||
if (t.hasPaired || (t.parents || []).length || (t.children || []).length) this.recordFamily(t);
|
||||
this.maxGeneration = Math.max(this.maxGeneration, t.generation);
|
||||
this.emit?.("tarinai:born", { tarinai: t, source: opts.parents?.length ? "family" : "spawn" });
|
||||
return t;
|
||||
},
|
||||
|
||||
|
|
@ -451,7 +538,7 @@
|
|||
if (!p?.inside) return null;
|
||||
let best = null, bestD = Infinity;
|
||||
for (const it of this.nearbyItems(p.x, p.y, 140)) {
|
||||
if (!it || it.dead || !(typeof globalThis.isNestContainerType === "function" ? globalThis.isNestContainerType(it.type) : (it.type === "nest_box" || it.type === "pipe"))) continue;
|
||||
if (!it || it.dead || !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(it)) continue;
|
||||
const d = distXY(p.x, p.y, it.x, it.y);
|
||||
if (d < bestD && d <= Math.max(80, it.r * 2.1)) { best = it; bestD = d; }
|
||||
}
|
||||
|
|
@ -462,7 +549,7 @@
|
|||
},
|
||||
|
||||
nestBoxTooltipLines(box) {
|
||||
if (!box || box.dead || !(typeof globalThis.isNestContainerType === "function" ? globalThis.isNestContainerType(box.type) : (box.type === "nest_box" || box.type === "pipe"))) return [];
|
||||
if (!box || box.dead || !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(box)) return [];
|
||||
const isPipe = box.type === "pipe";
|
||||
let occupants = [];
|
||||
if (isPipe) {
|
||||
|
|
@ -500,65 +587,25 @@
|
|||
|
||||
temperatureTooltipLineFor(target, label = "体感気温") {
|
||||
if (!target || !this.temperatureAt) return "";
|
||||
const temp = target.type === "pipe" && this.pipeInteriorTemperatureFor
|
||||
? this.pipeInteriorTemperatureFor(target)
|
||||
: this.temperatureAt(target.x, target.y);
|
||||
const isActor = Array.isArray(this.tarinai) && this.tarinai.includes(target);
|
||||
const temp = isActor
|
||||
? (this.feltTemperatureFor?.(target) ?? this.temperatureAt(target.x, target.y, target))
|
||||
: (target.type === "pipe" && this.pipeInteriorTemperatureFor
|
||||
? this.pipeInteriorTemperatureFor(target)
|
||||
: this.temperatureAt(target.x, target.y));
|
||||
if (!Number.isFinite(Number(temp))) return "";
|
||||
const status = this.temperatureStatusFor?.(temp) || null;
|
||||
const status = this.temperatureStatusFor?.(temp, isActor ? target : null) || null;
|
||||
const suffix = status?.label ? `(${status.label})` : "";
|
||||
return `${label}: ${Number(temp).toFixed(1)}℃${suffix}`;
|
||||
},
|
||||
|
||||
pointerTarinaiTooltipInfo() {
|
||||
const p = this.pointer;
|
||||
if (!p?.inside || !Array.isArray(this.tarinai)) return null;
|
||||
let best = null, bestD = Infinity;
|
||||
for (const t of this.tarinai) {
|
||||
if (!t || t.dead) continue;
|
||||
if (this.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const d = distXY(p.x, p.y, t.x, t.y);
|
||||
const limit = Math.max(46, (t.radius || 20) * 1.35);
|
||||
if (d <= limit && d < bestD) { best = t; bestD = d; }
|
||||
}
|
||||
if (!best) return null;
|
||||
const nameLine = String(best.name || "たりない");
|
||||
const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "")
|
||||
|| window.TEXT_CATALOG?.stateLabel?.(best.state, best)
|
||||
|| best.thought
|
||||
|| "様子を見ている";
|
||||
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 = this.temperatureTooltipLineFor(best, "体感気温") || "体感気温: --℃";
|
||||
return { target: best, lines: [nameLine, String(behavior), statusLine, tempLine], kind: "tarinai" };
|
||||
},
|
||||
|
||||
pointerOwnedBedInfo() {
|
||||
const p = this.pointer;
|
||||
if (!p?.inside) return null;
|
||||
let best = null, bestD = Infinity;
|
||||
for (const it of this.nearbyItems(p.x, p.y, 140)) {
|
||||
if (!it || it.dead || it.type !== "grass_bed") continue;
|
||||
const d = distXY(p.x, p.y, it.x, it.y);
|
||||
if (d < bestD && d <= Math.max(60, (it.r || 24) * 2.0)) { best = it; bestD = d; }
|
||||
}
|
||||
if (!best) return null;
|
||||
const owner = this.liveTarinaiById?.(best.ownerId) || null;
|
||||
return { bed: best, owner };
|
||||
},
|
||||
|
||||
ownedBedTooltipLines(bed) {
|
||||
if (!bed || bed.dead || bed.type !== "grass_bed") return [];
|
||||
const owner = this.liveTarinaiById?.(bed.ownerId) || null;
|
||||
return ["\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9", `\u6301\u3061\u4e3b: ${owner?.name || "\u4e0d\u660e"}`, this.temperatureTooltipLineFor?.(bed, "\u5468\u8FBA\u6C17\u6E29")].filter(Boolean);
|
||||
},
|
||||
|
||||
pointerItemTooltipInfo() {
|
||||
const tarinaiInfo = this.pointerTarinaiTooltipInfo?.();
|
||||
const tarinaiInfo = pointerTarinaiTooltipInfoLocal(this);
|
||||
if (tarinaiInfo?.target) return tarinaiInfo;
|
||||
const nest = this.pointerNestBoxInfo?.();
|
||||
if (nest?.box) return { target: nest.box, lines: this.nestBoxTooltipLines(nest.box), kind: nest.box?.type === "pipe" ? "pipe" : "nest_box" };
|
||||
const bedInfo = this.pointerOwnedBedInfo?.();
|
||||
if (bedInfo?.bed) return { target: bedInfo.bed, lines: this.ownedBedTooltipLines(bedInfo.bed), kind: "grass_bed" };
|
||||
const bedInfo = pointerOwnedBedInfoLocal(this);
|
||||
if (bedInfo?.bed) return { target: bedInfo.bed, lines: ownedBedTooltipLinesLocal(this, bedInfo.bed), kind: "grass_bed" };
|
||||
return null;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@
|
|||
const targetItem = opts?.targetItem || target?.hostItem || (target?.isStructure || target?.type ? target : null);
|
||||
const exclude = opts?.exclude || targetItem || null;
|
||||
const rr = Math.max(8, Number(actor.radius || 20) || 20);
|
||||
const padding = Math.max(16, Number(opts.padding || 0) || rr * 0.78);
|
||||
const requestedPadding = Number(opts.padding || 0) || rr * 0.78;
|
||||
const padding = Math.max(20, requestedPadding, rr * 0.95);
|
||||
const pointClearance = Math.max(10, padding * 0.72);
|
||||
const directClear = !this.pathBlockedByFence?.(ax, ay, tx, ty, padding, { exclude });
|
||||
if (directClear) {
|
||||
actor._pathWaypoint = null;
|
||||
|
|
@ -27,7 +29,7 @@
|
|||
const cached = actor._pathWaypoint || null;
|
||||
if (cached && cached.targetId === targetId && (this.time || 0) < (cached.until || 0)
|
||||
&& Number.isFinite(cached.x) && Number.isFinite(cached.y)
|
||||
&& !this.pointBlockedByObstacle(cached.x, cached.y, Math.max(6, padding * 0.45), { exclude, maxChecks: 18 })
|
||||
&& !this.pointBlockedByObstacle(cached.x, cached.y, pointClearance, { exclude, maxChecks: 18 })
|
||||
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
|
||||
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId };
|
||||
}
|
||||
|
|
@ -87,7 +89,7 @@
|
|||
let bestScore = Infinity;
|
||||
const old = cached && Number.isFinite(cached.x) && Number.isFinite(cached.y) ? cached : null;
|
||||
for (const c of candidates) {
|
||||
if (this.pointBlockedByObstacle(c.x, c.y, Math.max(5, padding * 0.42), { exclude, maxChecks: 20 })) continue;
|
||||
if (this.pointBlockedByObstacle(c.x, c.y, pointClearance, { exclude, maxChecks: 20 })) continue;
|
||||
const firstBlocked = this.pathBlockedByFence?.(ax, ay, c.x, c.y, padding, { exclude });
|
||||
if (firstBlocked) continue;
|
||||
const secondBlocked = this.pathBlockedByFence?.(c.x, c.y, tx, ty, padding, { exclude });
|
||||
|
|
@ -111,11 +113,14 @@
|
|||
const ax = Number(actor.x), ay = Number(actor.y), tx = Number(target.x), ty = Number(target.y);
|
||||
const targetId = opts.targetId || opts.targetItem?.id || target?.id || `pos:${Math.round(tx)},${Math.round(ty)}`;
|
||||
const cached = actor._gridPathWaypoint || null;
|
||||
const padding = Math.max(16, Number(opts.padding || 0) || (Number(actor.radius || 20) * 0.78));
|
||||
const actorRadius = Math.max(8, Number(actor.radius || 20) || 20);
|
||||
const requestedPadding = Number(opts.padding || 0) || actorRadius * 0.78;
|
||||
const padding = Math.max(20, requestedPadding, actorRadius * 0.95);
|
||||
const pointClearance = Math.max(10, padding * 0.72);
|
||||
const exclude = opts.exclude || opts.targetItem || target || null;
|
||||
if (cached && cached.targetId === targetId && (this.time || 0) < (cached.until || 0)
|
||||
&& Number.isFinite(cached.x) && Number.isFinite(cached.y)
|
||||
&& !this.pointBlockedByObstacle?.(cached.x, cached.y, Math.max(6, padding * 0.42), { exclude, maxChecks: 20 })
|
||||
&& !this.pointBlockedByObstacle?.(cached.x, cached.y, pointClearance, { exclude, maxChecks: 20 })
|
||||
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
|
||||
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId, gridPath: true };
|
||||
}
|
||||
|
|
@ -141,7 +146,7 @@
|
|||
if (!inBounds(ix, iy)) return false;
|
||||
if (ix === start.ix && iy === start.iy) return true;
|
||||
if (ix === goal.ix && iy === goal.iy) return true;
|
||||
return !this.pointBlockedByObstacle?.(px(ix), py(iy), Math.max(6, padding * 0.45), { exclude, maxChecks: 18 });
|
||||
return !this.pointBlockedByObstacle?.(px(ix), py(iy), pointClearance, { exclude, maxChecks: 18 });
|
||||
};
|
||||
const edgeOpen = (a, b) => !this.pathBlockedByFence?.(px(a.ix), py(a.iy), px(b.ix), py(b.iy), padding, { exclude });
|
||||
const h = (ix, iy) => Math.hypot(px(ix) - tx, py(iy) - ty);
|
||||
|
|
@ -158,8 +163,14 @@
|
|||
const nx = cur.ix + dx, ny = cur.iy + dy;
|
||||
if (!pointOpen(nx, ny)) continue;
|
||||
const nb = { ix: nx, iy: ny };
|
||||
if (!edgeOpen(cur, nb)) continue;
|
||||
const diagonal = dx && dy;
|
||||
if (diagonal) {
|
||||
const sideA = { ix: cur.ix + dx, iy: cur.iy };
|
||||
const sideB = { ix: cur.ix, iy: cur.iy + dy };
|
||||
if (!pointOpen(sideA.ix, sideA.iy) || !pointOpen(sideB.ix, sideB.iy)) continue;
|
||||
if (!edgeOpen(cur, sideA) || !edgeOpen(cur, sideB) || !edgeOpen(sideA, nb) || !edgeOpen(sideB, nb)) continue;
|
||||
}
|
||||
if (!edgeOpen(cur, nb)) continue;
|
||||
const ng = cur.g + (diagonal ? 1.42 : 1) * step;
|
||||
const k = key(nx, ny);
|
||||
const oldNode = bestByKey.get(k);
|
||||
|
|
|
|||
|
|
@ -611,7 +611,7 @@
|
|||
const pb = physicsApi();
|
||||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(it, 0), worldRef);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(it, 1), worldRef);
|
||||
const d = runtime?.pointSegmentDistance?.(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y) ?? Infinity;
|
||||
const d = global.TarinaiGeometry.pointSegmentDistance(x, y, a?.x ?? it.x, a?.y ?? it.y, b?.x ?? it.x, b?.y ?? it.y);
|
||||
const tolerance = worldRef?.screenSizeToWorld ? worldRef.screenSizeToWorld(12) : 12;
|
||||
if (d <= tolerance && !blockedReason) blockedReason = "link";
|
||||
continue;
|
||||
|
|
@ -908,7 +908,7 @@
|
|||
const a = runtime?.endpointWorld?.(pb?.endpoint?.(placedItem, 0), worldRef);
|
||||
const b = runtime?.endpointWorld?.(pb?.endpoint?.(placedItem, 1), worldRef);
|
||||
if (a && b) {
|
||||
const d = runtime?.pointSegmentDistance?.(softItem.x, softItem.y, a.x, a.y, b.x, b.y);
|
||||
const d = global.TarinaiGeometry.pointSegmentDistance(softItem.x, softItem.y, a.x, a.y, b.x, b.y);
|
||||
return Number.isFinite(d) && d <= r + (placedItem.type === "rod" ? 9 : 7);
|
||||
}
|
||||
}
|
||||
|
|
@ -1045,7 +1045,7 @@
|
|||
const otherIsGate = it.type === "gate_fence";
|
||||
const margin = (item.type === "gate_fence" || otherIsGate) ? 1.5 : baseMargin;
|
||||
for (const r of this.solidObstacleRects(it)) {
|
||||
if (global.TarinaiCollisionFootprints.rectsOverlap(rect, r, margin)) return true;
|
||||
if (global.TarinaiGeometry.rectsOverlap(rect, r, margin)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ const RESET_PRESETS = Object.freeze({
|
|||
return Object.prototype.hasOwnProperty.call(RESET_PRESETS, id) ? id : "default";
|
||||
}
|
||||
|
||||
global.TarinaiResetPresets = Object.freeze({
|
||||
ids: Object.freeze(Object.keys(RESET_PRESETS)),
|
||||
definitions: RESET_PRESETS,
|
||||
normalize: normalizeResetPresetId,
|
||||
});
|
||||
|
||||
function presetScaleForField(field) {
|
||||
const id = field?.id || "garden";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} };
|
||||
this.nearbyQueryStatsThisFrame = { obstacleRectHits: 0, obstacleRectMisses: 0, obstacleRectBypass: 0, obstacleRectBuilt: 0 };
|
||||
this.constraintDirtyStatsThisFrame = { marked: 0, coalesced: 0 };
|
||||
this._solidObstacleRectQueryCache = new Map();
|
||||
if (!this._solidObstacleRectQueryCache) this._solidObstacleRectQueryCache = new Map();
|
||||
},
|
||||
|
||||
spendScheduledWork(kind = "ai", urgent = false) {
|
||||
|
|
@ -98,6 +98,7 @@
|
|||
bucketTypes: this.itemTypeBuckets?.size || 0,
|
||||
bucketsDirty: Boolean(this.itemBucketsDirty),
|
||||
idMapSize: this.itemIdMap?.size || 0,
|
||||
ownerBuckets: this.itemOwnerBuckets?.size || 0,
|
||||
bucketRebuildsTotal: this.itemBucketRebuildsTotal || 0,
|
||||
},
|
||||
behavior: {
|
||||
|
|
@ -278,7 +279,7 @@
|
|||
},
|
||||
|
||||
ensureItemBuckets(reason = "read") {
|
||||
if (!this.itemTypeBuckets || !this.itemIdMap || this.itemBucketsDirty) {
|
||||
if (!this.itemTypeBuckets || !this.itemIdMap || !this.itemOwnerBuckets || this.itemBucketsDirty) {
|
||||
this.updateItemCounts(reason);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -312,17 +313,33 @@
|
|||
const bucket = this.itemTypeBuckets?.get?.(type);
|
||||
return bucket || [];
|
||||
},
|
||||
|
||||
ownedItemsFor(ownerId) {
|
||||
if (ownerId == null || ownerId === "") return [];
|
||||
this.ensureItemBuckets?.(`owned-items:${ownerId}`);
|
||||
return this.itemOwnerBuckets?.get?.(ownerId) || [];
|
||||
},
|
||||
updateItemCounts(reason = "manual") {
|
||||
const counts = this.itemCounts;
|
||||
if (!this.itemTypeBuckets) this.itemTypeBuckets = new Map();
|
||||
if (!this.itemIdMap) this.itemIdMap = new Map();
|
||||
if (!this.itemOwnerBuckets) this.itemOwnerBuckets = new Map();
|
||||
for (const key of Object.keys(counts)) counts[key] = 0;
|
||||
this.itemTypeBuckets.clear();
|
||||
this.itemIdMap.clear();
|
||||
this.itemOwnerBuckets.clear();
|
||||
for (const it of this.items) {
|
||||
if (!it || it.dead) continue;
|
||||
counts[it.type] = (counts[it.type] || 0) + 1;
|
||||
if (it.id != null) this.itemIdMap.set(it.id, it);
|
||||
if (it.ownerId) {
|
||||
let ownerBucket = this.itemOwnerBuckets.get(it.ownerId);
|
||||
if (!ownerBucket) {
|
||||
ownerBucket = [];
|
||||
this.itemOwnerBuckets.set(it.ownerId, ownerBucket);
|
||||
}
|
||||
ownerBucket.push(it);
|
||||
}
|
||||
let bucket = this.itemTypeBuckets.get(it.type);
|
||||
if (!bucket) {
|
||||
bucket = [];
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@
|
|||
const wave = Math.sin(phase * Math.PI * 2 - Math.PI * 0.25);
|
||||
return wave * range * 0.5;
|
||||
},
|
||||
seasonTemperatureBias() {
|
||||
return this.seasonIndex?.() === 1 ? (CONFIG.summerTemperatureBoost ?? 5) : 0;
|
||||
},
|
||||
dailyTemperatureWave(progress = 0) {
|
||||
const hour = ((((Number(progress) || 0) % 1) + 1) % 1) * 24;
|
||||
const minHour = 5.0; // \u671D\u65B9\u306B\u6700\u4F4E\u6C17\u6E29
|
||||
|
|
@ -86,6 +89,7 @@
|
|||
} else {
|
||||
temp = (CONFIG.standardTemperature ?? 15) + this.annualTemperatureOffset() + dailyWave * dailyRange * 0.5;
|
||||
}
|
||||
temp += this.seasonTemperatureBias?.() ?? 0;
|
||||
this.temperatureDailyRange = dailyRange;
|
||||
return this.climateTemperature(temp);
|
||||
},
|
||||
|
|
@ -193,6 +197,24 @@
|
|||
label: discomfort <= 0 ? "\u5FEB\u9069" : (direction === "cold" ? (harmExcess > 0 ? "\u5371\u967A\u306A\u5BD2\u3055" : "\u5BD2\u3044") : (harmExcess > 0 ? "\u5371\u967A\u306A\u6691\u3055" : "\u6691\u3044")),
|
||||
};
|
||||
},
|
||||
temperatureComfortCandidateItems(direction = "") {
|
||||
const bucketed = typeof this.itemsOfType === "function";
|
||||
if (!bucketed) return this.items || [];
|
||||
const types = ["nest_box", "pipe"];
|
||||
if (direction === "cold") types.push("stove");
|
||||
else if (direction === "hot") types.push("dry_ice", "fan");
|
||||
else types.push("stove", "dry_ice", "fan");
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const type of types) {
|
||||
for (const it of this.itemsOfType(type) || []) {
|
||||
if (!it || it.dead || seen.has(it)) continue;
|
||||
seen.add(it);
|
||||
out.push(it);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
findComfortTemperatureSpot(actor, opts = {}) {
|
||||
if (!actor || actor.dead) return null;
|
||||
const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt(actor.x, actor.y, actor);
|
||||
|
|
@ -218,7 +240,7 @@
|
|||
}
|
||||
}
|
||||
const itemRange = Math.max(24, CONFIG.temperatureItemRange ?? 190);
|
||||
for (const it of this.items || []) {
|
||||
for (const it of this.temperatureComfortCandidateItems?.(currentStatus.direction) || this.items || []) {
|
||||
if (!it || it.dead) continue;
|
||||
if (it.type === "nest_box" || it.type === "pipe") {
|
||||
const shelterTemp = it.type === "pipe" && this.pipeInteriorTemperatureFor
|
||||
|
|
@ -281,12 +303,19 @@
|
|||
const current = this.feltTemperatureFor?.(actor) ?? this.temperatureAt?.(actor.x, actor.y, actor);
|
||||
const currentStatus = this.temperatureStatusFor?.(current, actor);
|
||||
if (!currentStatus || currentStatus.comfortable) return false;
|
||||
if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false;
|
||||
const temp = item.type === "pipe" && this.pipeInteriorTemperatureFor
|
||||
? this.pipeInteriorTemperatureFor(item, actor)
|
||||
: this.temperatureAt?.(item.x, item.y, actor);
|
||||
const status = this.temperatureStatusFor?.(temp, actor);
|
||||
return Boolean(status && !status.comfortable);
|
||||
if (!status || status.comfortable) return false;
|
||||
// A nest/pipe is still a valid night shelter when it improves a bad
|
||||
// temperature, even if it cannot make the interior fully comfortable.
|
||||
if (item.type === "nest_box" || item.type === "pipe") {
|
||||
if (currentStatus.direction === "cold" && temp > current + 0.45) return false;
|
||||
if (currentStatus.direction === "hot" && temp < current - 0.45) return false;
|
||||
}
|
||||
if (!this.hasReachableComfortTemperatureSpot?.(actor)) return false;
|
||||
return true;
|
||||
},
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue