Styling fixes and Olden Era mmodule

This commit is contained in:
Jakub Zych
2026-05-21 15:00:12 +02:00
parent 4ceff4015a
commit dd3493921b
136 changed files with 1028 additions and 111 deletions

View File

@@ -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';