studio/src/editor/PlayerHud.jsx
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
Open-source веб-студия для создания игр Рублокса, двойная лицензия
AGPL-3.0 + Коммерческая.

Главное:
- Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16
- Самодостаточный движок ~28к строк (66 файлов): BlockManager,
  TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController,
  ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов
- Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco)
- Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn)
- Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt)
- 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.)
- Конфигурируемый бэкенд через VITE_API_BASE — работает со staging
  (dev-api.rublox.pro) без настройки
- Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка
- Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING,
  SECURITY, CHANGELOG
- ESLint + Prettier + EditorConfig
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Перед публикацией:
- Все импорты из minecraftia заменены на локальные
- Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env
- Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо)
- AdminKubikonModeration не публикуется (модерация — в team.rublox.pro)
- 93 МБ ассетов public/kubikon-assets вынесены в .gitignore
  (раздаются через release artifact)
2026-05-27 23:41:10 +03:00

320 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import Icon from './Icon';
/**
* PlayerHud — HUD игрока в Play-режиме:
* - HP-полоска слева сверху с сегментами и подсветкой
* - Красный flash при получении урона
* - Счётчик патронов справа снизу с кольцом перезарядки
*/
function PlayerHud({ visible, hp, maxHp, ammo, damaged }) {
if (!visible) return null;
const hpPct = Math.max(0, Math.min(100, (hp / maxHp) * 100));
const lowHp = hp <= maxHp * 0.25;
const midHp = !lowHp && hp <= maxHp * 0.5;
const hpGradient = lowHp
? 'linear-gradient(90deg, #ff6f7a 0%, #c0303f 100%)'
: midHp
? 'linear-gradient(90deg, #ffc857 0%, #f59e0b 100%)'
: 'linear-gradient(90deg, #22d97a 0%, #0f9d56 100%)';
const hpGlow = lowHp
? '0 0 16px rgba(255, 111, 122, 0.55)'
: midHp
? '0 0 12px rgba(255, 200, 87, 0.35)'
: '0 0 12px rgba(34, 217, 122, 0.35)';
return (
<>
<style>{HUD_KEYFRAMES}</style>
{/* === HP-bar — ЛЕВЫЙ ВЕРХНИЙ УГОЛ === */}
<div style={{
position: 'absolute',
left: 14,
top: 14,
width: 260,
pointerEvents: 'none',
zIndex: 30,
background: 'rgba(20, 24, 45, 0.78)',
border: '1px solid rgba(255, 255, 255, 0.10)',
borderRadius: 14,
padding: '10px 12px',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow:
'0 8px 24px rgba(0, 0, 0, 0.45), '
+ '0 0 0 1px rgba(51, 87, 255, 0.10), '
+ 'inset 0 1px 0 rgba(255, 255, 255, 0.06)',
}}>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
marginBottom: 6,
}}>
<div style={{
fontSize: 18, lineHeight: 1,
animation: lowHp ? 'hudHeartBeat 0.8s ease-in-out infinite' : 'none',
filter: lowHp ? 'drop-shadow(0 0 8px rgba(255,111,122,0.7))' : 'none',
}}><Icon name="heart" size={14} /></div>
<div style={{
fontSize: 14, fontWeight: 800, color: '#f1f5fb',
textShadow: '0 1px 3px rgba(0,0,0,0.7)',
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
letterSpacing: 0.3,
fontVariantNumeric: 'tabular-nums',
}}>
{Math.round(hp)}<span style={{
color: 'rgba(241, 245, 251, 0.42)',
fontWeight: 700, fontSize: 12, marginLeft: 2,
}}> / {maxHp}</span>
</div>
<div style={{
marginLeft: 'auto',
fontSize: 10, fontWeight: 800,
color: lowHp ? '#ff6f7a' : midHp ? '#ffc857' : '#22d97a',
textTransform: 'uppercase', letterSpacing: 1,
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
}}>
{Math.round(hpPct)}%
</div>
</div>
{/* Полоска */}
<div style={{
height: 10,
background: 'rgba(0, 0, 0, 0.45)',
border: '1px solid rgba(255, 255, 255, 0.08)',
borderRadius: 999,
overflow: 'hidden',
position: 'relative',
boxShadow: 'inset 0 2px 4px rgba(0, 0, 0, 0.4)',
}}>
<div style={{
height: '100%',
width: `${hpPct}%`,
background: hpGradient,
borderRadius: 999,
transition: 'width 200ms ease, background 200ms ease, box-shadow 200ms ease',
boxShadow: hpGlow,
position: 'relative',
}}>
{/* Блик сверху */}
<div style={{
position: 'absolute', left: 0, right: 0, top: 0, height: '50%',
background: 'linear-gradient(180deg, rgba(255,255,255,0.28), transparent)',
borderRadius: '999px 999px 0 0',
}} />
</div>
{/* Сегментные риски (10 шт) */}
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} style={{
position: 'absolute',
top: 0, bottom: 0,
left: `${(i + 1) * 10}%`,
width: 1,
background: 'rgba(0, 0, 0, 0.35)',
pointerEvents: 'none',
}} />
))}
</div>
</div>
{/* Красный flash при получении урона */}
{damaged && (
<div style={{
position: 'absolute',
inset: 0,
pointerEvents: 'none',
zIndex: 29,
background: 'radial-gradient(circle, transparent 25%, rgba(255, 60, 80, 0.55) 100%)',
animation: 'hudHurtFlash 0.4s ease-out',
}} />
)}
{/* === Ammo — справа внизу, над hot-bar === */}
{ammo && (
<AmmoIndicator ammo={ammo} />
)}
</>
);
}
const AmmoIndicator = ({ ammo }) => {
const reloading = !!ammo.reloading;
const magPct = ammo.magazineMax > 0
? (ammo.magazine / ammo.magazineMax) * 100
: 0;
const lowAmmo = !reloading && magPct < 25;
// SVG-кольцо progress перезарядки
const RING_R = 30;
const RING_C = 2 * Math.PI * RING_R;
return (
<div style={{
position: 'absolute',
right: 18,
bottom: 100,
pointerEvents: 'none',
zIndex: 30,
background: 'rgba(20, 24, 45, 0.82)',
border: '1px solid rgba(255, 255, 255, 0.10)',
borderRadius: 16,
padding: '14px 18px',
color: '#f1f5fb',
fontFamily: '"Roboto Condensed", system-ui, sans-serif',
minWidth: 150,
textAlign: 'center',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
boxShadow:
'0 12px 32px rgba(0, 0, 0, 0.50), '
+ '0 0 0 1px rgba(51, 87, 255, 0.12), '
+ 'inset 0 1px 0 rgba(255, 255, 255, 0.06)',
}}>
{reloading ? (
<div style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8,
}}>
{/* Кольцо progress */}
<div style={{ position: 'relative', width: 72, height: 72 }}>
<svg width={72} height={72} style={{ transform: 'rotate(-90deg)' }}>
<circle
cx={36} cy={36} r={RING_R}
stroke="rgba(255,255,255,0.08)"
strokeWidth={5}
fill="none"
/>
<circle
cx={36} cy={36} r={RING_R}
stroke="#3357ff"
strokeWidth={5}
fill="none"
strokeLinecap="round"
strokeDasharray={RING_C}
strokeDashoffset={RING_C * (1 - ammo.reloadProgress)}
style={{
transition: 'stroke-dashoffset 0.05s linear',
filter: 'drop-shadow(0 0 6px rgba(51, 87, 255, 0.65))',
}}
/>
</svg>
<div style={{
position: 'absolute', inset: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 22,
animation: 'hudSpin 1.2s linear infinite',
}}><Icon name="refresh" size={14} /></div>
</div>
<div style={{
fontSize: 11, fontWeight: 800, color: '#3357ff',
textTransform: 'uppercase', letterSpacing: 1.5,
}}>
Перезарядка
</div>
</div>
) : (
<>
{/* Magazine */}
<div style={{
display: 'flex', alignItems: 'baseline',
justifyContent: 'center', gap: 4,
fontVariantNumeric: 'tabular-nums',
}}>
<span style={{
fontSize: 32, fontWeight: 900, lineHeight: 1,
color: lowAmmo ? '#ff6f7a' : '#f1f5fb',
letterSpacing: -1,
textShadow: lowAmmo
? '0 0 12px rgba(255, 111, 122, 0.55)'
: 'none',
animation: lowAmmo ? 'hudPulse 1s ease-in-out infinite' : 'none',
}}>
{ammo.magazine}
</span>
<span style={{
fontSize: 14, fontWeight: 700,
color: 'rgba(241, 245, 251, 0.42)',
}}>
/ {ammo.magazineMax}
</span>
</div>
{/* Тонкая полоска текущей обоймы */}
<div style={{
height: 3,
background: 'rgba(0, 0, 0, 0.4)',
borderRadius: 999,
overflow: 'hidden',
marginTop: 6, marginBottom: 8,
}}>
<div style={{
height: '100%',
width: `${magPct}%`,
background: lowAmmo
? 'linear-gradient(90deg, #ff6f7a, #c0303f)'
: 'linear-gradient(90deg, #3357ff, #1e2da5)',
borderRadius: 999,
transition: 'width 200ms ease',
boxShadow: lowAmmo
? '0 0 8px rgba(255, 111, 122, 0.5)'
: '0 0 8px rgba(51, 87, 255, 0.4)',
}} />
</div>
{/* Reserve + R hint */}
<div style={{
display: 'flex', alignItems: 'center',
justifyContent: 'space-between', gap: 8,
fontSize: 11,
}}>
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
color: 'rgba(241, 245, 251, 0.62)', fontWeight: 700,
}}>
<Icon name="backpack" size={14} /> {ammo.reserve}
</span>
<span style={{
display: 'inline-flex', alignItems: 'center', gap: 4,
fontSize: 10, color: 'rgba(241, 245, 251, 0.42)',
fontWeight: 700, letterSpacing: 0.5,
textTransform: 'uppercase',
}}>
<kbd style={{
background: 'rgba(255, 255, 255, 0.08)',
border: '1px solid rgba(255, 255, 255, 0.15)',
borderRadius: 4,
padding: '1px 6px',
fontSize: 10, fontFamily: 'inherit', fontWeight: 800,
color: '#f1f5fb',
}}>R</kbd>
перезаряд
</span>
</div>
</>
)}
</div>
);
};
const HUD_KEYFRAMES = `
@keyframes hudHurtFlash {
0% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes hudHeartBeat {
0%, 100% { transform: scale(1); }
25%, 75% { transform: scale(1.18); }
50% { transform: scale(0.95); }
}
@keyframes hudSpin { to { transform: rotate(360deg); } }
@keyframes hudPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.55; }
}
`;
export default PlayerHud;