Styling fixes and Olden Era mmodule
This commit is contained in:
259
webapp/resources/views/home.blade.php
Normal file
259
webapp/resources/views/home.blade.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OPHI-118 / control panel</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
|
||||
<style>
|
||||
html, body {
|
||||
color: var(--term-fg);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
body.page-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.banner { margin-bottom: 28px; }
|
||||
|
||||
/* ---------- Banner / lede ---------- */
|
||||
.banner h1 {
|
||||
color: var(--term-fg-bright);
|
||||
text-shadow: 0 0 12px var(--term-glow);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.banner .lede {
|
||||
color: var(--term-fg-dim);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.banner .lede .caret { color: var(--term-fg-bright); margin-right: 8px; }
|
||||
|
||||
/* ---------- Tile grid ---------- */
|
||||
.tiles {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 22px;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.tile {
|
||||
--tile-fg: var(--term-fg);
|
||||
--tile-fg-hi: var(--term-fg-bright);
|
||||
--tile-dim: var(--term-fg-dim);
|
||||
--tile-edge: var(--term-edge);
|
||||
--tile-glow: var(--term-glow);
|
||||
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(80,220,100,0.04) 0%, rgba(0,0,0,0) 60%),
|
||||
var(--term-bg);
|
||||
border: 2px solid var(--tile-edge);
|
||||
border-radius: 6px;
|
||||
padding: 26px 30px 22px;
|
||||
color: var(--tile-fg);
|
||||
text-decoration: none;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 14px;
|
||||
overflow: hidden;
|
||||
transition: border-color 140ms ease, transform 140ms ease, box-shadow 140ms ease;
|
||||
box-shadow:
|
||||
0 0 30px rgba(80, 220, 100, 0.04),
|
||||
inset 0 0 60px rgba(0, 30, 0, 0.55);
|
||||
}
|
||||
.tile:hover {
|
||||
border-color: var(--tile-fg-hi);
|
||||
box-shadow:
|
||||
0 0 40px var(--tile-glow),
|
||||
inset 0 0 60px rgba(0, 30, 0, 0.55);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.tile:hover .tile-title { text-shadow: 0 0 14px var(--tile-glow); }
|
||||
.tile::after {
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
pointer-events: none;
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
rgba(0,0,0,0) 0px,
|
||||
rgba(0,0,0,0) 2px,
|
||||
rgba(0,0,0,0.22) 3px,
|
||||
rgba(0,0,0,0.22) 4px
|
||||
);
|
||||
opacity: 0.45;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
/* HUD-cyan variant for the scenes tile */
|
||||
.tile.cyan {
|
||||
--tile-fg: var(--hud);
|
||||
--tile-fg-hi: #c4ecff;
|
||||
--tile-dim: var(--hud-dim);
|
||||
--tile-edge: rgba(79, 210, 255, 0.45);
|
||||
--tile-glow: rgba(79, 210, 255, 0.55);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(79,210,255,0.05) 0%, rgba(0,0,0,0) 60%),
|
||||
var(--term-bg);
|
||||
color: var(--tile-fg);
|
||||
}
|
||||
/* Warn-yellow variant for external links */
|
||||
.tile.warn {
|
||||
--tile-fg: var(--warn);
|
||||
--tile-fg-hi: #ffe580;
|
||||
--tile-dim: rgba(255, 208, 0, 0.55);
|
||||
--tile-edge: rgba(255, 208, 0, 0.45);
|
||||
--tile-glow: rgba(255, 208, 0, 0.55);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255,208,0,0.04) 0%, rgba(0,0,0,0) 60%),
|
||||
var(--term-bg);
|
||||
color: var(--tile-fg);
|
||||
}
|
||||
|
||||
.tile-head {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--tile-dim);
|
||||
font-size: 11px; letter-spacing: 0.20em;
|
||||
}
|
||||
.tile-slug { color: var(--tile-fg-hi); }
|
||||
.tile-badge {
|
||||
border: 1px solid var(--tile-edge);
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
color: var(--tile-fg);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.tile-title {
|
||||
color: var(--tile-fg-hi);
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.05;
|
||||
text-shadow: 0 0 8px var(--tile-glow);
|
||||
transition: text-shadow 140ms ease;
|
||||
}
|
||||
.tile-desc {
|
||||
color: var(--tile-fg);
|
||||
opacity: 0.85;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.tile-foot {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
color: var(--tile-dim);
|
||||
font-size: 12px; letter-spacing: 0.08em;
|
||||
}
|
||||
.tile-target {
|
||||
color: var(--tile-fg-hi);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tile-target .caret { color: var(--tile-fg); margin-right: 6px; }
|
||||
.tile-foot .arrow { font-size: 18px; color: var(--tile-fg-hi); }
|
||||
|
||||
/* ---------- CRT overlays (full-viewport) ---------- */
|
||||
.scanlines, .vignette, .flicker { position: fixed; z-index: 999; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
body.page-shell { padding: 20px 28px; }
|
||||
.tiles { grid-template-columns: 1fr; grid-template-rows: auto; }
|
||||
.tile-title { font-size: 30px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="page-shell">
|
||||
@include('partials.topbar', ['crumb' => 'CONTROL PANEL'])
|
||||
|
||||
<section class="banner">
|
||||
<h1>STREAMING CONTROL</h1>
|
||||
<div class="lede"><span class="caret">▶</span>select an operation</div>
|
||||
</section>
|
||||
|
||||
<main class="tiles">
|
||||
<a class="tile" href="/onboard">
|
||||
<div class="tile-head">
|
||||
<span><span class="tile-slug">[01]</span> PRE-FLIGHT</span>
|
||||
<span class="tile-badge">INTERNAL</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tile-title">ONBOARD</div>
|
||||
<div class="tile-desc">Compile a Project Loading manifest — game, subtitle, countdown, camera/mic state. Pushes the channel title + category to Twitch.</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
<span class="tile-target"><span class="caret">▶</span>/onboard</span>
|
||||
<span class="arrow">→</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="tile cyan" href="/scenes">
|
||||
<div class="tile-head">
|
||||
<span><span class="tile-slug">[02]</span> ROUTES</span>
|
||||
<span class="tile-badge">INTERNAL</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tile-title">SCENES</div>
|
||||
<div class="tile-desc">Every overlay URL OBS browser-sources point at, plus the data endpoints (playlist, cover, audio stream).</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
<span class="tile-target"><span class="caret">▶</span>/scenes</span>
|
||||
<span class="arrow">→</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="tile warn" href="https://ophi118.com" target="_blank" rel="noopener">
|
||||
<div class="tile-head">
|
||||
<span><span class="tile-slug">[03]</span> LANDING</span>
|
||||
<span class="tile-badge">EXTERNAL ↗</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tile-title">OPHI118.COM</div>
|
||||
<div class="tile-desc">Public landing page for the OPHI-118 broadcast. Bio, schedule, social links.</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
<span class="tile-target"><span class="caret">▶</span>ophi118.com</span>
|
||||
<span class="arrow">↗</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a class="tile warn" href="https://gamez.ciemnosc.com" target="_blank" rel="noopener">
|
||||
<div class="tile-head">
|
||||
<span><span class="tile-slug">[04]</span> SERVERS</span>
|
||||
<span class="tile-badge">EXTERNAL ↗</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tile-title">GAMEZ.CIEMNOSC.COM</div>
|
||||
<div class="tile-desc">Roster of the self-hosted game servers currently live — connect strings, mod lists, player counts.</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
<span class="tile-target"><span class="caret">▶</span>gamez.ciemnosc.com</span>
|
||||
<span class="arrow">↗</span>
|
||||
</div>
|
||||
</a>
|
||||
</main>
|
||||
|
||||
<div class="scanlines"></div>
|
||||
<div class="vignette"></div>
|
||||
<div class="flicker"></div>
|
||||
|
||||
<script>
|
||||
// UTC clock — same look as the in-scene HUD strip.
|
||||
const clock = document.getElementById('clock');
|
||||
function tick() {
|
||||
const d = new Date();
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
clock.textContent = `${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
|
||||
}
|
||||
tick();
|
||||
setInterval(tick, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,34 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OPHI-118 / overlays</title>
|
||||
<style>
|
||||
body { background:#0a0e0a; color:#5fdc62; font:14px/1.6 monospace; padding:24px; }
|
||||
a { color:#97f99a; }
|
||||
h1 { color:#4fd2ff; letter-spacing:.2em; font-size:14px; margin-bottom:12px; }
|
||||
ul { list-style:none; padding:0; }
|
||||
li { padding:3px 0; }
|
||||
small { color:#2c8d2f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>OPHI-118 SCENE OVERLAY APP</h1>
|
||||
<ul>
|
||||
<li><a href="/landing">/landing</a> <small>— stand-by</small></li>
|
||||
<li><a href="/loading">/loading</a> <small>— project load + countdown</small></li>
|
||||
<li><a href="/game">/game</a> <small>— game HUD (camera-tracked)</small></li>
|
||||
<li><a href="/desktop">/desktop</a> <small>— desktop HUD (camera + screen)</small></li>
|
||||
<li><a href="/goodbye">/goodbye</a> <small>— sign-off</small></li>
|
||||
<li><a href="/music-box">/music-box</a> <small>— full music box</small></li>
|
||||
<li><a href="/music-box/widget">/music-box/widget</a> <small>— 549×880 widget</small></li>
|
||||
<li><a href="/music-box/cover">/music-box/cover</a> <small>— cover art</small></li>
|
||||
<li><a href="/music-box/cover?bars=0">/music-box/cover?bars=0</a> <small>— compact cover</small></li>
|
||||
<li><a href="/music-box/nc">/music-box/nc</a> <small>— ncurses-styled music box</small></li>
|
||||
<li><a href="/music-daemon">/music-daemon</a> <small>— audio worker</small></li>
|
||||
<li> </li>
|
||||
<li><a href="/data/playlist.js">/data/playlist.js</a> <small>— window.__PLAYLIST</small></li>
|
||||
<li><a href="/cover.jpg">/cover.jpg</a> <small>— album art</small></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,30 +4,26 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OPHI-118 / onboard</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
|
||||
<style>
|
||||
/* Onboard wizard is the only page that's not an OBS browser_source — it
|
||||
renders in a real desktop browser at arbitrary viewport sizes. So we
|
||||
break from the canvas-pixel design language and use a centered
|
||||
terminal-card layout with breathing room. */
|
||||
body.overlay-body {
|
||||
padding: 24px;
|
||||
body.page-shell {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 24px;
|
||||
min-height: 100vh;
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
}
|
||||
.wizard {
|
||||
width: min(960px, 100%);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 360px;
|
||||
grid-template-columns: 1fr 480px;
|
||||
gap: 22px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
@media (max-width: 1100px) {
|
||||
.wizard { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@@ -362,12 +358,65 @@
|
||||
#toast.ok { border-color: var(--term-fg-bright); }
|
||||
#toast pre { white-space: pre-wrap; margin: 6px 0 0; color: var(--term-fg-dim); }
|
||||
|
||||
/* Header strip mirrors the on-stream HUD chrome. */
|
||||
.hud {
|
||||
font-size: 16px;
|
||||
padding: 0 4px;
|
||||
/* Olden Era extra module — appears only when the picked game is Olden Era. */
|
||||
#olden-module[hidden] { display: none !important; }
|
||||
#olden-module {
|
||||
margin-top: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px dashed var(--term-edge);
|
||||
}
|
||||
#olden-module::before {
|
||||
content: '┤ HEROES OF MIGHT & MAGIC: OLDEN ERA ├';
|
||||
display: block;
|
||||
color: var(--term-fg-bright);
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.28em;
|
||||
margin-bottom: 14px;
|
||||
text-shadow: 0 0 6px var(--term-glow);
|
||||
}
|
||||
.olden-pick {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.olden-pick select {
|
||||
flex: 1 1 auto;
|
||||
background: rgba(0, 30, 0, 0.45);
|
||||
border: 1px solid var(--term-edge);
|
||||
border-radius: 3px;
|
||||
color: var(--term-fg-bright);
|
||||
font: inherit;
|
||||
font-size: 15px;
|
||||
padding: 8px 12px;
|
||||
outline: 0;
|
||||
text-shadow: 0 0 6px var(--term-glow);
|
||||
}
|
||||
.olden-pick select:focus {
|
||||
border-color: var(--term-fg-bright);
|
||||
box-shadow: 0 0 0 1px var(--term-fg-bright), 0 0 18px rgba(95, 220, 98, 0.25);
|
||||
}
|
||||
.olden-preview {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
object-fit: contain;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid var(--term-edge);
|
||||
border-radius: 3px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.olden-preview.olden-portrait { object-fit: cover; }
|
||||
.olden-preview:not([src]),
|
||||
.olden-preview[src=""] { opacity: 0.25; }
|
||||
.toggle.olden-color button.olden-red.on {
|
||||
background: rgba(230, 58, 46, 0.20);
|
||||
color: #ff6a5e;
|
||||
text-shadow: 0 0 6px rgba(230, 58, 46, 0.55);
|
||||
}
|
||||
.toggle.olden-color button.olden-blue.on {
|
||||
background: rgba(58, 166, 255, 0.20);
|
||||
color: #6ec5ff;
|
||||
text-shadow: 0 0 6px rgba(58, 166, 255, 0.55);
|
||||
}
|
||||
.hud .group { gap: 18px; }
|
||||
|
||||
/* Footer note. */
|
||||
.foot-note {
|
||||
@@ -378,22 +427,13 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="overlay-body">
|
||||
<body class="page-shell">
|
||||
|
||||
<header class="hud hud--onair">
|
||||
<div class="group">
|
||||
<span><span class="led"></span>ONBOARD</span>
|
||||
<span>OPHI-118 // PROJECT MANIFEST</span>
|
||||
</div>
|
||||
<div class="group">
|
||||
<span class="dim">RIG</span>
|
||||
<span id="rig-name">{{ $telemetry['rig'] ?? '—' }}</span>
|
||||
</div>
|
||||
<div class="group">
|
||||
<span class="dim">UTC</span>
|
||||
<span id="clock">--:--:--</span>
|
||||
</div>
|
||||
</header>
|
||||
@include('partials.topbar', [
|
||||
'crumb' => 'ONBOARD',
|
||||
'context' => '<span class="dim">RIG</span><span id="rig-name">'
|
||||
. e($telemetry['rig'] ?? '—') . '</span>',
|
||||
])
|
||||
|
||||
<main class="wizard">
|
||||
<section class="panel" id="form-panel">
|
||||
@@ -498,6 +538,65 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="olden-module" hidden>
|
||||
<div class="form-row">
|
||||
<label for="ol-my-faction">My Faction</label>
|
||||
<div class="field olden-pick">
|
||||
<select id="ol-my-faction" name="olden[my][faction]"></select>
|
||||
<img class="olden-preview" id="ol-my-faction-img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ol-my-hero">My Main Hero</label>
|
||||
<div class="field olden-pick">
|
||||
<select id="ol-my-hero" name="olden[my][hero]"></select>
|
||||
<img class="olden-preview olden-portrait" id="ol-my-hero-img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ol-enemy-faction">Enemy Faction</label>
|
||||
<div class="field olden-pick">
|
||||
<select id="ol-enemy-faction" name="olden[enemy][faction]"></select>
|
||||
<img class="olden-preview" id="ol-enemy-faction-img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ol-enemy-hero">Enemy Main Hero</label>
|
||||
<div class="field olden-pick">
|
||||
<select id="ol-enemy-hero" name="olden[enemy][hero]"></select>
|
||||
<img class="olden-preview olden-portrait" id="ol-enemy-hero-img" alt="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>My Color</label>
|
||||
<div class="field">
|
||||
<div class="toggle olden-color" data-toggle="oldenColor" data-value="red">
|
||||
<button type="button" data-val="red" class="olden-red on">RED · LEFT</button>
|
||||
<button type="button" data-val="blue" class="olden-blue">BLUE · RIGHT</button>
|
||||
</div>
|
||||
<input type="hidden" id="ol-color" name="oldenColor" value="red">
|
||||
<div class="hint">Red → your nick on the matchup's left side, in red. Blue → right side, in blue.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="ol-map">Map / Template</label>
|
||||
<div class="field">
|
||||
<div class="prompt-line">
|
||||
<span class="caret">▶</span>
|
||||
<input
|
||||
type="text" id="ol-map" name="olden[map]"
|
||||
autocomplete="off" spellcheck="false"
|
||||
placeholder="e.g. Crimson Tide">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn" id="submit-btn">
|
||||
<span class="spinner" id="spinner"></span>WRITE MANIFEST
|
||||
@@ -563,7 +662,7 @@
|
||||
group.querySelectorAll('button').forEach(b => {
|
||||
b.classList.toggle('on', b.dataset.val === val);
|
||||
});
|
||||
hidden.value = val === 'true' ? '1' : '0';
|
||||
hidden.value = val === 'true' ? '1' : val === 'false' ? '0' : val;
|
||||
renderPreview();
|
||||
});
|
||||
});
|
||||
@@ -593,6 +692,7 @@
|
||||
|
||||
function setGameId(id) {
|
||||
gameInput.dataset.gameId = id || '';
|
||||
syncOldenVisibility();
|
||||
}
|
||||
function clearGameId() {
|
||||
if (gameInput.dataset.gameId) setGameId('');
|
||||
@@ -725,6 +825,84 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ─── olden era module ──────────────────────────────────────────────────
|
||||
const OLDEN_GAME_ID = @json(\App\Support\OldenEra::GAME_ID);
|
||||
const OLDEN_CATALOG = @json($oldenEra['catalog']);
|
||||
const OLDEN_SAVED = @json($oldenEra['manifest']);
|
||||
const oldenModule = $('#olden-module');
|
||||
const olColorInput = $('#ol-color');
|
||||
const olColorGroup = document.querySelector('.toggle.olden-color');
|
||||
const olMap = $('#ol-map');
|
||||
|
||||
function syncOldenVisibility() {
|
||||
if (!oldenModule) return;
|
||||
oldenModule.hidden = gameInput.dataset.gameId !== OLDEN_GAME_ID;
|
||||
}
|
||||
function fillFactionSelect(sel) {
|
||||
sel.innerHTML = Object.keys(OLDEN_CATALOG)
|
||||
.map(f => `<option value="${escapeHtml(f)}">${escapeHtml(f)}</option>`).join('');
|
||||
}
|
||||
function fillHeroSelect(side, faction) {
|
||||
const sel = $(`#ol-${side}-hero`);
|
||||
const heroes = (OLDEN_CATALOG[faction] || {}).heroes || [];
|
||||
sel.innerHTML = heroes
|
||||
.map(h => `<option value="${escapeHtml(h.name)}">${escapeHtml(h.name)}</option>`).join('');
|
||||
}
|
||||
function updateFactionImg(side) {
|
||||
const f = $(`#ol-${side}-faction`).value;
|
||||
$(`#ol-${side}-faction-img`).src = (OLDEN_CATALOG[f] || {}).logo || '';
|
||||
}
|
||||
function updateHeroImg(side) {
|
||||
const f = $(`#ol-${side}-faction`).value;
|
||||
const h = $(`#ol-${side}-hero`).value;
|
||||
const hero = ((OLDEN_CATALOG[f] || {}).heroes || []).find(x => x.name === h);
|
||||
$(`#ol-${side}-hero-img`).src = hero ? hero.portrait : '';
|
||||
}
|
||||
function setColor(val) {
|
||||
if (val !== 'red' && val !== 'blue') val = 'red';
|
||||
olColorInput.value = val;
|
||||
olColorGroup.dataset.value = val;
|
||||
olColorGroup.querySelectorAll('button').forEach(b => {
|
||||
b.classList.toggle('on', b.dataset.val === val);
|
||||
});
|
||||
}
|
||||
|
||||
['my', 'enemy'].forEach(side => {
|
||||
const factionSel = $(`#ol-${side}-faction`);
|
||||
const heroSel = $(`#ol-${side}-hero`);
|
||||
fillFactionSelect(factionSel);
|
||||
factionSel.addEventListener('change', () => {
|
||||
fillHeroSelect(side, factionSel.value);
|
||||
updateFactionImg(side);
|
||||
updateHeroImg(side);
|
||||
});
|
||||
heroSel.addEventListener('change', () => updateHeroImg(side));
|
||||
});
|
||||
|
||||
// Pre-populate from saved olden-era-manifest.json (if any).
|
||||
(function preFillOlden() {
|
||||
const saved = OLDEN_SAVED || {};
|
||||
const factions = Object.keys(OLDEN_CATALOG);
|
||||
if (!factions.length) return;
|
||||
const pick = (want, side) => {
|
||||
const f = factions.includes(want?.faction) ? want.faction : factions[0];
|
||||
$(`#ol-${side}-faction`).value = f;
|
||||
fillHeroSelect(side, f);
|
||||
const heroes = (OLDEN_CATALOG[f] || {}).heroes || [];
|
||||
const wantedHero = want?.hero && heroes.find(h => h.name === want.hero) ? want.hero
|
||||
: (heroes[0] && heroes[0].name) || '';
|
||||
$(`#ol-${side}-hero`).value = wantedHero;
|
||||
updateFactionImg(side);
|
||||
updateHeroImg(side);
|
||||
};
|
||||
pick(saved.my, 'my');
|
||||
pick(saved.enemy, 'enemy');
|
||||
setColor(saved.color || 'red');
|
||||
olMap.value = saved.map || '';
|
||||
})();
|
||||
|
||||
syncOldenVisibility();
|
||||
|
||||
// ─── submit ────────────────────────────────────────────────────────────
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -740,6 +918,14 @@
|
||||
microphone: micHidden.value === '1',
|
||||
pushTwitch: pushInput.checked,
|
||||
};
|
||||
if (!oldenModule.hidden) {
|
||||
payload.olden = {
|
||||
color: olColorInput.value || 'red',
|
||||
my: { faction: $('#ol-my-faction').value, hero: $('#ol-my-hero').value },
|
||||
enemy: { faction: $('#ol-enemy-faction').value, hero: $('#ol-enemy-hero').value },
|
||||
map: olMap.value.trim() || null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const r = await fetch('/onboard', {
|
||||
method: 'POST',
|
||||
@@ -756,6 +942,7 @@
|
||||
return;
|
||||
}
|
||||
const lines = [`✓ wrote ${body.path}`];
|
||||
if (body.oldenPath) lines.push(`✓ wrote ${body.oldenPath}`);
|
||||
if (body.twitch) lines.push(`twitch: ${body.twitch}`);
|
||||
lines.push('→ refresh the Project Loading browser source in OBS.');
|
||||
toast.className = 'on ok';
|
||||
|
||||
@@ -56,6 +56,37 @@
|
||||
<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>
|
||||
|
||||
19
webapp/resources/views/partials/topbar.blade.php
Normal file
19
webapp/resources/views/partials/topbar.blade.php
Normal file
@@ -0,0 +1,19 @@
|
||||
{{--
|
||||
Shared breadcrumb / nav strip used by every non-overlay webapp page
|
||||
(/, /scenes, /onboard). Distinct from the in-scene .hud strip so OBS
|
||||
overlays render identically — only browser-facing pages use this.
|
||||
|
||||
@param string $crumb uppercase label for the current page (e.g. 'ONBOARD')
|
||||
@param string $context optional raw HTML for the middle slot;
|
||||
defaults to the node address.
|
||||
--}}
|
||||
<header class="crumb-bar">
|
||||
<nav class="crumbs">
|
||||
<span class="led" aria-hidden="true"></span>
|
||||
<a href="/" class="crumb crumb-root">OPHI-118</a>
|
||||
<span class="sep">›</span>
|
||||
<span class="crumb crumb-here">{{ $crumb }}</span>
|
||||
</nav>
|
||||
<div class="crumb-ctx">{!! $context ?? '<span class="dim">NODE</span>127.0.0.1:1118' !!}</div>
|
||||
<div class="crumb-clock"><span class="dim">UTC</span><span id="clock">--:--:--</span></div>
|
||||
</header>
|
||||
107
webapp/resources/views/scenes.blade.php
Normal file
107
webapp/resources/views/scenes.blade.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>OPHI-118 / scenes</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/hud.css') }}">
|
||||
<style>
|
||||
html, body {
|
||||
color: var(--term-fg);
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--term-fg-bright);
|
||||
text-shadow: 0 0 8px var(--term-glow);
|
||||
font-size: 18px; font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.lede {
|
||||
color: var(--term-fg-dim);
|
||||
font-size: 13px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
color: var(--hud); letter-spacing: 0.14em;
|
||||
font-size: 12px; font-weight: 700;
|
||||
margin: 22px 0 8px;
|
||||
border-bottom: 1px dashed var(--hud-dim);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
ul { list-style: none; padding: 0; }
|
||||
li { padding: 4px 0; display: flex; gap: 14px; align-items: baseline; }
|
||||
li::before {
|
||||
content: '▸';
|
||||
color: var(--term-fg-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
a.route {
|
||||
color: var(--term-fg-bright);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent;
|
||||
min-width: 280px;
|
||||
display: inline-block;
|
||||
}
|
||||
a.route:hover {
|
||||
text-shadow: 0 0 8px var(--term-glow);
|
||||
border-bottom-color: var(--term-fg);
|
||||
}
|
||||
.note { color: var(--term-fg-dim); }
|
||||
|
||||
.scanlines, .vignette, .flicker { position: fixed; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="page-shell">
|
||||
@include('partials.topbar', ['crumb' => 'SCENES'])
|
||||
|
||||
<h1>SCENE OVERLAY ROUTES</h1>
|
||||
<div class="lede">Browser sources in <code>basic/scenes/Default_Stream_HUD.json</code> point at these URLs on 127.0.0.1:1118.</div>
|
||||
|
||||
<div class="group-title">OVERLAYS</div>
|
||||
<ul>
|
||||
<li><a class="route" href="/landing">/landing</a> <span class="note">— stand-by</span></li>
|
||||
<li><a class="route" href="/loading">/loading</a> <span class="note">— project load + countdown</span></li>
|
||||
<li><a class="route" href="/game">/game</a> <span class="note">— game HUD (camera-tracked)</span></li>
|
||||
<li><a class="route" href="/desktop">/desktop</a> <span class="note">— desktop HUD (camera + screen)</span></li>
|
||||
<li><a class="route" href="/goodbye">/goodbye</a> <span class="note">— sign-off</span></li>
|
||||
<li><a class="route" href="/music-daemon">/music-daemon</a> <span class="note">— passive WS listener</span></li>
|
||||
</ul>
|
||||
|
||||
<div class="group-title">MUSIC BOX VARIANTS</div>
|
||||
<ul>
|
||||
<li><a class="route" href="/music-box">/music-box</a> <span class="note">— full music box</span></li>
|
||||
<li><a class="route" href="/music-box/widget">/music-box/widget</a> <span class="note">— 549×880 widget</span></li>
|
||||
<li><a class="route" href="/music-box/cover">/music-box/cover</a> <span class="note">— cover art</span></li>
|
||||
<li><a class="route" href="/music-box/cover?bars=0">/music-box/cover?bars=0</a> <span class="note">— compact cover</span></li>
|
||||
<li><a class="route" href="/music-box/nc">/music-box/nc</a> <span class="note">— ncurses-styled music box</span></li>
|
||||
</ul>
|
||||
|
||||
<div class="group-title">DATA ENDPOINTS</div>
|
||||
<ul>
|
||||
<li><a class="route" href="/data/playlist.js">/data/playlist.js</a> <span class="note">— window.__PLAYLIST</span></li>
|
||||
<li><a class="route" href="/cover.jpg">/cover.jpg</a> <span class="note">— current album art</span></li>
|
||||
<li><a class="route" href="/track">/track?p=<base64></a> <span class="note">— audio stream</span></li>
|
||||
</ul>
|
||||
|
||||
<div class="scanlines"></div>
|
||||
<div class="vignette"></div>
|
||||
<div class="flicker"></div>
|
||||
|
||||
<script>
|
||||
const clock = document.getElementById('clock');
|
||||
function tick() {
|
||||
const d = new Date();
|
||||
const p = n => String(n).padStart(2, '0');
|
||||
clock.textContent = `${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`;
|
||||
}
|
||||
tick(); setInterval(tick, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user