207 lines
7.3 KiB
PHP
207 lines
7.3 KiB
PHP
@extends('layouts.overlay')
|
|
|
|
@section('title', 'GAME')
|
|
|
|
@section('body-class', 'camera-hud-body')
|
|
|
|
@section('styles')
|
|
<style>
|
|
:root {
|
|
--pad-y: 40px;
|
|
--pad-x: 72px;
|
|
/* Camera frame defaults — auto-synced from OBS via WS. */
|
|
--cam-x: 1855px;
|
|
--cam-y: 128px;
|
|
--cam-w: 580px;
|
|
--cam-h: 326px;
|
|
--cam-pad: 10px;
|
|
}
|
|
.hud {
|
|
position: absolute;
|
|
top: var(--pad-y);
|
|
left: var(--pad-x);
|
|
right: var(--pad-x);
|
|
font-size: 20px;
|
|
text-shadow: 0 0 8px rgba(7, 8, 13, 0.9), 0 0 4px rgba(7, 8, 13, 0.9);
|
|
}
|
|
.hud .group { gap: 24px; }
|
|
.hud .group > span { gap: 10px; }
|
|
.hud .led { width: 10px; height: 10px; }
|
|
</style>
|
|
@endsection
|
|
|
|
@push('data-scripts')
|
|
<script>window.__LOADING = @json($manifest);</script>
|
|
@endpush
|
|
|
|
@section('crt')
|
|
{{-- no CRT overlays on the transparent game HUD --}}
|
|
@endsection
|
|
|
|
@section('content')
|
|
@include('partials.hud-strip', [
|
|
'variant' => 'live',
|
|
'idText' => 'OPHI-118 // LIVE',
|
|
])
|
|
|
|
@include('partials.camera-frame', ['id' => 'cam', 'label' => 'CAM 01'])
|
|
|
|
<footer class="status-bar">
|
|
<div class="status-line">
|
|
<div class="live-block">
|
|
<span class="live-dot"></span>
|
|
<span>LIVE</span>
|
|
</div>
|
|
<span class="sep">·</span>
|
|
<span class="game-name" id="game-name">—</span>
|
|
<span class="sep" id="game-mode-sep">·</span>
|
|
<span class="game-mode" id="game-mode">—</span>
|
|
|
|
@if ($olden)
|
|
@php
|
|
$me = ['side' => 'my', 'faction' => $olden['my']['faction'], 'hero' => $olden['my']['hero']];
|
|
$foe = ['side' => 'enemy', 'faction' => $olden['enemy']['faction'], 'hero' => $olden['enemy']['hero']];
|
|
$color = ($olden['color'] ?? 'red') === 'blue' ? 'blue' : 'red';
|
|
$myLeft = $color === 'red';
|
|
$left = $myLeft ? $me : $foe;
|
|
$right = $myLeft ? $foe : $me;
|
|
$rig = strtoupper($rigName ?: 'ME');
|
|
$nameFor = fn($s) => $s['side'] === 'my' ? $rig : strtoupper($s['hero']);
|
|
$url = fn($p) => $oldenAssetBase . '/' . str_replace('%2F', '/', rawurlencode($p));
|
|
@endphp
|
|
<div class="matchup matchup-{{ $color }}">
|
|
<span class="m-side m-{{ $left['side'] }}">
|
|
<img class="m-faction" src="{{ $url('factions/' . $left['faction'] . '.png') }}" alt="">
|
|
<img class="m-hero" src="{{ $url('heroes/' . $left['faction'] . '/' . $left['hero'] . '.png') }}" alt="">
|
|
<span class="m-name">{{ $nameFor($left) }}</span>
|
|
</span>
|
|
<span class="m-vs">VS</span>
|
|
<span class="m-side m-{{ $right['side'] }}">
|
|
<span class="m-name">{{ $nameFor($right) }}</span>
|
|
<img class="m-hero" src="{{ $url('heroes/' . $right['faction'] . '/' . $right['hero'] . '.png') }}" alt="">
|
|
<img class="m-faction" src="{{ $url('factions/' . $right['faction'] . '.png') }}" alt="">
|
|
</span>
|
|
@if (!empty($olden['map']))
|
|
<span class="m-on">ON</span>
|
|
<span class="m-map">{{ strtoupper($olden['map']) }}</span>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div class="status-line meta-line">
|
|
<span class="icon">◷</span>
|
|
<span class="elapsed" id="elapsed">00:00:00</span>
|
|
<span class="sep">·</span>
|
|
<span class="icon" id="mic-icon" title="microphone">🎙</span>
|
|
<span id="mic-state">—</span>
|
|
<span class="sep">·</span>
|
|
<div class="np-block" id="np">
|
|
<span class="arrow">▶</span>
|
|
<span class="title" id="np-title">—</span>
|
|
<span class="time" id="np-time">[--:-- / --:--]</span>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
@endsection
|
|
|
|
@section('scripts')
|
|
<script>
|
|
'use strict';
|
|
const pad = HUD.pad;
|
|
|
|
const m = window.__LOADING || {};
|
|
const gameName = (m.game || '—').toUpperCase();
|
|
const subtitle = m.subtitle ? m.subtitle.toUpperCase() : null;
|
|
const cameraOn = m.camera ?? true;
|
|
const micOn = m.microphone ?? true;
|
|
|
|
document.getElementById('game-name').textContent = gameName;
|
|
if (subtitle) {
|
|
document.getElementById('game-mode').textContent = subtitle;
|
|
} else {
|
|
document.getElementById('game-mode-sep').style.display = 'none';
|
|
document.getElementById('game-mode').style.display = 'none';
|
|
}
|
|
const micEl = document.getElementById('mic-state');
|
|
micEl.textContent = micOn ? 'ON' : 'MUTED';
|
|
micEl.style.color = micOn ? 'var(--term-fg)' : 'var(--accent)';
|
|
if (!cameraOn) document.getElementById('cam').classList.add('off');
|
|
|
|
const streamStart = Date.now();
|
|
function tickElapsed() {
|
|
const s = Math.floor((Date.now() - streamStart) / 1000);
|
|
const h = Math.floor(s / 3600);
|
|
const mm = Math.floor((s / 60) % 60);
|
|
const ss = s % 60;
|
|
document.getElementById('elapsed').textContent = `${pad(h)}:${pad(mm)}:${pad(ss)}`;
|
|
}
|
|
tickElapsed(); setInterval(tickElapsed, 1000);
|
|
|
|
const params = new URLSearchParams(location.search);
|
|
const CAM_SOURCE = params.get('camera') || 'Camera';
|
|
const CAM_SCENE = params.get('scene') || 'Game';
|
|
|
|
function fmtTime(s) {
|
|
if (!isFinite(s) || s < 0) return '--:--';
|
|
return `${pad(Math.floor(s / 60))}:${pad(Math.floor(s % 60))}`;
|
|
}
|
|
const npBlock = document.getElementById('np');
|
|
const npTitle = document.getElementById('np-title');
|
|
const npTime = document.getElementById('np-time');
|
|
const camEl = document.getElementById('cam');
|
|
const rootStyle = document.documentElement.style;
|
|
const setCameraVisible = (visible) => camEl.classList.toggle('off', !visible);
|
|
function setCameraTransform(t) {
|
|
if (!t) return;
|
|
const w = (t.sourceWidth ?? 0) * (t.scaleX ?? 1);
|
|
const h = (t.sourceHeight ?? 0) * (t.scaleY ?? 1);
|
|
if (!(w > 0 && h > 0)) return;
|
|
rootStyle.setProperty('--cam-x', `${t.positionX}px`);
|
|
rootStyle.setProperty('--cam-y', `${t.positionY}px`);
|
|
rootStyle.setProperty('--cam-w', `${w}px`);
|
|
rootStyle.setProperty('--cam-h', `${h}px`);
|
|
}
|
|
|
|
async function syncCamera(obs) {
|
|
let camItemId = null;
|
|
try {
|
|
const r = await obs.call('GetSceneItemId',
|
|
{ sceneName: CAM_SCENE, sourceName: CAM_SOURCE });
|
|
camItemId = r.sceneItemId;
|
|
const e = await obs.call('GetSceneItemEnabled',
|
|
{ sceneName: CAM_SCENE, sceneItemId: camItemId });
|
|
setCameraVisible(e.sceneItemEnabled);
|
|
const t = await obs.call('GetSceneItemTransform',
|
|
{ sceneName: CAM_SCENE, sceneItemId: camItemId });
|
|
setCameraTransform(t.sceneItemTransform);
|
|
} catch (err) {
|
|
console.warn(`[game] camera sync init failed (${CAM_SCENE}/${CAM_SOURCE}):`, err.message);
|
|
return;
|
|
}
|
|
obs.addEventListener('event', (ev) => {
|
|
const d = ev.detail || {};
|
|
if (d.eventData?.sceneName !== CAM_SCENE
|
|
|| d.eventData?.sceneItemId !== camItemId) return;
|
|
if (d.eventType === 'SceneItemEnableStateChanged') {
|
|
setCameraVisible(d.eventData.sceneItemEnabled);
|
|
} else if (d.eventType === 'SceneItemTransformChanged') {
|
|
setCameraTransform(d.eventData.sceneItemTransform);
|
|
}
|
|
});
|
|
}
|
|
|
|
OBSWSBootstrap.connect({
|
|
onConnect: async (obs) => {
|
|
obs.onCustom('mpd:state', (s) => {
|
|
if (!s.title) return;
|
|
npBlock.classList.add('on');
|
|
npTitle.textContent = s.title;
|
|
npTime.textContent = `[${fmtTime(s.currentTime)} / ${fmtTime(s.duration)}]`;
|
|
});
|
|
await syncCamera(obs);
|
|
},
|
|
onClose: () => npBlock.classList.remove('on'),
|
|
});
|
|
</script>
|
|
@endsection
|