�������/������� (������� �� �������) + realtime �� game.rublox.pro #24
@ -5413,6 +5413,56 @@ export class BabylonScene {
|
|||||||
return this._isPlaying;
|
return this._isPlaying;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Задача 12+05: конфиг экрана загрузки из настроек проекта. */
|
||||||
|
setLoadingConfig(cfg, thumbnail) {
|
||||||
|
if (cfg && typeof cfg === 'object') {
|
||||||
|
this._loadingConfig = {
|
||||||
|
logo: cfg.logo || null,
|
||||||
|
accentColor: cfg.accentColor || '#ffc020',
|
||||||
|
defaultSpinner: cfg.defaultSpinner !== false,
|
||||||
|
defaultSkipButton: !!cfg.defaultSkipButton,
|
||||||
|
// Задача 05:
|
||||||
|
enabled: cfg.enabled !== false,
|
||||||
|
background: cfg.background || cfg.backgroundUrl || null,
|
||||||
|
cover: cfg.cover || cfg.coverUrl || null,
|
||||||
|
style: cfg.style || 'ken-burns',
|
||||||
|
placeName: cfg.placeName || '',
|
||||||
|
studioName: cfg.studioName || '',
|
||||||
|
verified: !!cfg.verified,
|
||||||
|
duration: Number.isFinite(cfg.duration) && cfg.duration > 0 ? Number(cfg.duration) : 2.5,
|
||||||
|
progressBar: cfg.progressBar !== false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this._loadingConfig = null;
|
||||||
|
}
|
||||||
|
if (thumbnail !== undefined) this._projectThumbnail = thumbnail || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Задача 05: стартовый экран загрузки при входе в Play (Ken-Burns + название места). */
|
||||||
|
showStartupLoadingScreen() {
|
||||||
|
const cfg = this._loadingConfig;
|
||||||
|
if (!cfg || cfg.enabled === false) return;
|
||||||
|
if (!this.gameRuntime) return;
|
||||||
|
try {
|
||||||
|
const ls = this.gameRuntime._ensureLoadingScreen?.();
|
||||||
|
if (!ls) return;
|
||||||
|
ls.show({
|
||||||
|
style: cfg.style,
|
||||||
|
background: cfg.background || cfg.cover || this._projectThumbnail,
|
||||||
|
cover: cfg.cover || this._projectThumbnail,
|
||||||
|
placeName: cfg.placeName || this._projectName || '',
|
||||||
|
studioName: cfg.studioName || '',
|
||||||
|
verified: cfg.verified,
|
||||||
|
duration: cfg.duration,
|
||||||
|
progressBar: cfg.progressBar,
|
||||||
|
spinner: true,
|
||||||
|
bgColor: '#070a14',
|
||||||
|
pauseSimulation: false,
|
||||||
|
blockInput: true,
|
||||||
|
});
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Переключить в режим игры. Создаём PlayerController, прячем ghost-блок,
|
* Переключить в режим игры. Создаём PlayerController, прячем ghost-блок,
|
||||||
* запоминаем позицию редактор-камеры чтобы вернуть при exit.
|
* запоминаем позицию редактор-камеры чтобы вернуть при exit.
|
||||||
@ -5526,6 +5576,8 @@ export class BabylonScene {
|
|||||||
// поэтому скрипты стартуем в следующем кадре.
|
// поэтому скрипты стартуем в следующем кадре.
|
||||||
this.gameRuntime = new GameRuntime(this);
|
this.gameRuntime = new GameRuntime(this);
|
||||||
try { this.modalManager?.attachRuntime?.(this.gameRuntime); } catch (e) {}
|
try { this.modalManager?.attachRuntime?.(this.gameRuntime); } catch (e) {}
|
||||||
|
// Задача 05: стартовый экран загрузки (Ken-Burns + название места).
|
||||||
|
try { this.showStartupLoadingScreen(); } catch (e) {}
|
||||||
// Аудио-менеджер (GD-музыка/SFX) — создаётся вместе с runtime.
|
// Аудио-менеджер (GD-музыка/SFX) — создаётся вместе с runtime.
|
||||||
// ВАЖНО: поле называется gameAudioManager чтобы не путать со штатным
|
// ВАЖНО: поле называется gameAudioManager чтобы не путать со штатным
|
||||||
// this.audioManager (AudioManager — ambient/music для всех проектов).
|
// this.audioManager (AudioManager — ambient/music для всех проектов).
|
||||||
@ -7431,15 +7483,9 @@ export class BabylonScene {
|
|||||||
} else {
|
} else {
|
||||||
this._skinsConfig = null;
|
this._skinsConfig = null;
|
||||||
}
|
}
|
||||||
// Задача 12: конфиг экрана загрузки.
|
// Задача 12+05: конфиг экрана загрузки (через setLoadingConfig — единый маппинг).
|
||||||
if (state.scene.loadingScreen && typeof state.scene.loadingScreen === 'object') {
|
if (state.scene.loadingScreen && typeof state.scene.loadingScreen === 'object') {
|
||||||
const ls = state.scene.loadingScreen;
|
this.setLoadingConfig(state.scene.loadingScreen);
|
||||||
this._loadingConfig = {
|
|
||||||
logo: ls.logo || null,
|
|
||||||
accentColor: ls.accentColor || '#ffc020',
|
|
||||||
defaultSpinner: ls.defaultSpinner !== false,
|
|
||||||
defaultSkipButton: !!ls.defaultSkipButton,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
this._loadingConfig = null;
|
this._loadingConfig = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -360,6 +360,8 @@ export class GameRuntime {
|
|||||||
ls.setBridge(
|
ls.setBridge(
|
||||||
(id) => { for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingSkip', loadingId: id }); },
|
(id) => { for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingSkip', loadingId: id }); },
|
||||||
(id) => { for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingComplete', loadingId: id }); },
|
(id) => { for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingComplete', loadingId: id }); },
|
||||||
|
// Задача 05: onHide.
|
||||||
|
() => { for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingHidden' }); },
|
||||||
);
|
);
|
||||||
this.scene3d.loadingScreen = ls;
|
this.scene3d.loadingScreen = ls;
|
||||||
}
|
}
|
||||||
@ -1742,9 +1744,9 @@ export class GameRuntime {
|
|||||||
if (ls && payload) {
|
if (ls && payload) {
|
||||||
try {
|
try {
|
||||||
const id = ls.show(payload.opts || {});
|
const id = ls.show(payload.opts || {});
|
||||||
if (payload.replyId != null) {
|
// replyId может отсутствовать (стартовый экран) — всё равно шлём
|
||||||
|
// loadingShown для game.loading.isVisible() (задача 05).
|
||||||
for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingShown', replyId: payload.replyId, loadingId: id });
|
for (const sb of this.sandboxes) sb.sendGlobalEvent({ type: 'loadingShown', replyId: payload.replyId, loadingId: id });
|
||||||
}
|
|
||||||
} catch (e) { this._log('error', 'loading.show: ' + (e?.message || e)); }
|
} catch (e) { this._log('error', 'loading.show: ' + (e?.message || e)); }
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -1752,6 +1754,7 @@ export class GameRuntime {
|
|||||||
if (cmd === 'loading.setProgress') { this.scene3d?.loadingScreen?.setProgress(payload?.value); return; }
|
if (cmd === 'loading.setProgress') { this.scene3d?.loadingScreen?.setProgress(payload?.value); return; }
|
||||||
if (cmd === 'loading.setText') { this.scene3d?.loadingScreen?.setText(payload?.text); return; }
|
if (cmd === 'loading.setText') { this.scene3d?.loadingScreen?.setText(payload?.text); return; }
|
||||||
if (cmd === 'loading.setCover') { this.scene3d?.loadingScreen?.setCover(payload?.cover); return; }
|
if (cmd === 'loading.setCover') { this.scene3d?.loadingScreen?.setCover(payload?.cover); return; }
|
||||||
|
if (cmd === 'loading.setBackground') { this.scene3d?.loadingScreen?.setBackground?.(payload?.background); return; }
|
||||||
if (cmd === 'loading.close') { this.scene3d?.loadingScreen?.close(); return; }
|
if (cmd === 'loading.close') { this.scene3d?.loadingScreen?.close(); return; }
|
||||||
|
|
||||||
// === Damage Floaters (задача 40) ===
|
// === Damage Floaters (задача 40) ===
|
||||||
|
|||||||
@ -35,7 +35,25 @@ function injectSpinnerCss() {
|
|||||||
style.textContent =
|
style.textContent =
|
||||||
'@keyframes kbn-ls-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
|
'@keyframes kbn-ls-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
|
||||||
'.kbn-ls-spinner{animation:kbn-ls-spin 0.9s linear infinite}' +
|
'.kbn-ls-spinner{animation:kbn-ls-spin 0.9s linear infinite}' +
|
||||||
'@media (prefers-reduced-motion:reduce){.kbn-ls-spinner{animation:none}}';
|
// Ken Burns — медленный pan+zoom фона (задача 05).
|
||||||
|
'@keyframes kbn-ls-kenburns{' +
|
||||||
|
'0%{transform:scale(1.0) translate3d(0,0,0)}' +
|
||||||
|
'50%{transform:scale(1.10) translate3d(-3%,-2%,0)}' +
|
||||||
|
'100%{transform:scale(1.0) translate3d(-6%,0,0)}}' +
|
||||||
|
'.kbn-ls-kenburns{animation:kbn-ls-kenburns 24s ease-in-out infinite}' +
|
||||||
|
// particles — медленно всплывающие искры.
|
||||||
|
'@keyframes kbn-ls-rise{' +
|
||||||
|
'0%{transform:translateY(0) scale(1);opacity:0}' +
|
||||||
|
'10%{opacity:0.9}' +
|
||||||
|
'90%{opacity:0.7}' +
|
||||||
|
'100%{transform:translateY(-110vh) scale(1.4);opacity:0}}' +
|
||||||
|
'.kbn-ls-particle{animation:kbn-ls-rise linear infinite}' +
|
||||||
|
// лёгкий «дыхательный» glow карточки-превью.
|
||||||
|
'@keyframes kbn-ls-cardglow{' +
|
||||||
|
'0%,100%{box-shadow:0 18px 60px rgba(0,0,0,0.6),0 0 0 rgba(120,160,255,0)}' +
|
||||||
|
'50%{box-shadow:0 18px 60px rgba(0,0,0,0.6),0 0 40px rgba(120,160,255,0.35)}}' +
|
||||||
|
'.kbn-ls-cardglow{animation:kbn-ls-cardglow 4s ease-in-out infinite}' +
|
||||||
|
'@media (prefers-reduced-motion:reduce){.kbn-ls-spinner,.kbn-ls-kenburns,.kbn-ls-particle,.kbn-ls-cardglow{animation:none}}';
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
@ -49,14 +67,17 @@ export class LoadingScreenOverlay {
|
|||||||
// Мост наружу (GameRuntime подписывает) — id-based колбэки.
|
// Мост наружу (GameRuntime подписывает) — id-based колбэки.
|
||||||
this._onSkipCb = null; // (id) => void
|
this._onSkipCb = null; // (id) => void
|
||||||
this._onCompleteCb = null; // (id) => void
|
this._onCompleteCb = null; // (id) => void
|
||||||
|
this._onHideCb = null; // () => void — задача 05 (game.loading.onHide)
|
||||||
|
this._parallaxHandler = null;
|
||||||
// DOM-ссылки активного экрана:
|
// DOM-ссылки активного экрана:
|
||||||
this._els = null;
|
this._els = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Колбэки-мост к GameRuntime (он рассылает globalEvent в sandbox'ы). */
|
/** Колбэки-мост к GameRuntime (он рассылает globalEvent в sandbox'ы). */
|
||||||
setBridge(onSkip, onComplete) {
|
setBridge(onSkip, onComplete, onHide) {
|
||||||
this._onSkipCb = onSkip;
|
this._onSkipCb = onSkip;
|
||||||
this._onCompleteCb = onComplete;
|
this._onCompleteCb = onComplete;
|
||||||
|
if (onHide) this._onHideCb = onHide;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Конфиг проекта (логотип/акцент по умолчанию) из BabylonScene._loadingConfig. */
|
/** Конфиг проекта (логотип/акцент по умолчанию) из BabylonScene._loadingConfig. */
|
||||||
@ -104,6 +125,15 @@ export class LoadingScreenOverlay {
|
|||||||
logoCornerRadius: opts.logoCornerRadius != null ? Number(opts.logoCornerRadius) : 12,
|
logoCornerRadius: opts.logoCornerRadius != null ? Number(opts.logoCornerRadius) : 12,
|
||||||
// Текст под картинкой
|
// Текст под картинкой
|
||||||
text: opts.text != null ? String(opts.text) : '',
|
text: opts.text != null ? String(opts.text) : '',
|
||||||
|
// --- Задача 05: Ken-Burns фон + карточка места ---
|
||||||
|
// style: 'ken-burns' | 'static' | 'parallax' | 'particles'
|
||||||
|
style: opts.style || cfg.style || 'ken-burns',
|
||||||
|
// фоновое размытое изображение (на весь экран); резолвится в _resolveCover.
|
||||||
|
background: opts.background != null ? opts.background : (cfg.background || null),
|
||||||
|
// карточка-витрина по центру (название места + автор), как в Roblox.
|
||||||
|
placeName: opts.placeName != null ? String(opts.placeName) : (cfg.placeName || ''),
|
||||||
|
studioName: opts.studioName != null ? String(opts.studioName) : (cfg.studioName || ''),
|
||||||
|
verified: opts.verified != null ? !!opts.verified : !!cfg.verified,
|
||||||
// Поведение
|
// Поведение
|
||||||
blockInput: opts.blockInput !== false,
|
blockInput: opts.blockInput !== false,
|
||||||
pauseSimulation: opts.pauseSimulation !== false,
|
pauseSimulation: opts.pauseSimulation !== false,
|
||||||
@ -163,20 +193,107 @@ export class LoadingScreenOverlay {
|
|||||||
// (используем opacity всего root для fade, а bgOpacity — через rgba фон):
|
// (используем opacity всего root для fade, а bgOpacity — через rgba фон):
|
||||||
root.style.background = this._bgRgba(st.bgColor, st.bgOpacity);
|
root.style.background = this._bgRgba(st.bgColor, st.bgOpacity);
|
||||||
|
|
||||||
// --- Cover (картинка по центру) ---
|
// --- Фоновый слой (Ken Burns / parallax / static) ---
|
||||||
|
// Размытое изображение игры на весь экран. Отдельный div под контентом,
|
||||||
|
// чтобы blur/анимация не трогали карточку и текст.
|
||||||
|
const bgUrl = this._resolveCover(st.background);
|
||||||
|
const bgLayer = document.createElement('div');
|
||||||
|
let bgClass = '';
|
||||||
|
if (bgUrl) {
|
||||||
|
if (st.style === 'ken-burns') bgClass = 'kbn-ls-kenburns';
|
||||||
|
bgLayer.className = bgClass;
|
||||||
|
bgLayer.style.cssText =
|
||||||
|
'position:absolute;inset:-8%;z-index:0;background-size:cover;background-position:center;' +
|
||||||
|
'filter:blur(8px) brightness(0.55);will-change:transform;' +
|
||||||
|
`background-image:url("${bgUrl}");`;
|
||||||
|
// parallax — лёгкий сдвиг по мыши (2 слоя имитируем одним + transform).
|
||||||
|
if (st.style === 'parallax') {
|
||||||
|
bgLayer.style.transition = 'transform 0.25s ease-out';
|
||||||
|
this._parallaxHandler = (e) => {
|
||||||
|
const cx = (e.clientX / window.innerWidth - 0.5) * 28;
|
||||||
|
const cy = (e.clientY / window.innerHeight - 0.5) * 18;
|
||||||
|
bgLayer.style.transform = `translate3d(${-cx}px,${-cy}px,0) scale(1.06)`;
|
||||||
|
};
|
||||||
|
window.addEventListener('mousemove', this._parallaxHandler);
|
||||||
|
}
|
||||||
|
root.appendChild(bgLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- particles слой (медленные искры) ---
|
||||||
|
if (st.style === 'particles') {
|
||||||
|
const pLayer = document.createElement('div');
|
||||||
|
pLayer.style.cssText = 'position:absolute;inset:0;z-index:1;overflow:hidden;pointer-events:none;';
|
||||||
|
for (let i = 0; i < 26; i++) {
|
||||||
|
const sp = document.createElement('span');
|
||||||
|
sp.className = 'kbn-ls-particle';
|
||||||
|
const size = 2 + Math.round(Math.random() * 4);
|
||||||
|
const dur = 7 + Math.random() * 10;
|
||||||
|
sp.style.cssText =
|
||||||
|
`position:absolute;bottom:-10px;left:${(Math.random() * 100).toFixed(1)}%;` +
|
||||||
|
`width:${size}px;height:${size}px;border-radius:50%;` +
|
||||||
|
`background:rgba(${180 + Math.round(Math.random() * 70)},${190 + Math.round(Math.random() * 60)},255,0.85);` +
|
||||||
|
`box-shadow:0 0 ${size * 2}px rgba(140,170,255,0.7);` +
|
||||||
|
`animation-duration:${dur.toFixed(1)}s;animation-delay:${(-Math.random() * dur).toFixed(1)}s;`;
|
||||||
|
pLayer.appendChild(sp);
|
||||||
|
}
|
||||||
|
root.appendChild(pLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обёртка контента (над фоном).
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.style.cssText =
|
||||||
|
'position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;';
|
||||||
|
|
||||||
|
// --- Cover (картинка-карточка по центру) ---
|
||||||
const coverUrl = this._resolveCover(cover);
|
const coverUrl = this._resolveCover(cover);
|
||||||
|
// Режим карточки места (задача 05): квадрат + название + автор под ней.
|
||||||
|
const hasPlaceCard = !!(st.placeName || st.studioName);
|
||||||
const coverImg = document.createElement('div');
|
const coverImg = document.createElement('div');
|
||||||
|
if (hasPlaceCard) {
|
||||||
|
coverImg.className = 'kbn-ls-cardglow';
|
||||||
|
coverImg.style.cssText =
|
||||||
|
'width:min(42vw,400px);aspect-ratio:1/1;border-radius:18px;' +
|
||||||
|
'background-size:cover;background-position:center;background-color:#1a1f2b;' +
|
||||||
|
'border:2px solid rgba(255,255,255,0.12);';
|
||||||
|
} else {
|
||||||
coverImg.style.cssText =
|
coverImg.style.cssText =
|
||||||
'max-width:min(62vw,860px);width:62vw;aspect-ratio:16/9;border-radius:16px;' +
|
'max-width:min(62vw,860px);width:62vw;aspect-ratio:16/9;border-radius:16px;' +
|
||||||
'box-shadow:0 18px 60px rgba(0,0,0,0.6);background-size:cover;background-position:center;' +
|
'box-shadow:0 18px 60px rgba(0,0,0,0.6);background-size:cover;background-position:center;' +
|
||||||
'background-color:#1a1f2b;margin-bottom:140px;';
|
'background-color:#1a1f2b;margin-bottom:140px;';
|
||||||
|
}
|
||||||
if (coverUrl) coverImg.style.backgroundImage = `url("${coverUrl}")`;
|
if (coverUrl) coverImg.style.backgroundImage = `url("${coverUrl}")`;
|
||||||
|
else if (bgUrl && hasPlaceCard) coverImg.style.backgroundImage = `url("${bgUrl}")`;
|
||||||
|
|
||||||
// --- Текст под картинкой ---
|
// --- Название места (крупный белый, под карточкой) ---
|
||||||
|
const placeEl = document.createElement('div');
|
||||||
|
placeEl.style.cssText =
|
||||||
|
'margin-top:22px;color:#fff;font-size:38px;font-weight:800;letter-spacing:0.5px;' +
|
||||||
|
'text-align:center;text-shadow:0 3px 14px rgba(0,0,0,0.7);' +
|
||||||
|
(st.placeName ? '' : 'display:none;');
|
||||||
|
placeEl.textContent = st.placeName || '';
|
||||||
|
|
||||||
|
// --- Автор + verified-галочка ---
|
||||||
|
const studioRow = document.createElement('div');
|
||||||
|
studioRow.style.cssText =
|
||||||
|
'margin-top:8px;display:flex;align-items:center;gap:7px;' +
|
||||||
|
'color:#cdd6e6;font-size:16px;font-weight:600;text-shadow:0 1px 4px rgba(0,0,0,0.6);' +
|
||||||
|
(st.studioName ? '' : 'display:none;');
|
||||||
|
const studioTxt = document.createElement('span');
|
||||||
|
studioTxt.textContent = st.studioName || '';
|
||||||
|
studioRow.appendChild(studioTxt);
|
||||||
|
if (st.verified) studioRow.appendChild(this._buildVerifiedBadge());
|
||||||
|
|
||||||
|
// --- Текст под картинкой (для не-карточного режима / mid-game) ---
|
||||||
const textEl = document.createElement('div');
|
const textEl = document.createElement('div');
|
||||||
|
if (hasPlaceCard) {
|
||||||
|
textEl.style.cssText =
|
||||||
|
'margin-top:14px;color:#e8edf5;font-size:16px;font-weight:500;text-align:center;' +
|
||||||
|
'text-shadow:0 1px 3px rgba(0,0,0,0.6);' + (st.text ? '' : 'display:none;');
|
||||||
|
} else {
|
||||||
textEl.style.cssText =
|
textEl.style.cssText =
|
||||||
'position:absolute;left:50%;transform:translateX(-50%);bottom:170px;' +
|
'position:absolute;left:50%;transform:translateX(-50%);bottom:170px;' +
|
||||||
'color:#e8edf5;font-size:18px;font-weight:500;text-align:center;text-shadow:0 1px 3px rgba(0,0,0,0.6);';
|
'color:#e8edf5;font-size:18px;font-weight:500;text-align:center;text-shadow:0 1px 3px rgba(0,0,0,0.6);';
|
||||||
|
}
|
||||||
textEl.textContent = st.text || '';
|
textEl.textContent = st.text || '';
|
||||||
|
|
||||||
// --- Прогресс-бар ---
|
// --- Прогресс-бар ---
|
||||||
@ -245,8 +362,13 @@ export class LoadingScreenOverlay {
|
|||||||
spinWrap.appendChild(spinTxt);
|
spinWrap.appendChild(spinTxt);
|
||||||
spinWrap.appendChild(spinCircle);
|
spinWrap.appendChild(spinCircle);
|
||||||
|
|
||||||
root.appendChild(coverImg);
|
// Центральная композиция (карточка + название + автор + текст) — в content.
|
||||||
root.appendChild(textEl);
|
content.appendChild(coverImg);
|
||||||
|
content.appendChild(placeEl);
|
||||||
|
content.appendChild(studioRow);
|
||||||
|
content.appendChild(textEl);
|
||||||
|
root.appendChild(content);
|
||||||
|
// Нижние/угловые элементы — абсолютно на root (поверх фона, рядом с content).
|
||||||
root.appendChild(barWrap);
|
root.appendChild(barWrap);
|
||||||
root.appendChild(percent);
|
root.appendChild(percent);
|
||||||
root.appendChild(skipBtn);
|
root.appendChild(skipBtn);
|
||||||
@ -255,7 +377,19 @@ export class LoadingScreenOverlay {
|
|||||||
parent.appendChild(root);
|
parent.appendChild(root);
|
||||||
|
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this._els = { root, coverImg, textEl, barWrap, bar, percent, skipBtn, logo, spinWrap };
|
this._els = { root, content, coverImg, placeEl, studioRow, textEl, barWrap, bar, percent, skipBtn, logo, spinWrap };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verified-галочка (синий круг + белый чек) — inline SVG, без emoji-шрифта. */
|
||||||
|
_buildVerifiedBadge() {
|
||||||
|
const wrap = document.createElement('span');
|
||||||
|
wrap.style.cssText = 'display:inline-flex;align-items:center;';
|
||||||
|
wrap.innerHTML =
|
||||||
|
'<svg width="18" height="18" viewBox="0 0 24 24" aria-label="verified">' +
|
||||||
|
'<circle cx="12" cy="12" r="11" fill="#3897f0"/>' +
|
||||||
|
'<path d="M7 12.5l3 3 7-7" fill="none" stroke="#fff" stroke-width="2.4" ' +
|
||||||
|
'stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||||
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** tick — fade-фазы, авто-duration, появление кнопки Пропустить. */
|
/** tick — fade-фазы, авто-duration, появление кнопки Пропустить. */
|
||||||
@ -329,6 +463,23 @@ export class LoadingScreenOverlay {
|
|||||||
if (url) this._els.coverImg.style.backgroundImage = `url("${url}")`;
|
if (url) this._els.coverImg.style.backgroundImage = `url("${url}")`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Задача 05: сменить фоновое (Ken-Burns) изображение на лету. */
|
||||||
|
setBackground(bg) {
|
||||||
|
if (!this._st || !this._els) return;
|
||||||
|
const url = this._resolveCover(bg);
|
||||||
|
if (!url) return;
|
||||||
|
this._st.background = bg;
|
||||||
|
// фоновый слой — первый ребёнок root с background-image; найдём его.
|
||||||
|
const layer = this._els.root.querySelector('.kbn-ls-kenburns')
|
||||||
|
|| this._els.root.firstElementChild;
|
||||||
|
if (layer && layer !== this._els.content) layer.style.backgroundImage = `url("${url}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Задача 05: виден ли экран сейчас. */
|
||||||
|
isVisible() {
|
||||||
|
return !!(this._st && this._st.phase !== 'out');
|
||||||
|
}
|
||||||
|
|
||||||
/** Закрыть программно (с fadeOut). */
|
/** Закрыть программно (с fadeOut). */
|
||||||
close() {
|
close() {
|
||||||
const st = this._st;
|
const st = this._st;
|
||||||
@ -361,6 +512,13 @@ export class LoadingScreenOverlay {
|
|||||||
if (st.blockInput) { try { this.s.player?.setInputBlocked?.(false); } catch { /* ignore */ } }
|
if (st.blockInput) { try { this.s.player?.setInputBlocked?.(false); } catch { /* ignore */ } }
|
||||||
if (st.pauseSimulation && this.s.gameRuntime) { try { this.s.gameRuntime.paused = false; } catch { /* ignore */ } }
|
if (st.pauseSimulation && this.s.gameRuntime) { try { this.s.gameRuntime.paused = false; } catch { /* ignore */ } }
|
||||||
}
|
}
|
||||||
|
// Снять parallax-listener (задача 05).
|
||||||
|
if (this._parallaxHandler) {
|
||||||
|
try { window.removeEventListener('mousemove', this._parallaxHandler); } catch { /* ignore */ }
|
||||||
|
this._parallaxHandler = null;
|
||||||
|
}
|
||||||
|
// onHide-мост (задача 05) — сообщаем скриптам что экран скрылся.
|
||||||
|
if (this._onHideCb) { try { this._onHideCb(); } catch { /* ignore */ } }
|
||||||
if (this.root) { try { this.root.remove(); } catch { /* ignore */ } }
|
if (this.root) { try { this.root.remove(); } catch { /* ignore */ } }
|
||||||
this.root = null;
|
this.root = null;
|
||||||
this._els = null;
|
this._els = null;
|
||||||
|
|||||||
@ -70,6 +70,8 @@ let _placeOnPlaceHandlers = [];
|
|||||||
let _placeOnCancelHandlers = [];
|
let _placeOnCancelHandlers = [];
|
||||||
let _placeOnMoveHandlers = [];
|
let _placeOnMoveHandlers = [];
|
||||||
let _invUiSlotClickHandlers = [];
|
let _invUiSlotClickHandlers = [];
|
||||||
|
// Задача 05: зеркало видимости экрана загрузки (для game.loading.isVisible()).
|
||||||
|
let _loadingVisible = false;
|
||||||
// Мультиплеер (Фаза 4.3): снимок игроков комнаты { me, list }.
|
// Мультиплеер (Фаза 4.3): снимок игроков комнаты { me, list }.
|
||||||
let _players = { me: null, list: [] };
|
let _players = { me: null, list: [] };
|
||||||
// Общее состояние комнаты game.room.get/set — зеркало из main thread.
|
// Общее состояние комнаты game.room.get/set — зеркало из main thread.
|
||||||
@ -2727,6 +2729,7 @@ const game = {
|
|||||||
_localSeq: 0,
|
_localSeq: 0,
|
||||||
_localToReal: new Map(), // localId → реальный loadingId (приходит в loadingShown)
|
_localToReal: new Map(), // localId → реальный loadingId (приходит в loadingShown)
|
||||||
_handlers: new Map(), // localId → { onSkip:[], onComplete:[] }
|
_handlers: new Map(), // localId → { onSkip:[], onComplete:[] }
|
||||||
|
_onHide: [], // задача 05 — глобальные подписки на скрытие
|
||||||
show(opts) {
|
show(opts) {
|
||||||
opts = opts && typeof opts === 'object' ? opts : {};
|
opts = opts && typeof opts === 'object' ? opts : {};
|
||||||
const localId = ++this._localSeq;
|
const localId = ++this._localSeq;
|
||||||
@ -2745,11 +2748,20 @@ const game = {
|
|||||||
setProgress(v) { _send('loading.setProgress', { localId, value: Number(v) || 0 }); },
|
setProgress(v) { _send('loading.setProgress', { localId, value: Number(v) || 0 }); },
|
||||||
setText(t) { _send('loading.setText', { localId, text: String(t == null ? '' : t) }); },
|
setText(t) { _send('loading.setText', { localId, text: String(t == null ? '' : t) }); },
|
||||||
setCover(c) { _send('loading.setCover', { localId, cover: c }); },
|
setCover(c) { _send('loading.setCover', { localId, cover: c }); },
|
||||||
|
setBackground(b) { _send('loading.setBackground', { localId, background: b }); },
|
||||||
close() { _send('loading.close', { localId }); },
|
close() { _send('loading.close', { localId }); },
|
||||||
onSkip(fn) { if (typeof fn === 'function') self._handlers.get(localId)?.onSkip.push(fn); },
|
onSkip(fn) { if (typeof fn === 'function') self._handlers.get(localId)?.onSkip.push(fn); },
|
||||||
onComplete(fn) { if (typeof fn === 'function') self._handlers.get(localId)?.onComplete.push(fn); },
|
onComplete(fn) { if (typeof fn === 'function') self._handlers.get(localId)?.onComplete.push(fn); },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
// --- Задача 05: управление активным экраном без хэндла (стартовый/любой текущий) ---
|
||||||
|
onHide(fn) { if (typeof fn === 'function') this._onHide.push(fn); },
|
||||||
|
setBackground(b) { _send('loading.setBackground', { background: b }); },
|
||||||
|
setText(t) { _send('loading.setText', { text: String(t == null ? '' : t) }); },
|
||||||
|
setCover(c) { _send('loading.setCover', { cover: c }); },
|
||||||
|
setProgress(v) { _send('loading.setProgress', { value: Number(v) || 0 }); },
|
||||||
|
hide() { _send('loading.close', {}); },
|
||||||
|
isVisible() { return !!_loadingVisible; },
|
||||||
/** Типовой переход с фейковым прогрессом. Возвращает Promise (резолв по complete/skip). */
|
/** Типовой переход с фейковым прогрессом. Возвращает Promise (резолв по complete/skip). */
|
||||||
transition(opts) {
|
transition(opts) {
|
||||||
opts = opts && typeof opts === 'object' ? { ...opts } : {};
|
opts = opts && typeof opts === 'object' ? { ...opts } : {};
|
||||||
@ -4122,6 +4134,7 @@ self.onmessage = (e) => {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
} else if (t === 'loadingShown') {
|
} else if (t === 'loadingShown') {
|
||||||
// Задача 12: реальный loadingId от runtime — маппим local→real.
|
// Задача 12: реальный loadingId от runtime — маппим local→real.
|
||||||
|
_loadingVisible = true;
|
||||||
try {
|
try {
|
||||||
const lo = (typeof game !== 'undefined') && game.loading;
|
const lo = (typeof game !== 'undefined') && game.loading;
|
||||||
if (lo && payload && payload.replyId) {
|
if (lo && payload && payload.replyId) {
|
||||||
@ -4131,6 +4144,13 @@ self.onmessage = (e) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
} else if (t === 'loadingHidden') {
|
||||||
|
// Задача 05: экран скрылся — зеркало + onHide-подписки.
|
||||||
|
_loadingVisible = false;
|
||||||
|
try {
|
||||||
|
const lo = (typeof game !== 'undefined') && game.loading;
|
||||||
|
if (lo) for (const fn of (lo._onHide || [])) _safeCall(fn, undefined, 'loading.onHide');
|
||||||
|
} catch (e) {}
|
||||||
} else if (t === 'loadingSkip' || t === 'loadingComplete') {
|
} else if (t === 'loadingSkip' || t === 'loadingComplete') {
|
||||||
// Игрок нажал «Пропустить» или прогресс достиг 100% — зовём подписчиков.
|
// Игрок нажал «Пропустить» или прогресс достиг 100% — зовём подписчиков.
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user