feat(13): ������� ���� ���� (game.mainMenu) #18
@ -156,6 +156,7 @@ export class BabylonScene {
|
|||||||
this.loadingScreen = null;
|
this.loadingScreen = null;
|
||||||
this._LoadingScreenOverlayClass = LoadingScreenOverlay;
|
this._LoadingScreenOverlayClass = LoadingScreenOverlay;
|
||||||
this._loadingConfig = null;
|
this._loadingConfig = null;
|
||||||
|
this._mainMenuConfig = null; // задача 13
|
||||||
this._projectThumbnail = null;
|
this._projectThumbnail = null;
|
||||||
this.spawnerManager = null; // спавнеры зомби
|
this.spawnerManager = null; // спавнеры зомби
|
||||||
this.environment = null;
|
this.environment = null;
|
||||||
@ -7349,6 +7350,9 @@ export class BabylonScene {
|
|||||||
} else {
|
} else {
|
||||||
this._loadingConfig = null;
|
this._loadingConfig = null;
|
||||||
}
|
}
|
||||||
|
// Задача 13: конфиг главного меню (passthrough).
|
||||||
|
this._mainMenuConfig = (state.scene.mainMenu && typeof state.scene.mainMenu === 'object')
|
||||||
|
? state.scene.mainMenu : null;
|
||||||
|
|
||||||
// ОПТИМИЗАЦИЯ: предзагружаем модель ИГРОКА (character) тоже —
|
// ОПТИМИЗАЦИЯ: предзагружаем модель ИГРОКА (character) тоже —
|
||||||
// PlayerController.start() её ждёт, но если предзагрузить сейчас,
|
// PlayerController.start() её ждёт, но если предзагрузить сейчас,
|
||||||
|
|||||||
@ -2313,6 +2313,11 @@ export class GameRuntime {
|
|||||||
this.scene3d?.player?.cameraReset?.();
|
this.scene3d?.player?.cameraReset?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cmd === 'player.setInputBlocked') {
|
||||||
|
// Задача 13: блок управления (главное меню — игрок наблюдатель).
|
||||||
|
this.scene3d?.player?.setInputBlocked?.(!!payload?.blocked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (cmd === 'player.setSkinVisible') {
|
if (cmd === 'player.setSkinVisible') {
|
||||||
const player = this.scene3d?.player;
|
const player = this.scene3d?.player;
|
||||||
if (player) {
|
if (player) {
|
||||||
|
|||||||
@ -733,6 +733,10 @@ const game = {
|
|||||||
_send('player.teleport', { x: nx, y: ny, z: nz });
|
_send('player.teleport', { x: nx, y: ny, z: nz });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/** Заблокировать/разблокировать управление игроком (WASD/прыжок). */
|
||||||
|
setInputBlocked(blocked) {
|
||||||
|
_send('player.setInputBlocked', { blocked: !!blocked });
|
||||||
|
},
|
||||||
/** Сдвинуть игрока ТОЛЬКО по X (не трогая Z и Y). Для раннеров —
|
/** Сдвинуть игрока ТОЛЬКО по X (не трогая Z и Y). Для раннеров —
|
||||||
* смена полосы без отмены продвижения autorun. */
|
* смена полосы без отмены продвижения autorun. */
|
||||||
setLaneX(x) {
|
setLaneX(x) {
|
||||||
@ -2196,6 +2200,140 @@ const game = {
|
|||||||
_send('camera.reset', {});
|
_send('camera.reset', {});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Главное меню игры (задача 13) — оркестратор поверх camera.cutscene + gui
|
||||||
|
* + audio + loading. Живая 3D-сцена + cinematic-камера + GUI + музыка;
|
||||||
|
* клик ИГРАТЬ → переход в игру.
|
||||||
|
*/
|
||||||
|
mainMenu: {
|
||||||
|
_onShow: [], _onPlay: [], _onHide: [],
|
||||||
|
_active: false,
|
||||||
|
_curMusic: null,
|
||||||
|
_opts: null,
|
||||||
|
_camCfg: null,
|
||||||
|
_loopArmed: false,
|
||||||
|
show(opts) {
|
||||||
|
opts = opts && typeof opts === 'object' ? opts : {};
|
||||||
|
this._opts = opts;
|
||||||
|
this._active = true;
|
||||||
|
_send('player.setInputBlocked', { blocked: true });
|
||||||
|
game.hud.setVisible(false);
|
||||||
|
this._camCfg = opts.camera || { mode: 'orbit', center: { x: 0, y: 1, z: 0 }, radius: 6, height: 2, duration: 12 };
|
||||||
|
this.setCamera(this._camCfg);
|
||||||
|
if (!this._loopArmed) {
|
||||||
|
this._loopArmed = true;
|
||||||
|
const self = this;
|
||||||
|
game.onCutsceneDone(() => {
|
||||||
|
if (self._active && self._camCfg) self.setCamera(self._camCfg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._buildGui(opts);
|
||||||
|
if (opts.music && typeof opts.music === 'string') {
|
||||||
|
this._curMusic = opts.music;
|
||||||
|
_send('audio.playMusic', { trackId: opts.music });
|
||||||
|
}
|
||||||
|
for (const fn of this._onShow) _safeCall(fn, undefined, 'mainMenu.onShow');
|
||||||
|
},
|
||||||
|
setCamera(cam) {
|
||||||
|
cam = cam && typeof cam === 'object' ? cam : {};
|
||||||
|
if (this._active) this._camCfg = cam;
|
||||||
|
const mode = cam.mode || 'orbit';
|
||||||
|
const dur = Number.isFinite(Number(cam.duration)) ? Number(cam.duration) : 12;
|
||||||
|
if (mode === 'static') {
|
||||||
|
const p = cam.position || { x: 0, y: 5, z: 8 };
|
||||||
|
const t = cam.target || { x: 0, y: 1, z: 0 };
|
||||||
|
_send('camera.cutscene', { points: [p, p], lookAt: [t, t], segDuration: 9999 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === 'orbit') {
|
||||||
|
const c = cam.center || { x: 0, y: 1, z: 0 };
|
||||||
|
const r = Number.isFinite(Number(cam.radius)) ? Number(cam.radius) : 6;
|
||||||
|
const h = Number.isFinite(Number(cam.height)) ? Number(cam.height) : 2;
|
||||||
|
const N = 16;
|
||||||
|
const pts = [], looks = [];
|
||||||
|
for (let i = 0; i <= N; i++) {
|
||||||
|
const a = (i / N) * Math.PI * 2;
|
||||||
|
pts.push({ x: c.x + Math.cos(a) * r, y: c.y + h, z: c.z + Math.sin(a) * r });
|
||||||
|
looks.push({ x: c.x, y: c.y, z: c.z });
|
||||||
|
}
|
||||||
|
_send('camera.cutscene', { points: pts, lookAt: looks, segDuration: dur / N });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mode === 'preset-cuts') {
|
||||||
|
const cuts = Array.isArray(cam.cuts) ? cam.cuts : [];
|
||||||
|
if (cuts.length < 1) return;
|
||||||
|
const pts = [], looks = [];
|
||||||
|
for (const c of cuts) {
|
||||||
|
const p = c.position || { x: 0, y: 3, z: 6 };
|
||||||
|
const t = c.target || { x: 0, y: 1, z: 0 };
|
||||||
|
pts.push(p, p); looks.push(t, t);
|
||||||
|
}
|
||||||
|
_send('camera.cutscene', { points: pts, lookAt: looks, segDuration: (cuts[0].duration || 2) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const wps = Array.isArray(cam.waypoints) ? cam.waypoints : [];
|
||||||
|
if (wps.length < 2) return;
|
||||||
|
const pts = wps.map(w => w.position || { x: 0, y: 2, z: 0 });
|
||||||
|
const looks = wps.map(w => w.target || { x: 0, y: 1, z: 0 });
|
||||||
|
_send('camera.cutscene', { points: pts, lookAt: looks, segDuration: dur / Math.max(1, pts.length - 1) });
|
||||||
|
},
|
||||||
|
setPatchNotes(pn) {
|
||||||
|
pn = pn && typeof pn === 'object' ? pn : {};
|
||||||
|
const title = (pn.title || '') + (pn.version ? ' (' + pn.version + '):' : ':');
|
||||||
|
const items = Array.isArray(pn.items) ? pn.items : [];
|
||||||
|
game.gui.create('text', {
|
||||||
|
id: '_mm_pn_title', x: 78, y: 28, w: 40, h: 6, anchor: 'center',
|
||||||
|
text: title, textColor: '#ffffff', textSize: 26, fontWeight: 900,
|
||||||
|
textStroke: { color: '#000', width: 3 }, bgColor: 'transparent', bgOpacity: 0,
|
||||||
|
});
|
||||||
|
items.slice(0, 7).forEach((it, i) => {
|
||||||
|
game.gui.create('text', {
|
||||||
|
id: '_mm_pn_' + i, x: 78, y: 36 + i * 6, w: 40, h: 5, anchor: 'center',
|
||||||
|
text: '- ' + it, textColor: '#e8edf5', textSize: 20, fontWeight: 700,
|
||||||
|
textStroke: { color: '#000', width: 2 }, bgColor: 'transparent', bgOpacity: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
_buildGui(opts) {
|
||||||
|
if (opts.title) {
|
||||||
|
game.gui.create('text', {
|
||||||
|
id: '_mm_logo', x: 30, y: 12, w: 56, h: 12, anchor: 'center',
|
||||||
|
text: String(opts.title), textColor: '#ffd23a', textSize: 48, fontWeight: 900,
|
||||||
|
textStroke: { color: '#000', width: 4 }, bgColor: 'transparent', bgOpacity: 0,
|
||||||
|
animationPreset: 'glow',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (opts.patchNotes) this.setPatchNotes(opts.patchNotes);
|
||||||
|
game.gui.create('button', {
|
||||||
|
id: '_mm_play', x: 84, y: 90, w: 28, h: 11, anchor: 'center',
|
||||||
|
text: opts.playButtonText || 'ИГРАТЬ',
|
||||||
|
bgGradient: { stops: ['#ffe066', '#e0a000'], angle: 90 },
|
||||||
|
textColor: '#3a2a00', textSize: 30, fontWeight: 900, borderRadius: 14,
|
||||||
|
textStroke: { color: '#fff7d0', width: 1 },
|
||||||
|
hover: { scale: 1.06, brightness: 1.1 }, active: { scale: 0.96 },
|
||||||
|
});
|
||||||
|
const self = this;
|
||||||
|
game.gui.onClick('_mm_play', () => {
|
||||||
|
for (const fn of self._onPlay) _safeCall(fn, undefined, 'mainMenu.onPlay');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hide() {
|
||||||
|
if (!this._active) return;
|
||||||
|
this._active = false;
|
||||||
|
this._camCfg = null;
|
||||||
|
const ids = ['_mm_logo', '_mm_play', '_mm_pn_title', '_mm_pn_0', '_mm_pn_1', '_mm_pn_2', '_mm_pn_3', '_mm_pn_4', '_mm_pn_5', '_mm_pn_6'];
|
||||||
|
for (const id of ids) { try { game.gui.remove(id); } catch (e) {} }
|
||||||
|
if (this._curMusic) { _send('audio.stopMusic', {}); this._curMusic = null; }
|
||||||
|
_send('camera.reset', {});
|
||||||
|
_send('player.setInputBlocked', { blocked: false });
|
||||||
|
game.hud.setVisible(true);
|
||||||
|
for (const fn of this._onHide) _safeCall(fn, undefined, 'mainMenu.onHide');
|
||||||
|
},
|
||||||
|
onShow(fn) { if (typeof fn === 'function') this._onShow.push(fn); },
|
||||||
|
onPlay(fn) { if (typeof fn === 'function') this._onPlay.push(fn); },
|
||||||
|
onHide(fn) { if (typeof fn === 'function') this._onHide.push(fn); },
|
||||||
|
isActive() { return this._active; },
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Управление стандартным HUD движка (HP-бар, hotbar, кнопка меню, чат).
|
* Управление стандартным HUD движка (HP-бар, hotbar, кнопка меню, чат).
|
||||||
* Нужно для игр которые делают свой UI через game.gui.* и не хотят
|
* Нужно для игр которые делают свой UI через game.gui.* и не хотят
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user