Compare commits

..

7 Commits

Author SHA1 Message Date
min
4fe86ee723 Merge pull request 'feat(player): �� ���������� fullscreen-������� � �������� ���������� (APK/desktop)' (#34) from feat/player-native-app-no-fs-prompt-2026-06-15 into main
All checks were successful
CI / Lint (push) Successful in 58s
CI / Build (push) Successful in 1m32s
CI / Secret scan (push) Successful in 21s
CI / PR size check (push) Has been skipped
CI / Deploy to S1 + S2 (push) Successful in 3m1s
2026-06-15 19:25:05 +00:00
min
86620eee1c feat(player): не показывать fullscreen-оверлей в нативном приложении
All checks were successful
CI / Lint (pull_request) Successful in 56s
CI / Build (pull_request) Successful in 1m36s
CI / Secret scan (pull_request) Successful in 24s
CI / PR size check (pull_request) Successful in 7s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
В Android-приложении (Capacitor) и десктопе (Electron) WebView/окно уже
на весь экран — стартовый оверлей «Нажми чтобы играть» избыточен (в
браузере он нужен для user-gesture перед fullscreen, в нативе барьера нет).
Теперь в нативном приложении игра запускается сразу:
- IS_ANDROID_APP: детект по window.Capacitor + метке RubloxAndroid в UA;
- IS_NATIVE_APP = desktop || android;
- gameStarted инициализируется true в нативе → оверлей пропускается;
- handleGameStart/handleMobileStart не дёргают requestFullscreen в нативе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:14:26 +03:00
min
e0f5ac9a29 Merge pull request 'feat(player): �� �������� fullscreen � �������-����������' (#33) from feat/player-desktop-no-fullscreen-2026-06-15 into main
All checks were successful
CI / Lint (push) Successful in 1m2s
CI / Build (push) Successful in 1m34s
CI / Secret scan (push) Successful in 21s
CI / PR size check (push) Has been skipped
CI / Deploy to S1 + S2 (push) Successful in 3m0s
2026-06-15 17:27:43 +00:00
min
f77a741428 feat(player): не включать fullscreen в десктоп-приложении
All checks were successful
CI / Lint (pull_request) Successful in 57s
CI / Build (pull_request) Successful in 1m31s
CI / Secret scan (pull_request) Successful in 20s
CI / PR size check (pull_request) Successful in 6s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
В Electron-обёртке (rublox-desktop) окно уже на весь экран без браузерной
панели и вкладок — fullscreen не нужен (раньше защищал от Ctrl+W/Ctrl+T,
в десктопе этого риска нет). По флагу window.__RUBLOX_DESKTOP__ (его ставит
preload Electron):
- handleGameStart/handleMobileStart не вызывают requestFullscreen;
- стартовый текст «Нажми чтобы играть» без упоминания FS (показывает
  управление WASD);
- пункт «Полноэкранный режим» в меню игры скрыт.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:17:27 +03:00
min
f2b74a2597 fix(skin): ��������� ����� � ��������� ������� (#32)
All checks were successful
CI / Lint (push) Successful in 52s
CI / Build (push) Successful in 1m28s
CI / Secret scan (push) Successful in 20s
CI / PR size check (push) Has been skipped
CI / Deploy to S1 + S2 (push) Successful in 2m59s
2026-06-15 05:21:27 +00:00
min
3754ecf4a1 fix(skin): валидация скина в карточках игроков (legacy bacon → y-bot)
All checks were successful
CI / Lint (pull_request) Successful in 52s
CI / Build (pull_request) Successful in 1m28s
CI / Secret scan (pull_request) Successful in 20s
CI / PR size check (pull_request) Successful in 7s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
В меню «На сервере» аватарка бралась по player.skin. Если в БД legacy
skin_bacon-hair (которого нет) — показывался бекон. Теперь невалидный
скин (не в MIXAMO_SKINS) → аватар skin_y-bot, как и 3D-модель в игре.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 06:46:58 +03:00
min
b2b0eab546 feat(anim): 3-������ ������ + ������������ �������� (#31)
All checks were successful
CI / Lint (push) Successful in 54s
CI / Build (push) Successful in 1m30s
CI / Secret scan (push) Successful in 27s
CI / PR size check (push) Has been skipped
CI / Deploy to S1 + S2 (push) Successful in 2m58s
2026-06-14 21:31:32 +00:00
2 changed files with 58 additions and 21 deletions

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef, useCallback } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import Icon from '../editor-shared/Icon'; import Icon from '../editor-shared/Icon';
import { STORYS_addres, USER_addres } from '../api/API'; import { STORYS_addres, USER_addres } from '../api/API';
import { MIXAMO_SKINS } from '../engine/PlayerController';
const getToken = () => { const getToken = () => {
try { try {
@ -43,6 +44,10 @@ const HUD = {
font: '"Inter", system-ui, -apple-system, sans-serif', font: '"Inter", system-ui, -apple-system, sans-serif',
}; };
// В десктоп-приложении (Electron) пункт «Полноэкранный режим» не нужен
// окно и так на весь экран. preload выставляет window.__RUBLOX_DESKTOP__.
const IS_DESKTOP_APP = typeof window !== 'undefined' && !!window.__RUBLOX_DESKTOP__;
const TABS = [ const TABS = [
{ id: 'people', icon: 'users', title: 'Участники' }, { id: 'people', icon: 'users', title: 'Участники' },
{ id: 'settings', icon: 'settings', title: 'Настройки' }, { id: 'settings', icon: 'settings', title: 'Настройки' },
@ -608,19 +613,18 @@ function PlayerCard({ player, isMe, isFriend, isPending, onAddFriend }) {
let avatarUrl = null; let avatarUrl = null;
let isSkin = false; let isSkin = false;
if (player.skin && typeof player.skin === 'string') { if (player.skin && typeof player.skin === 'string') {
const isLegacy = LEGACY_SKINS.has(player.skin); // ВАЛИДАЦИЯ: legacy R15-скины (bacon-hair и пр.) больше не существуют.
if (!isLegacy && player.skin.startsWith('skin_')) { // Если скин НЕ в наборе валидных Mixamo (80 шт) показываем аватар
// Mixamo: PNG на rublox-site (на проде rublox.pro, // дефолтного skin_y-bot (как и в самой игре 3D-модель валидируется).
// локально localhost:3000) рядом с GLB. let skinId = player.skin;
if (!MIXAMO_SKINS.has(skinId) && !skinId.startsWith('customskin:')) {
skinId = 'skin_y-bot';
}
const base = (typeof window !== 'undefined' const base = (typeof window !== 'undefined'
&& window.location.hostname === 'localhost') && window.location.hostname === 'localhost')
? 'http://localhost:3000' ? 'http://localhost:3000'
: 'https://rublox.pro'; : 'https://rublox.pro';
avatarUrl = `${base}/character-assets/skins/${player.skin}.png?v=20260614`; avatarUrl = `${base}/character-assets/skins/${skinId}.png?v=20260614`;
} else {
// Legacy R15: путь по старому шаблону.
avatarUrl = `/kubikon-assets/characters/${player.skin}/avatar.png?v=2026_05_27`;
}
isSkin = true; isSkin = true;
} else if (player.photo_thumb_b64) { } else if (player.photo_thumb_b64) {
avatarUrl = player.photo_thumb_b64.startsWith('data:') avatarUrl = player.photo_thumb_b64.startsWith('data:')
@ -931,6 +935,7 @@ function TabSettings({ sceneRef }) {
/> />
<SettingsSection title="Экран" /> <SettingsSection title="Экран" />
{!IS_DESKTOP_APP && (
<ArrowsRow <ArrowsRow
label="Полноэкранный режим" label="Полноэкранный режим"
hint="Развернуть игру на весь экран" hint="Развернуть игру на весь экран"
@ -947,6 +952,7 @@ function TabSettings({ sceneRef }) {
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}} }}
/> />
)}
<SliderRow <SliderRow
label="Качество графики" label="Качество графики"
hint="Разрешение рендера и тени" hint="Разрешение рендера и тени"

View File

@ -25,6 +25,24 @@ import useDeviceType from '../hooks/useDeviceType';
import KubikonMobileControls from './KubikonMobileControls'; import KubikonMobileControls from './KubikonMobileControls';
import GameLoadingScreen from './GameLoadingScreen'; import GameLoadingScreen from './GameLoadingScreen';
// В десктоп-приложении (Electron-обёртка rublox-desktop) окно уже на весь
// экран без браузерной панели и без вкладок fullscreen не нужен (раньше он
// защищал от случайного Ctrl+W/Ctrl+T в браузере; в Electron этого риска нет).
// preload выставляет window.__RUBLOX_DESKTOP__.
const IS_DESKTOP_APP = typeof window !== 'undefined' && !!window.__RUBLOX_DESKTOP__;
// В Android-приложении (Capacitor-обёртка rublox-android) WebView уже на весь
// экран браузерный fullscreen не нужен, а стартовый оверлей «Нажми чтобы
// играть» избыточен (в браузере он нужен для user-gesture перед FS, в APK
// этого барьера не требуется). Capacitor выставляет window.Capacitor.
const IS_ANDROID_APP = typeof window !== 'undefined'
&& (!!window.Capacitor
|| /RubloxAndroid/i.test(navigator.userAgent || ''));
// Объединённый признак «нативного приложения» (десктоп ИЛИ Android) там,
// где поведение совпадает (не дёргать fullscreen).
const IS_NATIVE_APP = IS_DESKTOP_APP || IS_ANDROID_APP;
// Плеер живёт на player.rublox.pro он не знает SPA-роутов Майнкрафтии // Плеер живёт на player.rublox.pro он не знает SPA-роутов Майнкрафтии
// (/kubikon, /login, /auth). Поэтому вместо navigate(...) делаем // (/kubikon, /login, /auth). Поэтому вместо navigate(...) делаем
// явный window.location.assign на внешний домен. // явный window.location.assign на внешний домен.
@ -230,7 +248,10 @@ const KubikonPlayer = () => {
// ВКЛЮЧИТЬ fullscreen и заблокировать Ctrl+W/Ctrl+T и др. системные // ВКЛЮЧИТЬ fullscreen и заблокировать Ctrl+W/Ctrl+T и др. системные
// хоткеи. Без этого браузер закрывает вкладку при случайном Ctrl+W. // хоткеи. Без этого браузер закрывает вкладку при случайном Ctrl+W.
// requestFullscreen() требует user gesture поэтому без клика никак. // requestFullscreen() требует user gesture поэтому без клика никак.
const [gameStarted, setGameStarted] = useState(false); // В нативном приложении (Electron/Capacitor) fullscreen не нужен (окно и
// так на весь экран), поэтому стартовый оверлей пропускаем игра
// запускается сразу.
const [gameStarted, setGameStarted] = useState(IS_NATIVE_APP);
const [hp, setHp] = useState({ hp: 100, maxHp: 100 }); const [hp, setHp] = useState({ hp: 100, maxHp: 100 });
// Скрипт через game.hud.setVisible(false) полностью скрывает стандартный HUD. // Скрипт через game.hud.setVisible(false) полностью скрывает стандартный HUD.
const [stdHudVisible, setStdHudVisible] = useState(true); const [stdHudVisible, setStdHudVisible] = useState(true);
@ -1124,21 +1145,24 @@ const KubikonPlayer = () => {
|| root.webkitRequestFullscreen || root.webkitRequestFullscreen
|| root.mozRequestFullScreen || root.mozRequestFullScreen
|| root.msRequestFullscreen; || root.msRequestFullscreen;
if (req) { // В нативном приложении (Electron/Capacitor) окно и так на весь
// экран FS не нужен.
if (req && !IS_NATIVE_APP) {
try { await req.call(root); } catch (e) { /* отменено */ } try { await req.call(root); } catch (e) { /* отменено */ }
} }
setMobileStartTapped(true); setMobileStartTapped(true);
}, []); }, []);
/** Стартовый клик «Начать игру» запрашивает fullscreen /** Стартовый клик «Начать игру» запрашивает fullscreen
* (Chrome блокирует Ctrl+W/Ctrl+T в fullscreen) и снимает оверлей. */ * (Chrome блокирует Ctrl+W/Ctrl+T в fullscreen) и снимает оверлей.
* В нативном приложении (Electron/Capacitor) FS не нужен. */
const handleGameStart = useCallback(async () => { const handleGameStart = useCallback(async () => {
const root = document.documentElement; const root = document.documentElement;
const req = root.requestFullscreen const req = root.requestFullscreen
|| root.webkitRequestFullscreen || root.webkitRequestFullscreen
|| root.mozRequestFullScreen || root.mozRequestFullScreen
|| root.msRequestFullscreen; || root.msRequestFullscreen;
if (req) { if (req && !IS_NATIVE_APP) {
try { await req.call(root); } catch (e) { /* юзер запретил — играем без FS */ } try { await req.call(root); } catch (e) { /* юзер запретил — играем без FS */ }
} }
setGameStarted(true); setGameStarted(true);
@ -1286,11 +1310,18 @@ const KubikonPlayer = () => {
lineHeight: 1.4, lineHeight: 1.4,
padding: '0 24px', padding: '0 24px',
}}> }}>
{IS_DESKTOP_APP ? (
<>Управление: <b>WASD</b> движение, <b>пробел</b> прыжок,
мышь камера.</>
) : (
<>
Игра откроется в полноэкранном режиме Игра откроется в полноэкранном режиме
это защитит от случайного закрытия вкладки это защитит от случайного закрытия вкладки
(Ctrl+W, Ctrl+T и др.). (Ctrl+W, Ctrl+T и др.).
<br /> <br />
Выход: <b>Esc</b> или <b>F11</b>. Выход: <b>Esc</b> или <b>F11</b>.
</>
)}
</div> </div>
<button <button
type="button" type="button"