diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index b82b57e..75420f8 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -2890,12 +2890,65 @@ export class BabylonScene { } } + // 3) Касания объектов, на которые подписан ГЛОБАЛЬНЫЙ скрипт через + // findOne(x).onTouch(...) (rt._watchedTouchRefs). Это объекты без + // собственного скрипта-target и не триггеры — например цели туториала. + // Ключ touchState = 'w:'+ref, событие — адресное (routeInstEvent по ref). + const watched = rt._watchedTouchRefs; + if (watched && watched.size > 0) { + for (const ref of watched) { + const target = this._refToTarget(ref); + if (!target) continue; + // Не дублируем, если на этот же объект уже висит target-скрипт + // (он обработан в блоке 1 и сам отправит touch своему скрипту — + // но адресное instTouch всё равно нужно глобальному подписчику, + // поэтому НЕ пропускаем, просто используем отдельный ключ 'w:'). + const aabb = this._targetAABB(target); + if (!aabb) continue; + const key = 'w:' + ref; + seen.add(key); + const overlap = + px + phw > aabb.minX - EPS && px - phw < aabb.maxX + EPS && + py + phh > aabb.minY - EPS && py - phh < aabb.maxY + EPS && + pz + phd > aabb.minZ - EPS && pz - phd < aabb.maxZ + EPS; + const wasTouching = this._touchState.get(key); + if (overlap && !wasTouching) { + this._touchState.set(key, true); + rt.routeInstEvent(ref, 'instTouch', {}); + } else if (!overlap && wasTouching) { + this._touchState.set(key, false); + rt.routeInstEvent(ref, 'instUntouch', {}); + } + } + } + // Чистим устаревшие записи (удалённые скрипты/триггеры) for (const id of this._touchState.keys()) { if (!seen.has(id)) this._touchState.delete(id); } } + /** Ref-строка 'primitive:NN' | 'model:NN' → {kind,id} для _targetAABB. */ + _refToTarget(ref) { + if (typeof ref !== 'string') return null; + const colon = ref.indexOf(':'); + if (colon < 0) return null; + const kind = ref.slice(0, colon); + const rest = ref.slice(colon + 1); + if (kind === 'primitive') { + // через runtime — корректно разрешает и local-ref, и числовой id + const id = this.gameRuntime?._resolvePrimitiveId + ? this.gameRuntime._resolvePrimitiveId(rest) + : (Number.isFinite(Number(rest)) ? Number(rest) : rest); + return { kind: 'primitive', id }; + } + if (kind === 'model') { + const n = Number(rest); + return { kind: 'model', id: Number.isFinite(n) ? n : rest }; + } + return null; + } + /** Получить мировой AABB target-объекта (для touch-детекции). */ _targetAABB(target) { if (!target) return null; @@ -5285,6 +5338,9 @@ export class BabylonScene { enterPlayMode() { if (this._isPlaying) return; this._isPlaying = true; + // Сброс состояния касаний — каждый прогон начинается «не касаясь», + // иначе rising-edge touch не сработает, если при стопе игрок стоял на цели. + if (this._touchState) this._touchState.clear(); // По умолчанию стандартный HUD видим в Play. // Скрипт может скрыть через game.hud.setVisible(false). this._setStdHudVisible(true); diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 0c503e6..2e9753a 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -87,6 +87,13 @@ export class GameRuntime { modules[s.name] = s.code; } } + // Первичный snapshot сцены — собираем СИНХРОННО ДО запуска скриптов и + // передаём прямо в init. Иначе findOne() в синхронном теле скрипта + // (на старте) возвращает null, т.к. snapshot раньше слался лишь в rAF + // ПОСЛЕ init → подписки obj.onTouch/find на старте не работали + // (баг «стрелка-указатель не переключается на след. цель»). + let initialScene = null; + try { initialScene = this._buildSceneSnapshot(); } catch (e) { initialScene = null; } for (const s of scripts) { if (!s || typeof s.code !== 'string' || !s.code.trim()) { // eslint-disable-next-line no-console @@ -96,6 +103,7 @@ export class GameRuntime { const sb = new ScriptSandbox(s.code, s.target || null); sb.scriptId = s.id; sb.setModules(modules); + if (initialScene) sb.setInitialScene(initialScene); // Если target есть — передаём начальную позицию self до старта if (s.target) { const pos = this._collectSelfPosition(s.target); @@ -347,6 +355,8 @@ export class GameRuntime { this._objectData = {}; this._interactables = []; this._activeInteractRef = null; + this._watchedTouchRefs = null; + this._watchedClickRefs = null; this._roomState = {}; this._seenSessions = null; this._teams = new Map(); @@ -1142,6 +1152,17 @@ export class GameRuntime { } } + /** + * Адресное событие касания/клика КОНКРЕТНОГО объекта по его ref. + * Доставляется всем sandbox'ам как globalEvent с type='instTouch'|... + ref; + * worker матчит по ref на findOne(x).onTouch/onUntouch/onClick. + * type: 'instTouch' | 'instUntouch' | 'instClick'. + */ + routeInstEvent(ref, type, extra = {}) { + if (!ref || !type) return; + this.routeGlobalEvent(type, { ref, ...extra }); + } + /** * Уведомление: моб убит. Рассылается во все скрипты как 'mobKilled'. * Скрипт может подписаться через `game.onMobKilled(fn)`. @@ -2708,6 +2729,26 @@ export class GameRuntime { return; } // === Phase 6.2: Instance-модель === + // inst.watchTouch / inst.watchClick — скрипт подписался на касание/клик + // ПРОИЗВОЛЬНОГО объекта (findOne(x).onTouch/onClick). Движок начинает + // следить за AABB этого объекта в _detectTouchEvents и слать обратно + // instTouch/instUntouch (через routeInstEvent). Клик — через picking. + if (cmd === 'inst.watchTouch') { + const ref = payload && payload.ref; + if (typeof ref === 'string') { + if (!this._watchedTouchRefs) this._watchedTouchRefs = new Set(); + this._watchedTouchRefs.add(ref); + } + return; + } + if (cmd === 'inst.watchClick') { + const ref = payload && payload.ref; + if (typeof ref === 'string') { + if (!this._watchedClickRefs) this._watchedClickRefs = new Set(); + this._watchedClickRefs.add(ref); + } + return; + } // inst.set — изменить простое свойство Instance (name). // Сложные свойства (color/visible/...) идут через scene.set* и реализованы выше. if (cmd === 'inst.set') { diff --git a/src/editor/engine/ScriptSandbox.js b/src/editor/engine/ScriptSandbox.js index 9e56276..3e32ec4 100644 --- a/src/editor/engine/ScriptSandbox.js +++ b/src/editor/engine/ScriptSandbox.js @@ -53,6 +53,8 @@ export class ScriptSandbox { }); }; // Передаём код + target (если есть). Worker внутри сам выполнит его в new Function(game, code). + // initialScene — первичный snapshot сцены, чтобы findOne() работал + // в синхронном теле скрипта на старте (до прихода sceneSnapshot в rAF). this.worker.postMessage({ cmd: 'init', payload: { @@ -60,6 +62,7 @@ export class ScriptSandbox { target: this.target, selfPosition: this._initialSelfPosition || null, modules: this._modules || {}, + initialScene: this._initialScene || null, }, }); } @@ -69,6 +72,11 @@ export class ScriptSandbox { this._initialSelfPosition = p; } + /** Первичный snapshot сцены (до start) — чтобы findOne работал на старте. */ + setInitialScene(snap) { + this._initialScene = snap; + } + /** Установить код модулей для game.require — { 'имя': 'код' }. Вызывать до start(). */ setModules(modules) { this._modules = modules || {}; diff --git a/src/editor/engine/ScriptSandboxWorker.js b/src/editor/engine/ScriptSandboxWorker.js index e282138..ab0ceff 100644 --- a/src/editor/engine/ScriptSandboxWorker.js +++ b/src/editor/engine/ScriptSandboxWorker.js @@ -97,6 +97,15 @@ let _selfTouchHandlers = []; let _selfUntouchHandlers = []; // Подписки self.onInteract — взаимодействие по клавише E (ProximityPrompt) let _selfInteractHandlers = []; +// Подписки на касание/клик ПРОИЗВОЛЬНОГО объекта (через findOne(x).onTouch). +// ref → { touch:[fn], untouch:[fn], click:[fn] }. Движок следит за AABB этих +// объектов (cmd 'inst.watchTouch') и шлёт обратно instTouch/instUntouch/instClick. +const _instTouchHandlers = new Map(); +function _instHandlerBucket(ref) { + let b = _instTouchHandlers.get(ref); + if (!b) { b = { touch: [], untouch: [], click: [] }; _instTouchHandlers.set(ref, b); } + return b; +} // Зеркало всех GUI-элементов (синхронизируется main через cmd='guiSnapshot') let _guiIndex = []; // Задача 07: зеркало скинов (синхронизируется через 'skinsSnapshot'). @@ -467,6 +476,26 @@ function _getOrCreateInstance(ref, kindHint) { if (prop === 'setLabel') return (text, o) => _send('scene.setLabel', { ref, text, opts: o || {} }); if (prop === 'clearLabel') return () => _send('scene.clearLabel', { ref }); + // === События касания/клика ПРОИЗВОЛЬНОГО объекта === + // findOne('coin').onTouch(fn) — fn() когда игрок коснулся объекта. + // Аналог Roblox part.Touched:Connect. Движок начинает следить за AABB + // объекта (inst.watchTouch) и шлёт instTouch на rising edge. + if (prop === 'onTouch') return (fn) => { + if (typeof fn !== 'function') return; + _instHandlerBucket(ref).touch.push(fn); + _send('inst.watchTouch', { ref }); + }; + if (prop === 'onUntouch') return (fn) => { + if (typeof fn !== 'function') return; + _instHandlerBucket(ref).untouch.push(fn); + _send('inst.watchTouch', { ref }); + }; + if (prop === 'onClick') return (fn) => { + if (typeof fn !== 'function') return; + _instHandlerBucket(ref).click.push(fn); + _send('inst.watchClick', { ref }); + }; + return undefined; }, set(t, prop, value) { @@ -3539,6 +3568,17 @@ self.onmessage = (e) => { if (payload && payload.modules && typeof payload.modules === 'object') { _moduleCode = payload.modules; } + // Первичный snapshot сцены — заполняем _sceneIndex ДО исполнения кода, + // чтобы findOne()/find() работали в синхронном теле скрипта на старте + // (иначе obj.onTouch(...) не подписывался — объект ещё «не существовал»). + if (payload && payload.initialScene && typeof payload.initialScene === 'object') { + const s = payload.initialScene; + _sceneIndex = { + blocks: s.blocks || [], + models: s.models || [], + primitives: s.primitives || [], + }; + } try { const exportsObj = {}; const userFn = new Function('game', 'exports', '"use strict";\\n' + payload.code); @@ -3684,6 +3724,13 @@ self.onmessage = (e) => { } } else if (t === 'playerTouch') { for (const fn of _globalTouchHandlers) _safeCall(fn, payload, 'onPlayerTouch'); + } else if (t === 'instTouch' || t === 'instUntouch' || t === 'instClick') { + // Касание/клик произвольного объекта (findOne(x).onTouch/onUntouch/onClick). + const b = _instTouchHandlers.get(payload && payload.ref); + if (b) { + const list = t === 'instTouch' ? b.touch : t === 'instUntouch' ? b.untouch : b.click; + for (const fn of list) _safeCall(fn, payload, 'inst.' + t); + } } else if (t === 'hpChange') { for (const fn of _hpChangeHandlers) _safeCall(fn, payload, 'onHpChange'); } else if (t === 'mobKilled') { @@ -3928,6 +3975,7 @@ self.onmessage = (e) => { _selfTouchHandlers = []; _selfUntouchHandlers = []; _selfInteractHandlers = []; + _instTouchHandlers.clear(); _globalKeyDownHandlers = {}; _globalKeyUpHandlers = {}; _globalClickHandlers = [];