diff --git a/src/editor/KubikonEditor.jsx b/src/editor/KubikonEditor.jsx index 3717b0b..75908fc 100644 --- a/src/editor/KubikonEditor.jsx +++ b/src/editor/KubikonEditor.jsx @@ -3367,13 +3367,17 @@ const KubikonEditor = () => { }} onDeleteModel={(id) => { sceneRef.current?.modelManager?.removeInstance(id); + sceneRef.current?._cleanupOrphanScripts?.(); sceneRef.current?.clearSelection(); + setScriptsList(sceneRef.current?.getScripts?.() || []); markDirty(); hierarchyDirtyRef.current = true; }} onDeletePrimitive={(id) => { sceneRef.current?.primitiveManager?.removeInstance(id); + sceneRef.current?._cleanupOrphanScripts?.(); sceneRef.current?.clearSelection(); + setScriptsList(sceneRef.current?.getScripts?.() || []); markDirty(); hierarchyDirtyRef.current = true; }} diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index 9b49603..f2c90c8 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -1701,8 +1701,10 @@ export class BabylonScene { // "уехавшая" тень на скрине пользователя // 2026-05-27. 0.005 — золотая середина для // кубов 1м с прямыми гранями. - const PCF_BIAS = 0.0005; - const PCF_NORMAL_BIAS = 0.005; + const PCF_BIAS = 0.0008; + // normalBias повышен 0.005→0.02: убирает «полосы»-acne на полу, которые + // появлялись от теней соседних объектов (на пустой сцене их не было). + const PCF_NORMAL_BIAS = 0.02; if (!this._shadowGenerator) { if (wantCsm) { @@ -6183,6 +6185,9 @@ export class BabylonScene { if (this._onScriptCrosshair) this.gameRuntime.setOnCrosshairChange(this._onScriptCrosshair); // eslint-disable-next-line no-console console.log('[BabylonScene] enterPlayMode — scripts to run:', this._scripts?.length || 0, this._scripts); + // Перед стартом чистим скрипты-сироты (их объект-носитель удалён) — + // иначе on-target скрипт удалённого объекта продолжал исполняться. Баг 2026-06-05. + this._cleanupOrphanScripts?.(); // Старт через requestAnimationFrame — даём Babylon собрать сцену requestAnimationFrame(() => { if (this._isPlaying) this.gameRuntime?.start(this._scripts || []); diff --git a/src/editor/engine/GameplayKits.js b/src/editor/engine/GameplayKits.js index 7d38b59..4736879 100644 --- a/src/editor/engine/GameplayKits.js +++ b/src/editor/engine/GameplayKits.js @@ -760,18 +760,22 @@ game.self.onClick(() => { { id: 'enemy-hp', name: 'Враг с HP', - desc: 'Враг с полоской здоровья над головой. Кликай — урон, при нуле HP погибает. (Вики: «Имена над врагами», «босс»)', + desc: 'Враг-персонаж: преследует игрока, бьёт при касании. Над головой — полоска здоровья. (Вики: «босс», «имена над врагами»)', icon: 'boss', category: 'npc', - prims: [{ type: 'cube', x: 0, y: 1.5, z: 0, sx: 1.5, sy: 3, sz: 1.5, color: '#7a2030', material: 'matte', name: 'Враг' }], + // Невидимый триггер-якорь; рядом спавнится NPC-враг. + prims: [{ type: 'cube', x: 0, y: 1, z: 0, sx: 1, sy: 2, sz: 1, color: '#7a2030', material: 'matte', visible: false, canCollide: false, name: 'Якорь врага' }], scripts: [{ attachTo: 'on-target', code: -`// Враг с HP: клик → -20 HP, метка обновляется, при 0 — исчезает. -let hp = 100; -function lbl(){ game.self.setLabel('Враг ❤ ' + hp, { color:'#fff', bg:'#7a2030' }); } -lbl(); -game.self.onClick(() => { - hp -= 20; if (hp <= 0) { game.self.setVisible(false); game.broadcast('score', { add: 50 }); - game.ui.set('kill', '💀 Враг повержен! +50', { x:50, y:75, anchor:'bottom', color:'#ffd23a', size:18 }); return; } - lbl(); +`// Враг-персонаж: спавним NPC, он преследует игрока и бьёт при касании. +const p = game.self.position; +const enemy = game.scene.spawnNpc('skin_retro-zombie', { x: p.x, z: p.z, name: 'Враг', hp: 100, speed: 3 }); +if (enemy && enemy.follow) enemy.follow('player'); +let cd = 0; +game.onTick((dt) => { + if (!enemy || !enemy.position) return; + cd -= dt; + const pl = game.player.position, e = enemy.position; + const d = Math.sqrt((pl.x-e.x)**2 + (pl.z-e.z)**2); + if (d < 2.5 && cd <= 0) { game.player.damage(10); cd = 1; } // удар раз в секунду });` }], }, { @@ -781,13 +785,25 @@ game.self.onClick(() => { icon: 'zombie', category: 'npc', prims: [{ type: 'cylinder', x: 0, y: 0.15, z: 0, sx: 2, sy: 0.3, sz: 2, color: '#7a2030', material: 'neon', name: 'Портал врагов' }], scripts: [{ attachTo: 'on-target', code: -`// Каждые 5с спавнит 2 врагов из точки портала, они идут к игроку. +`// Каждые 5с спавнит 2 врагов из портала, они идут к игроку И бьют при касании. const p = game.self.position; +const enemies = []; function wave(){ - for (let i=0;i<2;i++){ const e = game.scene.spawnNpc('skin_roblox-noob', { x:p.x+(Math.random()-0.5)*2, z:p.z+(Math.random()-0.5)*2, name:'Враг', speed:3 }); - if (e && e.follow) e.follow('player'); } + for (let i=0;i<2;i++){ const e = game.scene.spawnNpc('skin_retro-zombie', { x:p.x+(Math.random()-0.5)*2, z:p.z+(Math.random()-0.5)*2, name:'Враг', hp:60, speed:3 }); + if (e){ if (e.follow) e.follow('player'); enemies.push({ npc:e, cd:0 }); } } } -game.after(2, wave); game.every(5, wave);` }], +game.after(2, wave); game.every(5, wave); +// Урон игроку при касании любого врага (кулдаун у каждого свой). +game.onTick((dt) => { + const pl = game.player.position; + for (const en of enemies){ + if (!en.npc || !en.npc.position) continue; + en.cd -= dt; + const e = en.npc.position; + const d = Math.sqrt((pl.x-e.x)**2 + (pl.z-e.z)**2); + if (d < 2.5 && en.cd <= 0){ game.player.damage(8); en.cd = 1; } + } +});` }], }, // --- Экономика --- @@ -827,29 +843,48 @@ game.gui.onClick('clickbtn', () => { n++; show(); });` }], { id: 'key-lock', name: 'Ключ и замок', - desc: 'Подбери ключ, затем открой запертую дверь. Без ключа дверь не открывается. (Вики: «Ключ и сундук»)', + desc: 'Найди золотой ключ, подбери — и дверь рядом плавно откроется по E. Без ключа заперта. (Вики: «Ключ и сундук»)', icon: 'key', category: 'economy', prims: [ - { type: 'cube', x: 0, y: 1, z: 0, sx: 0.4, sy: 0.8, sz: 0.4, color: '#ffd23a', material: 'metal', name: 'Ключ' }, - { type: 'cube', x: 6, y: 2, z: 0, sx: 0.5, sy: 4, sz: 3, color: '#6b4423', material: 'matte', name: 'Запертая дверь' }, + // Ключ из примитивов: стержень + бородка + кольцо (torus). ПЕРВЫЙ — скрипт на нём. + { type: 'cylinder', x: 0, y: 1, z: 0, sx: 0.12, sy: 1.0, sz: 0.12, color: '#ffd23a', material: 'metal', name: 'Ключ' }, + { type: 'torus', x: 0, y: 1.6, z: 0, sx: 0.5, sy: 0.5, sz: 0.5, color: '#ffd23a', material: 'metal', canCollide: false, name: 'Кольцо ключа' }, + { type: 'cube', x: 0.18, y: 0.6, z: 0, sx: 0.3, sy: 0.12, sz: 0.12, color: '#ffd23a', material: 'metal', canCollide: false, name: 'Бородка ключа' }, + { type: 'cube', x: 0.18, y: 0.4, z: 0, sx: 0.2, sy: 0.12, sz: 0.12, color: '#ffd23a', material: 'metal', canCollide: false, name: 'Бородка ключа 2' }, + // Красивая дверь (полотно + рамка) на расстоянии. + { type: 'cube', x: 6, y: 2, z: 0, sx: 0.25, sy: 3.8, sz: 2.6, color: '#6b4423', material: 'matte', name: 'Запертая дверь' }, + { type: 'cube', x: 6, y: 2, z: -1.55, sx: 0.4, sy: 4.2, sz: 0.4, color: '#4a3018', material: 'matte', name: 'Косяк левый замка' }, + { type: 'cube', x: 6, y: 2, z: 1.55, sx: 0.4, sy: 4.2, sz: 0.4, color: '#4a3018', material: 'matte', name: 'Косяк правый замка' }, + { type: 'cube', x: 6, y: 4.1, z: 0, sx: 0.4, sy: 0.4, sz: 3.5, color: '#4a3018', material: 'matte', name: 'Перемычка замка' }, ], scripts: [{ attachTo: 'on-target', code: -`// Ключ (этот объект) подбирается касанием → открывает «Запертую дверь». +`// Ключ подбирается касанием. Дверь рядом открывается по E ТОЛЬКО с ключом — +// плавный поворот вокруг петли (как дверь по кнопке E). let hasKey = false; +const keyParts = ['Ключ','Кольцо ключа','Бородка ключа','Бородка ключа 2']; game.self.onTouch(() => { if (hasKey) return; hasKey = true; - game.self.setVisible(false); + for (const nm of keyParts){ const o = game.scene.findOne(nm); if (o) o.visible = false; } game.ui.set('key', '🔑 Ключ найден! Иди к двери (E).', {x:50,y:80,anchor:'bottom',color:'#ffd23a',size:18}); }); const door = game.scene.findOne('Запертая дверь'); -if (door && door.onInteract) { - let opened = false; +if (door && door.onInteract){ + const dp0 = door.position; + const halfW = 1.3, hingeZ = dp0.z - halfW; + let open = false, cur = 0, target = 0; + function rotY(lx,lz,a){ const s=Math.sin(a),c=Math.cos(a); return {x:lx*c+lz*s, z:-lx*s+lz*c}; } + game.onTick((dt) => { + if (cur===target) return; + const st = Math.PI*dt; cur = Math.abs(target-cur)<=st ? target : cur+Math.sign(target-cur)*st; + const pc = rotY(0, halfW, cur); + door.move(dp0.x+pc.x, dp0.y, hingeZ+pc.z); if (door.rotate) door.rotate(cur); + }); door.onInteract(() => { - if (!hasKey) { game.ui.set('key','🔒 Заперто. Нужен ключ.',{x:50,y:80,anchor:'bottom',color:'#ff5555',size:18}); return; } - if (opened) return; opened = true; - const dp = door.position; door.move(dp.x, dp.y-4.2, dp.z); - game.ui.set('key','✓ Дверь открыта!',{x:50,y:80,anchor:'bottom',color:'#36d57a',size:18}); - }, { text:'Открыть дверь', key:'e', distance:4 }); + if (!hasKey){ game.ui.set('key','🔒 Заперто. Нужен ключ.',{x:50,y:80,anchor:'bottom',color:'#ff5555',size:18}); return; } + open = !open; target = open ? Math.PI/2 : 0; + game.ui.set('key', open ? '✓ Дверь открыта!' : '🔒 Дверь закрыта.', {x:50,y:80,anchor:'bottom',color:'#36d57a',size:18}); + game.after(2, () => game.ui.set('key','',{})); + }, { text:'Открыть / закрыть', key:'e', distance:4 }); }` }], }, diff --git a/src/editor/engine/SelectionManager.js b/src/editor/engine/SelectionManager.js index cf1dbcf..f20b397 100644 --- a/src/editor/engine/SelectionManager.js +++ b/src/editor/engine/SelectionManager.js @@ -717,10 +717,13 @@ export class SelectionManager { this.blockManager.removeBlock(this._selection.gridX, this._selection.gridY, this._selection.gridZ); } else if (this._selection.type === 'model') { this.modelManager.removeInstance(this._selection.instanceId); + this.clear(); this._scene3d?._cleanupOrphanScripts?.(); return; } else if (this._selection.type === 'userModel') { this.userModelManager.removeInstance(this._selection.instanceId); + this.clear(); this._scene3d?._cleanupOrphanScripts?.(); return; } else if (this._selection.type === 'primitive') { this.primitiveManager.removeInstance(this._selection.id); + this.clear(); this._scene3d?._cleanupOrphanScripts?.(); return; } else if (this._selection.type === 'spawn') { // Удаление точки спавна → игрок будет появляться в (0, высота, 0). this._scene3d?.deleteSpawn?.();