- game.fx.pointer + расширенный game.fx.beam: BeamManager (текстуры/curved/ градиент/quest-marker), ScriptSandboxWorker (_normFxPoint от DataCloneError), GameRuntime (fx.createPointer/pointerTarget/pointerUpdate/beamUpdate/ beamVisible), BabylonScene._activatePointers. 1-в-1 со студией. - Dev JWT-панель на экране «Нужен JWT» (только localhost): кнопка → инпут → localStorage.player_jwt + reload. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
234 lines
7.1 KiB
JavaScript
234 lines
7.1 KiB
JavaScript
// LoadingScreen — брендовый сплеш плеера.
|
||
// Скопирован 1-в-1 с Android LoadingView.kt:
|
||
// - синий gradient brandTop #2841C8 → brandBottom #4B6EF0
|
||
// - лого A02 130px по центру
|
||
// - "Рублокс" 56sp белый под лого
|
||
// - 8-точечный spinner с активной точкой
|
||
// - подпись (text + анимированные точки)
|
||
// - декоративные пузыри с "дышащим" alpha
|
||
//
|
||
// CSS-анимации, без JS-фрейма каждый кадр.
|
||
|
||
import React, { useState } from 'react';
|
||
|
||
const TITLE = 'Рублокс';
|
||
|
||
/**
|
||
* Dev-only панель вставки JWT. Показывается на экране «Нужен JWT» (только
|
||
* localhost). Кнопка → инпут → сохраняет в localStorage['player_jwt'] и
|
||
* перезагружает страницу. На проде этот экран не наступает (там redirect).
|
||
*/
|
||
function DevJwtPanel() {
|
||
const [open, setOpen] = useState(false);
|
||
const [val, setVal] = useState('');
|
||
|
||
const apply = () => {
|
||
const t = (val || '').trim();
|
||
if (!t) return;
|
||
try {
|
||
localStorage.setItem('player_jwt', t);
|
||
// совместимость с другими местами чтения токена
|
||
localStorage.setItem('Authorization', t);
|
||
} catch (e) { /* ignore */ }
|
||
window.location.reload();
|
||
};
|
||
|
||
if (!open) {
|
||
return (
|
||
<button className="rb-devbtn" onClick={() => setOpen(true)} title="Вставить JWT (dev)">
|
||
🔑 Вставить JWT
|
||
</button>
|
||
);
|
||
}
|
||
return (
|
||
<div className="rb-devpanel">
|
||
<textarea
|
||
className="rb-devinput"
|
||
placeholder="Вставь сюда player_jwt…"
|
||
value={val}
|
||
onChange={(e) => setVal(e.target.value)}
|
||
autoFocus
|
||
spellCheck={false}
|
||
onKeyDown={(e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) apply(); }}
|
||
/>
|
||
<div className="rb-devrow">
|
||
<button className="rb-devapply" onClick={apply} disabled={!val.trim()}>
|
||
Войти
|
||
</button>
|
||
<button className="rb-devcancel" onClick={() => setOpen(false)}>
|
||
Отмена
|
||
</button>
|
||
</div>
|
||
<div className="rb-devhint">Ctrl+Enter — войти. Токен сохранится в localStorage.</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function LoadingScreen({ text = 'Подключение', subText = null, devJwt = false }) {
|
||
return (
|
||
<div className="rb-splash">
|
||
<style>{splashCss}</style>
|
||
|
||
{/* Декоративные пузыри — те же позиции что в Android LoadingView */}
|
||
{bubbles.map((b, i) => (
|
||
<span
|
||
key={i}
|
||
className="rb-bubble"
|
||
style={{
|
||
left: `${b.x * 100}%`,
|
||
top: `${b.y * 100}%`,
|
||
width: b.size,
|
||
height: b.size,
|
||
animationDelay: `${b.phase}s`,
|
||
}}
|
||
/>
|
||
))}
|
||
|
||
<img src="/A02.png" alt="" width={130} height={130} className="rb-logo" />
|
||
<div className="rb-title">{TITLE}</div>
|
||
|
||
{/* 8-точечный spinner */}
|
||
<div className="rb-spinner" aria-hidden="true">
|
||
{Array.from({ length: 8 }).map((_, i) => (
|
||
<span key={i} className="rb-dot" style={{ '--i': i }} />
|
||
))}
|
||
</div>
|
||
|
||
<div className="rb-status">
|
||
{text}<span className="rb-dots" aria-hidden="true">…</span>
|
||
</div>
|
||
{subText && <div className="rb-substatus">{subText}</div>}
|
||
|
||
{/* Dev-only: вставка JWT прямо с экрана (вместо ручного localStorage). */}
|
||
{devJwt && <DevJwtPanel />}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const bubbles = [
|
||
{ x: 0.05, y: 0.93, size: 48, phase: 0.0 },
|
||
{ x: 0.12, y: 0.88, size: 28, phase: 1.2 },
|
||
{ x: 0.20, y: 0.95, size: 20, phase: 2.4 },
|
||
{ x: 0.85, y: 0.85, size: 36, phase: 0.7 },
|
||
{ x: 0.92, y: 0.92, size: 44, phase: 1.9 },
|
||
{ x: 0.50, y: 0.97, size: 16, phase: 3.1 },
|
||
];
|
||
|
||
const splashCss = `
|
||
.rb-splash {
|
||
position: fixed; inset: 0;
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
background: linear-gradient(160deg, #2841C8 0%, #4B6EF0 100%);
|
||
color: #fff;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
overflow: hidden;
|
||
}
|
||
.rb-logo {
|
||
filter: drop-shadow(0 6px 14px rgba(0,0,0,0.35));
|
||
margin-bottom: 28px;
|
||
}
|
||
.rb-title {
|
||
font-size: 56px;
|
||
font-weight: 700;
|
||
letter-spacing: 1px;
|
||
margin-bottom: 40px;
|
||
text-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
||
}
|
||
.rb-spinner {
|
||
position: relative;
|
||
width: 64px; height: 64px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.rb-dot {
|
||
position: absolute;
|
||
top: 50%; left: 50%;
|
||
width: 6px; height: 6px; border-radius: 50%;
|
||
background: rgba(255,255,255,0.45);
|
||
transform:
|
||
translate(-50%, -50%)
|
||
rotate(calc(var(--i) * 45deg))
|
||
translateY(-22px);
|
||
animation: rbDotPulse 1s linear infinite;
|
||
animation-delay: calc(var(--i) * 0.125s);
|
||
}
|
||
@keyframes rbDotPulse {
|
||
0%, 70%, 100% { background: rgba(255,255,255,0.35); }
|
||
10% { background: rgba(255,255,255,1.0); }
|
||
}
|
||
.rb-status {
|
||
font-size: 16px; opacity: 0.85;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
.rb-dots {
|
||
display: inline-block; width: 24px; text-align: left;
|
||
animation: rbDotsBlink 1.4s steps(4, end) infinite;
|
||
}
|
||
@keyframes rbDotsBlink {
|
||
0% { clip-path: inset(0 100% 0 0); }
|
||
100% { clip-path: inset(0 0 0 0); }
|
||
}
|
||
.rb-substatus {
|
||
margin-top: 14px;
|
||
font-size: 13px; opacity: 0.65;
|
||
max-width: 360px; padding: 0 20px;
|
||
text-align: center; line-height: 1.45;
|
||
}
|
||
.rb-bubble {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
background: rgba(255,255,255,0.16);
|
||
animation: rbBubble 3.5s ease-in-out infinite;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
}
|
||
/* === Dev JWT panel === */
|
||
.rb-devbtn {
|
||
margin-top: 22px;
|
||
background: rgba(255,255,255,0.14);
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
color: #fff;
|
||
font-size: 13px; font-weight: 600;
|
||
padding: 8px 16px; border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
.rb-devbtn:hover { background: rgba(255,255,255,0.24); }
|
||
.rb-devpanel {
|
||
margin-top: 20px;
|
||
width: 380px; max-width: calc(100vw - 40px);
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
}
|
||
.rb-devinput {
|
||
width: 100%; height: 70px; resize: vertical;
|
||
background: rgba(0,0,0,0.28);
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
border-radius: 10px;
|
||
color: #fff; font-size: 12px;
|
||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||
padding: 10px; box-sizing: border-box;
|
||
outline: none;
|
||
}
|
||
.rb-devinput:focus { border-color: rgba(255,255,255,0.6); }
|
||
.rb-devrow { display: flex; gap: 8px; }
|
||
.rb-devapply {
|
||
flex: 1;
|
||
background: #ffffff; color: #2841C8;
|
||
border: none; border-radius: 10px;
|
||
font-size: 14px; font-weight: 700;
|
||
padding: 9px 0; cursor: pointer;
|
||
}
|
||
.rb-devapply:disabled { opacity: 0.4; cursor: default; }
|
||
.rb-devcancel {
|
||
background: transparent; color: rgba(255,255,255,0.8);
|
||
border: 1px solid rgba(255,255,255,0.3); border-radius: 10px;
|
||
font-size: 13px; padding: 9px 16px; cursor: pointer;
|
||
}
|
||
.rb-devhint {
|
||
font-size: 11px; opacity: 0.6; text-align: center;
|
||
}
|
||
@keyframes rbBubble {
|
||
0%, 100% { opacity: 0.35; }
|
||
50% { opacity: 0.70; }
|
||
}
|
||
`;
|