Merge pull request 'fix(engine): findOne(x).onTouch + findOne �� ������ + ������� Instance-proxy' (#11) from fix/pointer-ontouch-findone into main
This commit is contained in:
commit
cd31078e6d
@ -2874,12 +2874,59 @@ export class BabylonScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) Касания объектов, на которые подписан ГЛОБАЛЬНЫЙ скрипт через
|
||||||
|
// findOne(x).onTouch(...) (rt._watchedTouchRefs). Объекты без скрипта
|
||||||
|
// и не триггеры — например цели туториала. Событие адресное (по ref).
|
||||||
|
const watched = rt._watchedTouchRefs;
|
||||||
|
if (watched && watched.size > 0) {
|
||||||
|
for (const ref of watched) {
|
||||||
|
const target = this._refToTarget(ref);
|
||||||
|
if (!target) continue;
|
||||||
|
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()) {
|
for (const id of this._touchState.keys()) {
|
||||||
if (!seen.has(id)) this._touchState.delete(id);
|
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') {
|
||||||
|
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-детекции). */
|
/** Получить мировой AABB target-объекта (для touch-детекции). */
|
||||||
_targetAABB(target) {
|
_targetAABB(target) {
|
||||||
if (!target) return null;
|
if (!target) return null;
|
||||||
@ -5283,6 +5330,8 @@ export class BabylonScene {
|
|||||||
enterPlayMode() {
|
enterPlayMode() {
|
||||||
if (this._isPlaying) return;
|
if (this._isPlaying) return;
|
||||||
this._isPlaying = true;
|
this._isPlaying = true;
|
||||||
|
// Сброс состояния касаний — каждый прогон начинается «не касаясь».
|
||||||
|
if (this._touchState) this._touchState.clear();
|
||||||
// По умолчанию стандартный HUD видим в Play.
|
// По умолчанию стандартный HUD видим в Play.
|
||||||
// Скрипт может скрыть через game.hud.setVisible(false).
|
// Скрипт может скрыть через game.hud.setVisible(false).
|
||||||
this._setStdHudVisible(true);
|
this._setStdHudVisible(true);
|
||||||
|
|||||||
@ -81,6 +81,11 @@ export class GameRuntime {
|
|||||||
modules[s.name] = s.code;
|
modules[s.name] = s.code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Первичный snapshot сцены — собираем СИНХРОННО ДО запуска скриптов и
|
||||||
|
// передаём прямо в init. Иначе findOne() в синхронном теле скрипта
|
||||||
|
// (на старте) возвращает null → подписки obj.onTouch/find не работают.
|
||||||
|
let initialScene = null;
|
||||||
|
try { initialScene = this._buildSceneSnapshot(); } catch (e) { initialScene = null; }
|
||||||
for (const s of scripts) {
|
for (const s of scripts) {
|
||||||
if (!s || typeof s.code !== 'string' || !s.code.trim()) {
|
if (!s || typeof s.code !== 'string' || !s.code.trim()) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -90,6 +95,7 @@ export class GameRuntime {
|
|||||||
const sb = new ScriptSandbox(s.code, s.target || null);
|
const sb = new ScriptSandbox(s.code, s.target || null);
|
||||||
sb.scriptId = s.id;
|
sb.scriptId = s.id;
|
||||||
sb.setModules(modules);
|
sb.setModules(modules);
|
||||||
|
if (initialScene) sb.setInitialScene(initialScene);
|
||||||
// Если target есть — передаём начальную позицию self до старта
|
// Если target есть — передаём начальную позицию self до старта
|
||||||
if (s.target) {
|
if (s.target) {
|
||||||
const pos = this._collectSelfPosition(s.target);
|
const pos = this._collectSelfPosition(s.target);
|
||||||
@ -445,6 +451,8 @@ export class GameRuntime {
|
|||||||
this._objectData = {};
|
this._objectData = {};
|
||||||
this._interactables = [];
|
this._interactables = [];
|
||||||
this._activeInteractRef = null;
|
this._activeInteractRef = null;
|
||||||
|
this._watchedTouchRefs = null;
|
||||||
|
this._watchedClickRefs = null;
|
||||||
this._roomState = {};
|
this._roomState = {};
|
||||||
this._seenSessions = null;
|
this._seenSessions = null;
|
||||||
this._teams = new Map();
|
this._teams = new Map();
|
||||||
@ -1135,6 +1143,16 @@ export class GameRuntime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Адресное событие касания/клика КОНКРЕТНОГО объекта по его ref.
|
||||||
|
* Доставляется всем sandbox'ам как globalEvent с type='instTouch'|... + ref;
|
||||||
|
* worker матчит по ref на findOne(x).onTouch/onUntouch/onClick.
|
||||||
|
*/
|
||||||
|
routeInstEvent(ref, type, extra = {}) {
|
||||||
|
if (!ref || !type) return;
|
||||||
|
this.routeGlobalEvent(type, { ref, ...extra });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Уведомление: моб убит. Рассылается во все скрипты как 'mobKilled'.
|
* Уведомление: моб убит. Рассылается во все скрипты как 'mobKilled'.
|
||||||
* Скрипт может подписаться через `game.onMobKilled(fn)`.
|
* Скрипт может подписаться через `game.onMobKilled(fn)`.
|
||||||
@ -1302,6 +1320,26 @@ export class GameRuntime {
|
|||||||
this._log(payload?.level || 'info', payload?.text || '');
|
this._log(payload?.level || 'info', payload?.text || '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// inst.watchTouch / inst.watchClick — скрипт подписался на касание/клик
|
||||||
|
// ПРОИЗВОЛЬНОГО объекта (findOne(x).onTouch/onClick). Движок начинает
|
||||||
|
// следить за AABB этого объекта в _detectTouchEvents и слать обратно
|
||||||
|
// instTouch/instUntouch (через routeInstEvent).
|
||||||
|
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;
|
||||||
|
}
|
||||||
if (cmd === 'player.teleport') {
|
if (cmd === 'player.teleport') {
|
||||||
const player = this.scene3d?.player;
|
const player = this.scene3d?.player;
|
||||||
if (player && player._pos && payload) {
|
if (player && player._pos && payload) {
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export class ScriptSandbox {
|
|||||||
target: this.target,
|
target: this.target,
|
||||||
selfPosition: this._initialSelfPosition || null,
|
selfPosition: this._initialSelfPosition || null,
|
||||||
modules: this._modules || {},
|
modules: this._modules || {},
|
||||||
|
initialScene: this._initialScene || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -69,6 +70,11 @@ export class ScriptSandbox {
|
|||||||
this._initialSelfPosition = p;
|
this._initialSelfPosition = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Первичный snapshot сцены (до start) — чтобы findOne работал на старте. */
|
||||||
|
setInitialScene(snap) {
|
||||||
|
this._initialScene = snap;
|
||||||
|
}
|
||||||
|
|
||||||
/** Установить код модулей для game.require — { 'имя': 'код' }. Вызывать до start(). */
|
/** Установить код модулей для game.require — { 'имя': 'код' }. Вызывать до start(). */
|
||||||
setModules(modules) {
|
setModules(modules) {
|
||||||
this._modules = modules || {};
|
this._modules = modules || {};
|
||||||
|
|||||||
@ -91,6 +91,15 @@ let _selfTouchHandlers = [];
|
|||||||
let _selfUntouchHandlers = [];
|
let _selfUntouchHandlers = [];
|
||||||
// Подписки self.onInteract — взаимодействие по клавише E (ProximityPrompt)
|
// Подписки self.onInteract — взаимодействие по клавише E (ProximityPrompt)
|
||||||
let _selfInteractHandlers = [];
|
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')
|
// Зеркало всех GUI-элементов (синхронизируется main через cmd='guiSnapshot')
|
||||||
let _guiIndex = [];
|
let _guiIndex = [];
|
||||||
// Задача 07: зеркало скинов (синхронизируется через 'skinsSnapshot').
|
// Задача 07: зеркало скинов (синхронизируется через 'skinsSnapshot').
|
||||||
@ -269,6 +278,260 @@ let _terrainHM = null;
|
|||||||
// getData читает отсюда синхронно, setData шлёт команду в main.
|
// getData читает отсюда синхронно, setData шлёт команду в main.
|
||||||
let _dataIndex = {};
|
let _dataIndex = {};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Instance-proxy (паритет со студией): game.scene.find/findOne/all возвращают
|
||||||
|
// Proxy с методами (onTouch/onUntouch/onClick, tween, move, changed.connect,
|
||||||
|
// position/name/parent/children и т.д.). Coerces в строку-ref через
|
||||||
|
// Symbol.toPrimitive/valueOf/toString, поэтому старый код (scene.setColor и
|
||||||
|
// т.п., принимавший строковый ref) продолжает работать без изменений.
|
||||||
|
const _instCache = new Map(); // ref → Instance proxy
|
||||||
|
const _instEvents = new Map(); // ref → { propChanged: [{prop,fn}], destroying: [fn] }
|
||||||
|
const _instLastValues = new Map(); // ref → { x, y, z, name } предыдущий snapshot
|
||||||
|
|
||||||
|
function _safeCall2(fn, args, where) {
|
||||||
|
try { fn.apply(null, args); }
|
||||||
|
catch (err) {
|
||||||
|
_send('log', {
|
||||||
|
level: 'error',
|
||||||
|
text: 'Ошибка в ' + where + ': ' + (err && err.message ? err.message : err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getOrCreateInstance(ref, kindHint) {
|
||||||
|
if (!ref || typeof ref !== 'string') return null;
|
||||||
|
if (_instCache.has(ref)) return _instCache.get(ref);
|
||||||
|
|
||||||
|
const target = {
|
||||||
|
get ref() { return ref; },
|
||||||
|
get kind() {
|
||||||
|
if (kindHint) return kindHint;
|
||||||
|
for (const b of _sceneIndex.blocks) if (b.ref === ref) return 'block';
|
||||||
|
for (const m of _sceneIndex.models) if (m.ref === ref) return 'model';
|
||||||
|
for (const p of _sceneIndex.primitives) if (p.ref === ref) return 'primitive';
|
||||||
|
return 'unknown';
|
||||||
|
},
|
||||||
|
toString() { return ref; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxy = new Proxy(target, {
|
||||||
|
get(t, prop) {
|
||||||
|
if (prop === 'ref' || prop === 'kind') return t[prop];
|
||||||
|
if (prop === 'toString') return t.toString;
|
||||||
|
if (prop === Symbol.toPrimitive) return () => ref;
|
||||||
|
if (prop === 'valueOf') return () => ref;
|
||||||
|
|
||||||
|
// === Геттеры из snapshot ===
|
||||||
|
if (prop === 'position') {
|
||||||
|
for (const arr of [_sceneIndex.blocks, _sceneIndex.models, _sceneIndex.primitives]) {
|
||||||
|
for (const o of arr) if (o.ref === ref) return { x: o.x, y: o.y, z: o.z };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (prop === 'name') {
|
||||||
|
for (const arr of [_sceneIndex.models, _sceneIndex.primitives]) {
|
||||||
|
for (const o of arr) if (o.ref === ref) return o.name || null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (prop === 'parent') {
|
||||||
|
const data = _dataIndex[ref];
|
||||||
|
const parentRef = data && data.__parent;
|
||||||
|
return parentRef ? _getOrCreateInstance(parentRef) : null;
|
||||||
|
}
|
||||||
|
if (prop === 'children') {
|
||||||
|
const data = _dataIndex[ref];
|
||||||
|
const ids = (data && data.__children) || [];
|
||||||
|
return ids.map(id => _getOrCreateInstance(id)).filter(Boolean);
|
||||||
|
}
|
||||||
|
if (prop === 'descendants') {
|
||||||
|
return () => {
|
||||||
|
const out = [];
|
||||||
|
const stack = [...((_dataIndex[ref] || {}).__children || [])];
|
||||||
|
while (stack.length) {
|
||||||
|
const id = stack.pop();
|
||||||
|
const inst = _getOrCreateInstance(id);
|
||||||
|
if (inst) {
|
||||||
|
out.push(inst);
|
||||||
|
const kids = (_dataIndex[id] || {}).__children || [];
|
||||||
|
for (const k of kids) stack.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// === События ===
|
||||||
|
if (prop === 'changed') {
|
||||||
|
return {
|
||||||
|
connect(propName, fn) {
|
||||||
|
if (typeof fn !== 'function') return;
|
||||||
|
let evs = _instEvents.get(ref);
|
||||||
|
if (!evs) { evs = { propChanged: [], destroying: [] }; _instEvents.set(ref, evs); }
|
||||||
|
evs.propChanged.push({ prop: propName, fn });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (prop === 'destroying') {
|
||||||
|
return {
|
||||||
|
connect(fn) {
|
||||||
|
if (typeof fn !== 'function') return;
|
||||||
|
let evs = _instEvents.get(ref);
|
||||||
|
if (!evs) { evs = { propChanged: [], destroying: [] }; _instEvents.set(ref, evs); }
|
||||||
|
evs.destroying.push(fn);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Методы ===
|
||||||
|
if (prop === 'destroy' || prop === 'delete') return () => _send('scene.delete', { ref });
|
||||||
|
if (prop === 'clone') return (offset) => _send('scene.clone', { ref, offset: offset || {} });
|
||||||
|
if (prop === 'setAttribute') return (k, v) => _send('scene.setData', { ref, key: k, value: v });
|
||||||
|
if (prop === 'getAttribute') return (k) => {
|
||||||
|
const data = _dataIndex[ref];
|
||||||
|
return data ? data[k] : undefined;
|
||||||
|
};
|
||||||
|
if (prop === 'hasTag') return (t) => {
|
||||||
|
const data = _dataIndex[ref];
|
||||||
|
return Array.isArray(data && data.__tags) && data.__tags.includes(t);
|
||||||
|
};
|
||||||
|
if (prop === 'addTag') return (t) => _send('scene.tag', { ref, tag: t });
|
||||||
|
if (prop === 'removeTag') return (t) => _send('scene.untag', { ref, tag: t });
|
||||||
|
if (prop === 'tween') return (props, opts) => game.tween(ref, props, opts);
|
||||||
|
if (prop === 'move') return (x, y, z) => _send('scene.move', { ref, x, y, z });
|
||||||
|
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) {
|
||||||
|
if (prop === 'name') {
|
||||||
|
_send('inst.set', { ref, prop: 'name', value: String(value) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'parent') {
|
||||||
|
const parentRef = value && typeof value === 'object'
|
||||||
|
? (value.ref || value.toString())
|
||||||
|
: (value || null);
|
||||||
|
_send('inst.setParent', { ref, parentRef });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'position') {
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
const x = Number(value.x), y = Number(value.y), z = Number(value.z);
|
||||||
|
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
|
||||||
|
_send('scene.move', { ref, x, y, z });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'color') {
|
||||||
|
_send('scene.setColor', { ref, color: String(value) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'transparency' || prop === 'opacity') {
|
||||||
|
const v = Number(value);
|
||||||
|
if (Number.isFinite(v)) {
|
||||||
|
const op = prop === 'transparency' ? (1 - v) : v;
|
||||||
|
_send('scene.setOpacity', { ref, value: op });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'visible') {
|
||||||
|
_send('scene.setVisible', { ref, visible: !!value });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'canCollide') {
|
||||||
|
_send('scene.setCollide', { ref, collide: !!value });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (prop === 'material') {
|
||||||
|
_send('scene.setMaterial', { ref, name: String(value) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
_instCache.set(ref, proxy);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Триггер событий изменения свойства (вызывается при дельте snapshot'а). */
|
||||||
|
function _emitInstChange(ref, prop, newVal, oldVal) {
|
||||||
|
const evs = _instEvents.get(ref);
|
||||||
|
if (!evs) return;
|
||||||
|
for (const rec of evs.propChanged) {
|
||||||
|
if (rec.prop === prop) {
|
||||||
|
_safeCall2(rec.fn, [newVal, oldVal], 'inst.changed:' + prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Триггер destroying — объект больше не в snapshot. */
|
||||||
|
function _emitInstDestroying(ref) {
|
||||||
|
const evs = _instEvents.get(ref);
|
||||||
|
if (evs) {
|
||||||
|
for (const fn of evs.destroying) {
|
||||||
|
_safeCall2(fn, [], 'inst.destroying');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_instEvents.delete(ref);
|
||||||
|
_instCache.delete(ref);
|
||||||
|
_instLastValues.delete(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сравнение нового snapshot со старым — детект дельт для событий.
|
||||||
|
* Вызывается из обработчика 'sceneSnapshot' ПОСЛЕ обновления _sceneIndex.
|
||||||
|
*/
|
||||||
|
function _detectSnapshotDeltas() {
|
||||||
|
const live = new Set();
|
||||||
|
for (const arr of [_sceneIndex.blocks, _sceneIndex.models, _sceneIndex.primitives]) {
|
||||||
|
for (const o of arr) {
|
||||||
|
live.add(o.ref);
|
||||||
|
if (!_instCache.has(o.ref)) continue;
|
||||||
|
const prev = _instLastValues.get(o.ref) || {};
|
||||||
|
if (prev.x !== undefined && (prev.x !== o.x || prev.y !== o.y || prev.z !== o.z)) {
|
||||||
|
_emitInstChange(o.ref, 'position',
|
||||||
|
{ x: o.x, y: o.y, z: o.z },
|
||||||
|
{ x: prev.x, y: prev.y, z: prev.z });
|
||||||
|
}
|
||||||
|
if (prev.name !== undefined && prev.name !== o.name) {
|
||||||
|
_emitInstChange(o.ref, 'name', o.name, prev.name);
|
||||||
|
}
|
||||||
|
_instLastValues.set(o.ref, { x: o.x, y: o.y, z: o.z, name: o.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ref of [..._instCache.keys()]) {
|
||||||
|
if (live.has(ref)) continue;
|
||||||
|
const inst = _instCache.get(ref);
|
||||||
|
if (inst && inst.kind === 'primitive') {
|
||||||
|
_emitInstDestroying(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Модули (game.require). Код всех скриптов-модулей приходит при init.
|
// Модули (game.require). Код всех скриптов-модулей приходит при init.
|
||||||
// _moduleCode — { 'имя': 'код модуля' }
|
// _moduleCode — { 'имя': 'код модуля' }
|
||||||
// _moduleCache — { 'имя': exports } — кеш исполненных модулей
|
// _moduleCache — { 'имя': exports } — кеш исполненных модулей
|
||||||
@ -1404,15 +1667,23 @@ const game = {
|
|||||||
pivot: { x: Number(pivot.x), z: Number(pivot.z) },
|
pivot: { x: Number(pivot.x), z: Number(pivot.z) },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/** Найти объекты по name (для моделей/примитивов). Возвращает string[]. */
|
/**
|
||||||
|
* Найти объекты по name. Возвращает массив Instance-прокси (паритет
|
||||||
|
* со студией). Instance coerces в строку-ref, поэтому код, принимавший
|
||||||
|
* строковый ref, продолжает работать.
|
||||||
|
*/
|
||||||
find(name) {
|
find(name) {
|
||||||
const out = [];
|
const out = [];
|
||||||
const n = String(name || '').toLowerCase();
|
const n = String(name || '').toLowerCase();
|
||||||
for (const m of _sceneIndex.models) {
|
for (const m of _sceneIndex.models) {
|
||||||
if (m.name && String(m.name).toLowerCase() === n) out.push(m.ref);
|
if (m.name && String(m.name).toLowerCase() === n) {
|
||||||
|
out.push(_getOrCreateInstance(m.ref, 'model'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const p of _sceneIndex.primitives) {
|
for (const p of _sceneIndex.primitives) {
|
||||||
if (p.name && String(p.name).toLowerCase() === n) out.push(p.ref);
|
if (p.name && String(p.name).toLowerCase() === n) {
|
||||||
|
out.push(_getOrCreateInstance(p.ref, 'primitive'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
@ -1421,11 +1692,11 @@ const game = {
|
|||||||
const arr = this.find(name);
|
const arr = this.find(name);
|
||||||
return arr.length > 0 ? arr[0] : null;
|
return arr.length > 0 ? arr[0] : null;
|
||||||
},
|
},
|
||||||
/** Список ref'ов всех объектов заданного типа: 'block' | 'model' | 'primitive'. */
|
/** Список Instance всех объектов заданного типа: 'block' | 'model' | 'primitive'. */
|
||||||
all(kind) {
|
all(kind) {
|
||||||
if (kind === 'block') return _sceneIndex.blocks.map(b => b.ref);
|
if (kind === 'block') return _sceneIndex.blocks.map(b => _getOrCreateInstance(b.ref, 'block'));
|
||||||
if (kind === 'model') return _sceneIndex.models.map(m => m.ref);
|
if (kind === 'model') return _sceneIndex.models.map(m => _getOrCreateInstance(m.ref, 'model'));
|
||||||
if (kind === 'primitive') return _sceneIndex.primitives.map(p => p.ref);
|
if (kind === 'primitive') return _sceneIndex.primitives.map(p => _getOrCreateInstance(p.ref, 'primitive'));
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -3018,6 +3289,17 @@ self.onmessage = (e) => {
|
|||||||
if (payload && payload.modules && typeof payload.modules === 'object') {
|
if (payload && payload.modules && typeof payload.modules === 'object') {
|
||||||
_moduleCode = payload.modules;
|
_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 {
|
try {
|
||||||
// exports передаём всегда — скрипт может быть и модулем (пишет в
|
// exports передаём всегда — скрипт может быть и модулем (пишет в
|
||||||
// exports), и обычным скриптом (игнорирует его). Без этого
|
// exports), и обычным скриптом (игнорирует его). Без этого
|
||||||
@ -3166,6 +3448,13 @@ self.onmessage = (e) => {
|
|||||||
}
|
}
|
||||||
} else if (t === 'playerTouch') {
|
} else if (t === 'playerTouch') {
|
||||||
for (const fn of _globalTouchHandlers) _safeCall(fn, payload, 'onPlayerTouch');
|
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') {
|
} else if (t === 'hpChange') {
|
||||||
for (const fn of _hpChangeHandlers) _safeCall(fn, payload, 'onHpChange');
|
for (const fn of _hpChangeHandlers) _safeCall(fn, payload, 'onHpChange');
|
||||||
} else if (t === 'mobKilled') {
|
} else if (t === 'mobKilled') {
|
||||||
@ -3323,6 +3612,8 @@ self.onmessage = (e) => {
|
|||||||
models: payload.models || [],
|
models: payload.models || [],
|
||||||
primitives: payload.primitives || [],
|
primitives: payload.primitives || [],
|
||||||
};
|
};
|
||||||
|
// детект дельт и эмит events для Instance (если кто-то подписан).
|
||||||
|
try { _detectSnapshotDeltas(); } catch (e) {}
|
||||||
}
|
}
|
||||||
} else if (cmd === 'guiSnapshot') {
|
} else if (cmd === 'guiSnapshot') {
|
||||||
// payload: массив всех GUI-элементов (для game.gui.find/get/all)
|
// payload: массив всех GUI-элементов (для game.gui.find/get/all)
|
||||||
@ -3385,6 +3676,7 @@ self.onmessage = (e) => {
|
|||||||
_selfTouchHandlers = [];
|
_selfTouchHandlers = [];
|
||||||
_selfUntouchHandlers = [];
|
_selfUntouchHandlers = [];
|
||||||
_selfInteractHandlers = [];
|
_selfInteractHandlers = [];
|
||||||
|
_instTouchHandlers.clear();
|
||||||
_globalKeyDownHandlers = {};
|
_globalKeyDownHandlers = {};
|
||||||
_globalKeyUpHandlers = {};
|
_globalKeyUpHandlers = {};
|
||||||
_globalClickHandlers = [];
|
_globalClickHandlers = [];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user