diff --git a/v2/apps/game/src/main.ts b/v2/apps/game/src/main.ts index 478d330..4999737 100644 --- a/v2/apps/game/src/main.ts +++ b/v2/apps/game/src/main.ts @@ -122,14 +122,29 @@ // ---- Слой тайлей мира: общий атлас реестра worldTex ---- // Слоты тайлей моделей локальны; при штамповке opts.texRemap (areas.ts) // переводит их в глобальные id реестра. Материалы чанков сэмплируют ОДИН -// общий атлас (перестраивается после регистраций в модуле мира), его -// texture живёт вне материалов — диспозится только здесь, при пересборке. +// общий атлас; реестр наполняется лениво (штамповка новой области может +// зарегистрировать новые тайли), поэтому атлас пересобирается при каждом +// росте реестра — старая текстура диспозится ТОЛЬКО после переклейки +// нового атласа на живые меши (remeshDirty → currentWorldAtlas). const worldTiles = registryTiles; let worldAtlas: WorldTileAtlas | null = null; +let atlasTiles = -1; // сколько тайлей было в реестре на момент сборки атласа /** Актуальный атлас реестра (null, пока тайлей нет — материалы плоские). */ function currentWorldAtlas(): WorldTileAtlas | null { - if (!worldAtlas) worldAtlas = worldTileAtlas(worldTiles); + const n = Object.keys(worldTiles).length; + if (atlasTiles === n) return worldAtlas; // кэш валиден (включая пустой) + const rebuilt = worldTileAtlas(worldTiles); + if (rebuilt) { + // живые меши сэмплируют старую текстуру — переклеиваем, прежде чем + // её диспозить (иначе недоремешенные чанки рисуют мусор) + for (const m of chunkMeshes.values()) { + rebuilt.applyTo(m.material as THREE.MeshLambertMaterial); + } + } + worldAtlas?.dispose(); + atlasTiles = n; + worldAtlas = rebuilt; return worldAtlas; } @@ -701,6 +716,8 @@ /** Нажатие «E»: диалог → NPC → click-переход → интерактив → цветок (v1). */ function interact(): void { if (bagOpen) return; // «E» под сумкой не действует (мир замер) + if (menuOpen) return; // и под меню: nearPick протух (тик заморожен) — + // случайное «E» у двери стартовало бы переход под панелью if (runner.active) { if (!runner.waitingForChoice) runner.advance(); updateHud(); @@ -1083,7 +1100,9 @@ weather.update(elapsed); // дождь/снег поверх амбиента области lightning.update(elapsed); // вспышка молнии (если бьёт) postFx?.update(lighting?.nightFactor ?? 0, elapsed, ashEnabled); // виньетка/жара - fauna?.update(dt, elapsed); // олени бродят (в фейде/катсцене мир замер) + fauna?.update(dt, elapsed); // олени живут и в фейде/катсцене/диалоге — + // фон за «замершим» миром остаётся живым (гейты с кадр-равенством глушат + // fauna сами через setFauna(false)) if (fade.active) { fade.t += dt; if (fade.phase === 'out') { @@ -1394,7 +1413,11 @@ rebuildAmbient(); // вкл: пересобрать частицы; выкл: null (тик их не рисует) // гейт-режим «без атмосферы» глушит и осадки с облачностью weather.set(on ? activeWeatherSpec() : {}); - if (!on) weatherManual = null; + if (!on) { + lightning.cancel(); // хвост вспышки догорать некому — гасим сразу + weatherManual = null; + dev.refreshWeather(); // слайдеры показывают именно состояние + } }, /** Фауна вкл/выкл (гейт: кадр-равенство — олени бродят в реальном времени). */ setFauna(on: boolean): void { diff --git a/v2/packages/engine/src/render/__tests__/lightning.test.ts b/v2/packages/engine/src/render/__tests__/lightning.test.ts index 96d51b0..04142f2 100644 --- a/v2/packages/engine/src/render/__tests__/lightning.test.ts +++ b/v2/packages/engine/src/render/__tests__/lightning.test.ts @@ -118,4 +118,13 @@ expect(s.flashes.at(-1)).toEqual({ at: null, k: 0 }); // свет погашен expect(sys.info()).toMatchObject({ active: false, flash: 0 }); }); + it('cancel гасит вспышку немедленно; неактивной системе безвреден', () => { + const s = stub(); + const sys = new LightningSystem(s.renderer); + sys.cancel(); // безвреден без удара + sys.strike([5, 0, 5]); + sys.cancel(); + expect(sys.info()).toMatchObject({ active: false, flash: 0 }); + expect(s.flashes.at(-1)).toEqual({ at: null, k: 0 }); + }); }); \ No newline at end of file diff --git a/v2/packages/engine/src/render/lightning.ts b/v2/packages/engine/src/render/lightning.ts index c2a801d..ecc57bf 100644 --- a/v2/packages/engine/src/render/lightning.ts +++ b/v2/packages/engine/src/render/lightning.ts @@ -181,6 +181,15 @@ }; } + /** Погасить активную вспышку немедленно (переходы/гейты: мир замер — + * огибающей больше некогда догорать до нуля). Неактивной безвредна. */ + cancel(): void { + this.strikeT = -1; + this.flashK = 0; + this.layer.update([]); + this.renderer.setLightning(null, 0); + } + dispose(): void { // гасим всё, что могло остаться от незавершённой вспышки: слой и // свет рендера иначе замирают навсегда (update уже не догонит)