fix(studio): враг=NPC+урон, волна бьёт, удалённые скрипты не исполняются, ключ+дверь красивые, тени-acne
1) Враг с HP → R15-NPC (зомби-скин), преследует и бьёт игрока при касании. 2) Волна врагов: враги наносят урон при касании (onTick дистанция → damage). 3) Удалённые скрипты больше не исполняются: _cleanupOrphanScripts при удалении объекта (primitive/model/userModel) + перед enterPlayMode чистим скрипты-сироты. 4) Ключ и замок: ключ из примитивов (стержень+кольцо-torus+бородка), дверь как дверь-по-E (плавный поворот вокруг петли, только с ключом). 6) Тени-полосы: normalBias 0.005→0.02 (убирает acne-полосы от соседних объектов). Машина (vehicle:car) — остаётся рантайм-спавном (особенность транспорта). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
26e6306f6e
commit
c4d184257b
@ -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;
|
||||
}}
|
||||
|
||||
@ -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 || []);
|
||||
|
||||
@ -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;
|
||||
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 });
|
||||
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 });
|
||||
}` }],
|
||||
},
|
||||
|
||||
|
||||
@ -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?.();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user