diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index d5ff5e7..9b49603 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -1712,9 +1712,9 @@ export class BabylonScene { const csm = new CascadedShadowGenerator(size, this._sunLight); csm.numCascades = numCascades; csm.stabilizeCascades = true; - csm.lambda = 0.8; - csm.cascadeBlendPercentage = 0.07; - csm.shadowMaxZ = (q === 'high') ? 200 : 120; + csm.lambda = 0.6; // меньше — каскады равномернее, нет вытянутого дальнего + csm.cascadeBlendPercentage = 0.1; + csm.shadowMaxZ = (q === 'high') ? 90 : 60; // тени только вблизи (убирает «полосу через всю карту») csm.bias = PCF_BIAS; csm.normalBias = PCF_NORMAL_BIAS; csm.usePercentageCloserFiltering = true; @@ -1722,10 +1722,10 @@ export class BabylonScene { ? ShadowGenerator.QUALITY_HIGH : ShadowGenerator.QUALITY_MEDIUM; csm.darkness = 0.4; - csm.autoCalcDepthBounds = true; - // Плавное затухание тени у края каскада — убирает «полосу-хвост» - // тени персонажа на весь пол при движении (баг 2026-06-05). - csm.frustumEdgeFalloff = 8; + // autoCalcDepthBounds растягивал дальний каскад → длинная тонкая + // тень-полоса персонажа на весь пол. Выключаем + фикс. дальность. + csm.autoCalcDepthBounds = false; + csm.frustumEdgeFalloff = 12; this._shadowGenerator = csm; } else { // Обычный ShadowGenerator. Поднял разрешение для soft до 2048. diff --git a/src/editor/engine/GameplayKits.js b/src/editor/engine/GameplayKits.js index f6d8cee..f35dd8a 100644 --- a/src/editor/engine/GameplayKits.js +++ b/src/editor/engine/GameplayKits.js @@ -631,10 +631,17 @@ game.gui.onSubmit('codein', (text) => { if (String(text).trim() === CODE){ opened = true; target = Math.PI/2; // плавно распахнуть game.gui.remove('codein'); - game.ui.set('codehint','✓ Открыто!', {x:50,y:78,anchor:'bottom',color:'#36d57a',size:18}); - game.after(2, () => game.ui.set('codehint','',{})); + game.ui.set('codehint','✓ Открыто! Нажми E чтобы закрыть.', {x:50,y:78,anchor:'bottom',color:'#36d57a',size:16}); + game.after(2.5, () => game.ui.set('codehint','',{})); } else game.ui.set('codehint','✗ Неверный код', {x:50,y:78,anchor:'bottom',color:'#ff5555',size:18}); -});` }], +}); +// Закрыть открытую дверь по E → снова можно вводить код. +game.self.onInteract(() => { + if (!opened) return; + opened = false; near = false; target = 0; // плавно закрыть + game.ui.set('codehint','🔒 Дверь закрыта.', {x:50,y:78,anchor:'bottom',color:'#fff',size:16}); + game.after(2, () => game.ui.set('codehint','',{})); +}, { text: 'Закрыть дверь', key: 'e', distance: 4 });` }], }, { id: 'name-label', @@ -706,16 +713,15 @@ if (enemy && enemy.follow) enemy.follow('player');` }], }, { id: 'npc-trader', - name: 'Торговец (диалог)', - desc: 'Фигура торговца: подойди, нажми E — открывается диалог. (Вики: «Торговец»)', + name: 'Торговец (NPC)', + desc: 'NPC-персонаж торговец: подойди, нажми E — открывается диалог. (Вики: «Торговец»)', icon: 'trader', category: 'npc', - prims: [ - { type: 'cylinder', x: 0, y: 1, z: 0, sx: 1.2, sy: 2, sz: 1.2, color: '#3a6ea5', material: 'matte', name: 'Торговец' }, - { type: 'sphere', x: 0, y: 2.3, z: 0, sx: 0.9, sy: 0.9, sz: 0.9, color: '#e8c8a0', material: 'matte', canCollide: false, name: 'Голова торговца' }, - ], + // Невидимый prim-триггер держит onInteract; рядом спавнится NPC-персонаж. + prims: [{ type: 'cube', x: 0, y: 1.5, z: 0, sx: 2, sy: 3, sz: 2, color: '#3a6ea5', material: 'matte', visible: false, canCollide: false, name: 'Зона торговца' }], scripts: [{ attachTo: 'on-target', code: -`// Торговец с диалогом по E. -game.self.setLabel('Торговец Боб', { color:'#fff', bg:'#3a6ea5' }); +`// Торговец — настоящий NPC-персонаж. Триггер (этот объект) держит диалог по E. +const p = game.self.position; +const npc = game.scene.spawnNpc('character-a', { x: p.x, z: p.z, name: 'Торговец Боб' }); game.self.onInteract(() => { game.modal.dialog('Торговец Боб', [ 'Привет, путник! Заходи за товарами.', diff --git a/src/editor/engine/NpcManager.js b/src/editor/engine/NpcManager.js index 491e8d6..773d3c9 100644 --- a/src/editor/engine/NpcManager.js +++ b/src/editor/engine/NpcManager.js @@ -390,13 +390,19 @@ export class NpcManager { if (root._isWorldMatrixFrozen) { try { root.unfreezeWorldMatrix(); } catch (e) {} } - root.position.set(npc.x, npc.y, npc.z); + // Анимация ходьбы — процедурное покачивание (у Kenney-моделей нет + // скелета). Подпрыгивание по Y + лёгкое раскачивание корпуса. + if (moving) npc.walkPhase += dt * 10; + let bobY = 0, lean = 0; + if (moving && !npc.r15Animator) { + bobY = Math.abs(Math.sin(npc.walkPhase)) * 0.12; // шаги вверх-вниз + lean = Math.sin(npc.walkPhase) * 0.08; // покачивание + } + root.position.set(npc.x, npc.y + bobY, npc.z); root.rotation.y = npc.yaw; + root.rotation.z = lean; // data.x/y/z — чтобы scene.find/getPosition видели NPC. data.x = npc.x; data.y = npc.y; data.z = npc.z; - - // Анимация ходьбы — простое покачивание (без R15-скелета у Kenney). - if (moving) npc.walkPhase += dt * 6; // R15-NPC (skin_*): процедурная анимация бега/покоя через R15Animator. if (npc.r15Animator) { try {