Реализовано из 14 механик:
1. Teams (game.Teams, Player.Team, TeamColor): scene.teams[] из конвертера,
эвристика TeamBeacon-Model → автоматически создаются 4 команды.
В shim создаются Team-инстансы при snapshot, авто-эквип игрока в первую.
2. Leaderstats UI: IntValue.Value реактивно шлёт leaderstatSet → существующий
LeaderstatsManager (define + set). HUD автоматически рисуется в правом
верхнем по родительскому Name='leaderstats'.
3. BindableFunction + RemoteFunction + Message/Hint класс. Message с
реактивным .Text и .Parent шлёт hudMessage в наш RbxlHudOverlay.
4. KillFeed UI + creator-tag tracking. RbxlHudOverlay.addKillFeed() рисует
А → [weapon] → Б в правом верхнем. Humanoid.TakeDamage при Health=0
ищет creator-ObjectValue и шлёт killFeed. Авто-respawn через 2с.
5. SpawnLocation.TeamColor → scene.team_spawns[] для будущей логики
команд-спавна.
6. Tool:Clone() / Model:Clone() / :clone(): поверхностный клон + lowercase
alias. Также :MakeJoints/:BreakJoints/:Remove/:remove no-op методы.
7. Creator-tag handling в TakeDamage (см. пункт 4).
12. Bouncer/батут: BodyVelocity с +Y и Parent=Torso/HumanoidRootPart →
эвристика "толкаем вверх" → playerSet jumpVelocity → реальный jump
через player._vy.
14. Mouse.Icon → CSS cursor на canvas (crosshair для не-пустых).
Также:
- RbxlHudOverlay.js — новый модуль DOM-оверлей для HUD-элементов
(KillFeed/Message/WinGui). Lazy-создаётся при первом hudMessage/killFeed.
- BabylonScene.serialize включает scene.teams и scene.team_spawns.
- Converter: scene = teams[] + team_spawns[]. TeamBeacon Model'и → команды.
- Deploy converter.py на VM 130.
Остались: 8 Regeneration, 9 BattleArmor, 10 WinGui/FireButton кастомное
позиционирование, 11 AdminConsole (no-op уже ok), 13 NotLinkedBlocker.
178 lines
6.0 KiB
JavaScript
178 lines
6.0 KiB
JavaScript
/**
|
||
* RbxlHudOverlay — DOM-оверлей с HUD-элементами для импортированных
|
||
* Roblox-карт: KillFeed (правый верх), Message/Hint (центр), WinGui.
|
||
*
|
||
* Один контейнер `.rbxl-hud-overlay` на canvas.parentElement, в нём дочерние
|
||
* блоки по типу. Стили inline, ничего не зависит от CSS приложения.
|
||
*
|
||
* API:
|
||
* const hud = new RbxlHudOverlay(canvasParent);
|
||
* hud.addKillFeed(killer, victim, weapon)
|
||
* hud.showMessage(text, opts)
|
||
* hud.hideMessage()
|
||
* hud.showWin(text)
|
||
* hud.dispose()
|
||
*/
|
||
|
||
export class RbxlHudOverlay {
|
||
constructor(parent) {
|
||
this._parent = parent || document.body;
|
||
this._root = null;
|
||
this._killFeed = null;
|
||
this._message = null;
|
||
this._winBox = null;
|
||
this._killEntries = []; // [{el, expireAt}]
|
||
this._mount();
|
||
}
|
||
|
||
_mount() {
|
||
if (this._root) return;
|
||
const root = document.createElement('div');
|
||
root.className = 'rbxl-hud-overlay';
|
||
Object.assign(root.style, {
|
||
position: 'absolute',
|
||
inset: '0',
|
||
pointerEvents: 'none',
|
||
zIndex: '999',
|
||
fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
|
||
});
|
||
this._parent.appendChild(root);
|
||
this._root = root;
|
||
|
||
// KillFeed — правый верхний угол
|
||
const kf = document.createElement('div');
|
||
Object.assign(kf.style, {
|
||
position: 'absolute',
|
||
top: '60px',
|
||
right: '12px',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '6px',
|
||
maxWidth: '320px',
|
||
pointerEvents: 'none',
|
||
});
|
||
root.appendChild(kf);
|
||
this._killFeed = kf;
|
||
|
||
// Message — центр сверху (Roblox Message по центру экрана,
|
||
// но в верхней трети чтобы не мешать игре)
|
||
const msg = document.createElement('div');
|
||
Object.assign(msg.style, {
|
||
position: 'absolute',
|
||
top: '15%',
|
||
left: '50%',
|
||
transform: 'translateX(-50%)',
|
||
padding: '10px 24px',
|
||
background: 'rgba(0,0,0,0.6)',
|
||
color: '#fff',
|
||
fontSize: '22px',
|
||
fontWeight: '600',
|
||
borderRadius: '6px',
|
||
textShadow: '0 2px 4px rgba(0,0,0,0.8)',
|
||
display: 'none',
|
||
pointerEvents: 'none',
|
||
});
|
||
root.appendChild(msg);
|
||
this._message = msg;
|
||
|
||
// WinGui — большая надпись по центру
|
||
const win = document.createElement('div');
|
||
Object.assign(win.style, {
|
||
position: 'absolute',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
padding: '24px 48px',
|
||
background: 'rgba(0,0,0,0.75)',
|
||
color: '#ffd86b',
|
||
fontSize: '48px',
|
||
fontWeight: '800',
|
||
borderRadius: '12px',
|
||
textShadow: '0 4px 8px rgba(0,0,0,0.8)',
|
||
display: 'none',
|
||
pointerEvents: 'none',
|
||
});
|
||
root.appendChild(win);
|
||
this._winBox = win;
|
||
|
||
// Тик для авто-исчезновения KillFeed entries (через 5с)
|
||
this._tickInterval = setInterval(() => this._cleanupKills(), 500);
|
||
}
|
||
|
||
addKillFeed(killer, victim, weapon) {
|
||
if (!this._killFeed) return;
|
||
const entry = document.createElement('div');
|
||
Object.assign(entry.style, {
|
||
background: 'rgba(0,0,0,0.55)',
|
||
color: '#fff',
|
||
padding: '6px 10px',
|
||
borderRadius: '4px',
|
||
fontSize: '13px',
|
||
display: 'flex',
|
||
gap: '6px',
|
||
alignItems: 'center',
|
||
animation: 'rbxlHudFadeIn 0.3s',
|
||
});
|
||
const killerEl = document.createElement('span');
|
||
killerEl.textContent = String(killer || '?');
|
||
killerEl.style.color = '#5bd1e8';
|
||
const arrow = document.createElement('span');
|
||
arrow.textContent = weapon ? `→ [${weapon}] →` : '→';
|
||
arrow.style.color = '#ff9a52';
|
||
const victimEl = document.createElement('span');
|
||
victimEl.textContent = String(victim || '?');
|
||
victimEl.style.color = '#f87a7a';
|
||
entry.appendChild(killerEl);
|
||
entry.appendChild(arrow);
|
||
entry.appendChild(victimEl);
|
||
this._killFeed.appendChild(entry);
|
||
this._killEntries.push({ el: entry, expireAt: performance.now() + 5000 });
|
||
// Keep only last 8
|
||
while (this._killEntries.length > 8) {
|
||
const old = this._killEntries.shift();
|
||
try { old.el.remove(); } catch (_) {}
|
||
}
|
||
}
|
||
|
||
_cleanupKills() {
|
||
const now = performance.now();
|
||
const keep = [];
|
||
for (const e of this._killEntries) {
|
||
if (e.expireAt < now) { try { e.el.remove(); } catch (_) {} }
|
||
else keep.push(e);
|
||
}
|
||
this._killEntries = keep;
|
||
}
|
||
|
||
showMessage(text, opts = {}) {
|
||
if (!this._message) return;
|
||
this._message.textContent = String(text || '');
|
||
this._message.style.display = text ? 'block' : 'none';
|
||
if (opts.duration) {
|
||
clearTimeout(this._msgTimer);
|
||
this._msgTimer = setTimeout(() => this.hideMessage(), opts.duration);
|
||
}
|
||
}
|
||
|
||
hideMessage() {
|
||
if (this._message) this._message.style.display = 'none';
|
||
}
|
||
|
||
showWin(text) {
|
||
if (!this._winBox) return;
|
||
this._winBox.textContent = String(text || '');
|
||
this._winBox.style.display = 'block';
|
||
// Auto-hide через 6с
|
||
clearTimeout(this._winTimer);
|
||
this._winTimer = setTimeout(() => { if (this._winBox) this._winBox.style.display = 'none'; }, 6000);
|
||
}
|
||
|
||
dispose() {
|
||
try { this._root?.remove(); } catch (_) {}
|
||
clearInterval(this._tickInterval);
|
||
clearTimeout(this._msgTimer);
|
||
clearTimeout(this._winTimer);
|
||
this._root = null;
|
||
}
|
||
}
|