Styling fixes and Olden Era mmodule
181
webapp/README.md
@@ -1,58 +1,169 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
# OPHI-118 webapp
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
Laravel app that renders every on-stream overlay for the OPHI-118 Twitch rig and
|
||||
hosts the streamer's control surfaces. Runs under supervisord on `http://127.0.0.1:1118/`.
|
||||
|
||||
## About Laravel
|
||||
> Looking for the rig-wide picture (OBS, MPD, twitch-bot, deployment)?
|
||||
> See [`../README.md`](../README.md). This file is just the webapp.
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
## What's in here
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
### Control panel — `/`
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
Tile-based dashboard. Entry point from a browser bookmark; not used by OBS itself.
|
||||
|
||||
## Learning Laravel
|
||||

|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
|
||||
- **ONBOARD** — opens the project-loading wizard.
|
||||
- **SCENES** — opens the full route index.
|
||||
- **ophi118.com** — public channel landing page.
|
||||
- **gamez.ciemnosc.com** — self-hosted game-server roster.
|
||||
|
||||
In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
### Onboard wizard — `/onboard`
|
||||
|
||||
You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals.
|
||||
Web equivalent of `php artisan rig:loading`. Compiles the Project Loading manifest
|
||||
(game, subtitle, countdown, camera/mic state) and optionally pushes the channel
|
||||
title + category to Twitch via Helix.
|
||||
|
||||
## Agentic Development
|
||||

|
||||
|
||||
Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow:
|
||||
- **Type-ahead game search** against the Twitch Helix `search/categories`
|
||||
endpoint, debounced. ↑/↓/Enter/Esc keys to pick a match; locks the `gameId`.
|
||||
- **Live preview** mirrors what the `/loading` overlay will render — every edit
|
||||
re-renders the terminal-style output immediately.
|
||||
- **Stepper** for the countdown, **ENABLED/DISABLED toggles** for camera/mic,
|
||||
**push-to-Twitch** checkbox to commit the channel update via Helix.
|
||||
- **Graceful fallback**: with `TWITCH_CLIENT_ID` / `TWITCH_ACCESS_TOKEN` unset,
|
||||
the search disables and the manifest still saves locally.
|
||||
|
||||

|
||||
|
||||
### Scenes index — `/scenes`
|
||||
|
||||
All overlay URLs the OBS browser-sources point at, plus the data endpoints.
|
||||
|
||||

|
||||
|
||||
### Overlay routes
|
||||
|
||||
Browser sources in `basic/scenes/Default_Stream_HUD.json` consume these. Edits
|
||||
to the Blade views are picked up on the next OBS **Refresh cache** — no server
|
||||
restart needed.
|
||||
|
||||
| Route | Purpose |
|
||||
|---|---|
|
||||
| `/landing` | Fallout-style "PLEASE STAND BY" + telemetry rotator |
|
||||
| `/loading` | Terminal-style Project Loading + countdown |
|
||||
| `/game` | Game HUD (camera-frame OBS-WS sync, status bar) |
|
||||
| `/desktop` | Desktop HUD (camera + screen-capture frames) |
|
||||
| `/goodbye` | Sign-off terminal + "OFFLINE FOR" counter |
|
||||
| `/music-box` | Full-screen music box terminal |
|
||||
| `/music-box/widget` | Compact 549×880 widget |
|
||||
| `/music-box/cover` | Album-art widget (`?bars=0` for compact mode) |
|
||||
| `/music-box/nc` | ncurses-styled 2-pane variant |
|
||||
| `/music-daemon` | Passive WebSocket listener (diagnostic) |
|
||||
|
||||
### Data + control endpoints
|
||||
|
||||
| Route | Notes |
|
||||
|---|---|
|
||||
| `GET /data/playlist.js` | Returns `window.__PLAYLIST = {...};` with ETag |
|
||||
| `GET /cover.jpg` | Streams `bridges/cover.jpg` (kept fresh by `mpd-state.py`) |
|
||||
| `GET /track?p=<base64>` | Audio stream — path-traversal-guarded |
|
||||
| `POST /cmd/{skip\|prev\|pause\|resume}` | Broadcasts `mpd:cmd` over OBS WebSocket |
|
||||
|
||||
## Artisan commands
|
||||
|
||||
Each lives at `app/Console/Commands/Rig*Command.php`. Run from the `webapp/`
|
||||
directory.
|
||||
|
||||
```bash
|
||||
composer require laravel/boost --dev
|
||||
|
||||
php artisan boost:install
|
||||
php artisan rig:setup # gen public/js/obs-config.js + OBS_WS_* in .env
|
||||
php artisan rig:telemetry # refresh storage/data/telemetry.json
|
||||
php artisan rig:telemetry --collect # re-prompt every field interactively
|
||||
php artisan rig:loading # interactive Project Loading manifest (CLI mirror of /onboard)
|
||||
php artisan rig:playlist # reindex audio dirs → storage/data/playlist.json
|
||||
php artisan rig:cmd skip # CLI mirror of POST /cmd/skip
|
||||
```
|
||||
|
||||
Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices.
|
||||
## Running it
|
||||
|
||||
## Contributing
|
||||
The supervisord program file lives at [`../scripts/obs-webapp.supervisord.conf`](../scripts/obs-webapp.supervisord.conf)
|
||||
and is installed (root-owned) to `/etc/supervisor.d/obs-webapp.conf` by
|
||||
`scripts/deploy-rig.sh`.
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
```bash
|
||||
sudo supervisorctl status obs-webapp # → RUNNING
|
||||
sudo supervisorctl restart obs-webapp # after route / controller / .env changes
|
||||
tail -F storage/logs/supervisord.{out,err}.log
|
||||
```
|
||||
|
||||
## Code of Conduct
|
||||
For local hacking without supervisord:
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
```bash
|
||||
php artisan serve --host=127.0.0.1 --port=1118
|
||||
```
|
||||
|
||||
## Security Vulnerabilities
|
||||
## When to restart
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
| Change | Action |
|
||||
|---|---|
|
||||
| Blade view (`resources/views/`) | OBS → right-click source → **Refresh cache** |
|
||||
| Static CSS / JS (`public/`) | Same — Refresh cache |
|
||||
| Route / controller / service / config / `.env` | `sudo supervisorctl restart obs-webapp` |
|
||||
|
||||
## License
|
||||
## Layout
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
```
|
||||
webapp/
|
||||
├── app/
|
||||
│ ├── Console/Commands/ rig:setup, rig:telemetry, rig:loading,
|
||||
│ │ rig:playlist, rig:cmd
|
||||
│ ├── Http/Controllers/ Overlays/{Landing,Loading,Game,Desktop,
|
||||
│ │ Goodbye,MusicBox,MusicDaemon}, Data,
|
||||
│ │ MusicCommand, Audio, Onboard
|
||||
│ ├── Services/ ObsWsClient (Pawl), TwitchHelix,
|
||||
│ │ HardwareSnapshot
|
||||
│ └── Support/RigData.php
|
||||
├── resources/
|
||||
│ ├── views/
|
||||
│ │ ├── home.blade.php /
|
||||
│ │ ├── scenes.blade.php /scenes
|
||||
│ │ ├── onboard.blade.php /onboard
|
||||
│ │ ├── overlays/ per-scene overlays
|
||||
│ │ ├── partials/ topbar, hud-strip, crt-overlays, …
|
||||
│ │ └── layouts/ overlay base layout
|
||||
│ └── img/ favicon + screenshots (this README)
|
||||
├── public/
|
||||
│ ├── css/hud.css shared HUD chrome, scanlines, terminal styling
|
||||
│ ├── js/ hud, crt-static, obs-ws-bootstrap, obs-ws-mini
|
||||
│ ├── audio/static-hum.wav ffmpeg_source (not browser-served)
|
||||
│ └── favicon.ico → symlink to ../resources/img/favicon.ico
|
||||
├── routes/web.php
|
||||
├── config/rig.php storage paths, OBS WS creds, Twitch keys
|
||||
└── storage/data/ rig:* commands write JSON here
|
||||
├── loading.json
|
||||
├── playlist.json
|
||||
└── telemetry.json
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
`.env` keys the webapp cares about:
|
||||
|
||||
```
|
||||
APP_URL=http://127.0.0.1:1118
|
||||
|
||||
OBS_WS_URL=ws://localhost:4455
|
||||
OBS_WS_PASSWORD=… # written by `php artisan rig:setup`
|
||||
|
||||
TWITCH_CLIENT_ID=…
|
||||
TWITCH_ACCESS_TOKEN=… # legacy TWITCH_TOKEN is also accepted
|
||||
TWITCH_BROADCASTER_ID=…
|
||||
|
||||
RIG_NAME=…
|
||||
OBS_PROFILE=ophi118
|
||||
MUSIC_DIR_1=…
|
||||
```
|
||||
|
||||
See [`.env.example`](.env.example) for the full set.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\TwitchHelix;
|
||||
use App\Support\OldenEra;
|
||||
use App\Support\RigData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -18,13 +19,19 @@ use Throwable;
|
||||
*/
|
||||
class OnboardController extends Controller
|
||||
{
|
||||
public function show(RigData $rig, TwitchHelix $twitch)
|
||||
public function show(RigData $rig, TwitchHelix $twitch, OldenEra $olden)
|
||||
{
|
||||
return view('onboard', [
|
||||
'manifest' => $rig->loading(),
|
||||
'telemetry' => $rig->telemetry(),
|
||||
'trackCount' => $rig->playlistCount(),
|
||||
'twitchEnabled' => $twitch->enabled(),
|
||||
'oldenEra' => [
|
||||
'gameId' => OldenEra::GAME_ID,
|
||||
'assetBase' => OldenEra::URL,
|
||||
'catalog' => $olden->catalog(),
|
||||
'manifest' => $olden->manifest(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -47,7 +54,7 @@ class OnboardController extends Controller
|
||||
return response()->json(['matches' => $hits]);
|
||||
}
|
||||
|
||||
public function submit(Request $request, RigData $rig, TwitchHelix $twitch): JsonResponse
|
||||
public function submit(Request $request, RigData $rig, TwitchHelix $twitch, OldenEra $olden): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'game' => 'required|string|max:200',
|
||||
@@ -76,6 +83,28 @@ class OnboardController extends Controller
|
||||
json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"
|
||||
);
|
||||
|
||||
$oldenPath = null;
|
||||
if (($manifest['gameId'] ?? null) === OldenEra::GAME_ID) {
|
||||
$factions = array_keys($olden->catalog());
|
||||
$extra = $request->validate([
|
||||
'olden.color' => 'required|in:red,blue',
|
||||
'olden.my.faction' => 'required|string|in:' . implode(',', $factions),
|
||||
'olden.my.hero' => 'required|string|max:120',
|
||||
'olden.enemy.faction' => 'required|string|in:' . implode(',', $factions),
|
||||
'olden.enemy.hero' => 'required|string|max:120',
|
||||
'olden.map' => 'nullable|string|max:200',
|
||||
])['olden'];
|
||||
|
||||
$oldenPath = $olden->writeManifest([
|
||||
'compiledAt' => gmdate('Y-m-d\TH:i:s\Z'),
|
||||
'gameId' => OldenEra::GAME_ID,
|
||||
'color' => $extra['color'],
|
||||
'my' => $extra['my'],
|
||||
'enemy' => $extra['enemy'],
|
||||
'map' => isset($extra['map']) && $extra['map'] !== '' ? $extra['map'] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
$twitchStatus = null;
|
||||
if (!empty($data['pushTwitch']) && $twitch->enabled() && !empty($manifest['gameId'])) {
|
||||
try {
|
||||
@@ -92,6 +121,7 @@ class OnboardController extends Controller
|
||||
'manifest' => $manifest,
|
||||
'twitch' => $twitchStatus,
|
||||
'path' => str_replace(base_path() . '/', '', $path),
|
||||
'oldenPath' => $oldenPath ? str_replace(base_path() . '/', '', $oldenPath) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
namespace App\Http\Controllers\Overlays;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\OldenEra;
|
||||
use App\Support\RigData;
|
||||
|
||||
class GameController extends Controller
|
||||
{
|
||||
public function show(RigData $rig)
|
||||
public function show(RigData $rig, OldenEra $olden)
|
||||
{
|
||||
$manifest = $rig->loading();
|
||||
$isOlden = ($manifest['gameId'] ?? null) === OldenEra::GAME_ID;
|
||||
$oldenManifest = $isOlden ? $olden->manifest() : [];
|
||||
|
||||
return view('overlays.game', [
|
||||
'manifest' => $rig->loading(),
|
||||
'manifest' => $manifest,
|
||||
'olden' => !empty($oldenManifest) ? $oldenManifest : null,
|
||||
'oldenAssetBase' => OldenEra::URL,
|
||||
'rigName' => (string) (config('rig.streamer_nick') ?: 'OPHI118'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ use RuntimeException;
|
||||
* Thin Twitch Helix client. Replaces twitch-bot/search-game.py + set-channel.py
|
||||
* for use from `php artisan rig:loading`.
|
||||
*
|
||||
* Requires TWITCH_CLIENT_ID + TWITCH_TOKEN (user OAuth token with the
|
||||
* Requires TWITCH_CLIENT_ID + TWITCH_ACCESS_TOKEN (user OAuth token with the
|
||||
* channel:manage:broadcast scope) and TWITCH_BROADCASTER_ID in .env.
|
||||
* Legacy TWITCH_TOKEN is still accepted as a fallback (see config/rig.php).
|
||||
*/
|
||||
class TwitchHelix
|
||||
{
|
||||
|
||||
78
webapp/app/Support/OldenEra.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* Per-game extra manifest for Heroes of Might & Magic: Olden Era.
|
||||
*
|
||||
* `loading.json` keeps the generic stream metadata (game, subtitle, mic…).
|
||||
* Olden Era specifics — matchup factions, heroes, the player's team color,
|
||||
* map name — live in their own `olden-era-manifest.json` so the loading
|
||||
* schema doesn't grow a new key every time a game gets bespoke chrome.
|
||||
*/
|
||||
class OldenEra
|
||||
{
|
||||
/** Twitch Helix category id for "Heroes of Might & Magic: Olden Era". */
|
||||
public const GAME_ID = '1426873999';
|
||||
|
||||
/** Public URL prefix; resolved via the webapp/public/games symlink. */
|
||||
public const URL = '/games/olden-era';
|
||||
|
||||
private ?array $catalogCache = null;
|
||||
|
||||
/**
|
||||
* Faction → {logo URL, heroes list}. Built by globbing
|
||||
* resources/games/olden-era/{factions,heroes/<Faction>}/*.png.
|
||||
*/
|
||||
public function catalog(): array
|
||||
{
|
||||
if ($this->catalogCache !== null) return $this->catalogCache;
|
||||
|
||||
$root = resource_path('games/olden-era');
|
||||
$out = [];
|
||||
|
||||
foreach (glob($root . '/factions/*.png') ?: [] as $factionPng) {
|
||||
$faction = pathinfo($factionPng, PATHINFO_FILENAME);
|
||||
$heroes = [];
|
||||
foreach (glob($root . '/heroes/' . $faction . '/*.png') ?: [] as $heroPng) {
|
||||
$name = pathinfo($heroPng, PATHINFO_FILENAME);
|
||||
$heroes[] = [
|
||||
'name' => $name,
|
||||
'portrait' => self::URL . '/heroes/' . rawurlencode($faction) . '/' . rawurlencode($name) . '.png',
|
||||
];
|
||||
}
|
||||
usort($heroes, fn($a, $b) => strcasecmp($a['name'], $b['name']));
|
||||
$out[$faction] = [
|
||||
'logo' => self::URL . '/factions/' . rawurlencode($faction) . '.png',
|
||||
'heroes' => $heroes,
|
||||
];
|
||||
}
|
||||
|
||||
ksort($out);
|
||||
return $this->catalogCache = $out;
|
||||
}
|
||||
|
||||
public function manifest(): array
|
||||
{
|
||||
$path = $this->manifestPath();
|
||||
if (!is_file($path)) return [];
|
||||
$parsed = json_decode((string) @file_get_contents($path), true);
|
||||
return is_array($parsed) ? $parsed : [];
|
||||
}
|
||||
|
||||
public function writeManifest(array $data): string
|
||||
{
|
||||
$path = $this->manifestPath();
|
||||
@mkdir(dirname($path), 0775, true);
|
||||
file_put_contents(
|
||||
$path,
|
||||
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"
|
||||
);
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function manifestPath(): string
|
||||
{
|
||||
return rtrim(config('rig.storage_data'), '/') . '/olden-era-manifest.json';
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ return [
|
||||
|---------------------------------------------------------------------------
|
||||
*/
|
||||
'rig_name' => env('RIG_NAME'),
|
||||
'streamer_nick' => env('STREAMER_NICK', 'OPHI118'),
|
||||
'obs_studio_dir' => env('OBS_STUDIO_DIR', base_path('..')),
|
||||
'obs_profile' => env('OBS_PROFILE', 'ophi118'),
|
||||
|
||||
@@ -52,7 +53,9 @@ return [
|
||||
*/
|
||||
'twitch' => [
|
||||
'client_id' => env('TWITCH_CLIENT_ID'),
|
||||
'token' => env('TWITCH_TOKEN'),
|
||||
// Name aligned with twitch-bot/.env (TWITCH_ACCESS_TOKEN); old TWITCH_TOKEN
|
||||
// kept as a fallback so pre-existing webapp/.env files still work.
|
||||
'token' => env('TWITCH_ACCESS_TOKEN', env('TWITCH_TOKEN')),
|
||||
'broadcaster_id' => env('TWITCH_BROADCASTER_ID'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -574,3 +574,116 @@ body.camera-hud-body {
|
||||
}
|
||||
.np-block .title { color: var(--ink); }
|
||||
.np-block .time { color: var(--ink-dim); }
|
||||
|
||||
/* Olden Era matchup — right-aligned on the status-bar's LIVE/game-name line. */
|
||||
.matchup {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-size: 16px;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
.matchup .m-side {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.matchup .m-faction {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 6px rgba(7, 8, 13, 0.9));
|
||||
}
|
||||
.matchup .m-hero {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(232, 232, 224, 0.35);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.matchup .m-name {
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
.matchup .m-vs {
|
||||
color: var(--hud-dim);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.32em;
|
||||
font-size: 14px;
|
||||
}
|
||||
.matchup .m-on {
|
||||
color: var(--hud-dim);
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.32em;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.matchup .m-map {
|
||||
color: var(--hud);
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.18em;
|
||||
}
|
||||
.matchup-red .m-my .m-name {
|
||||
color: #ff6a5e;
|
||||
text-shadow: 0 0 6px rgba(230, 58, 46, 0.55);
|
||||
}
|
||||
.matchup-blue .m-my .m-name {
|
||||
color: #6ec5ff;
|
||||
text-shadow: 0 0 6px rgba(58, 166, 255, 0.55);
|
||||
}
|
||||
.matchup-red .m-my .m-hero { border-color: rgba(230, 58, 46, 0.7); }
|
||||
.matchup-blue .m-my .m-hero { border-color: rgba(58, 166, 255, 0.7); }
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
* Webapp navigation breadcrumb (used by /, /scenes, /onboard).
|
||||
* Distinct from the in-scene .hud strip so overlays stay untouched.
|
||||
* ──────────────────────────────────────────────────────────────── */
|
||||
.crumb-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--hud);
|
||||
letter-spacing: 0.10em;
|
||||
padding-bottom: 14px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid rgba(79, 210, 255, 0.25);
|
||||
}
|
||||
.crumb-bar > :nth-child(2) { justify-self: center; }
|
||||
.crumb-bar > :nth-child(3) { justify-self: end; }
|
||||
|
||||
.crumbs { display: flex; align-items: center; gap: 12px; }
|
||||
.crumbs .led {
|
||||
display: inline-block;
|
||||
width: 9px; height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--onair);
|
||||
box-shadow: 0 0 8px var(--onair);
|
||||
animation: rec-blink 1.6s ease-in-out infinite;
|
||||
}
|
||||
.crumb { color: var(--hud); text-decoration: none; transition: color 120ms ease, text-shadow 120ms ease; }
|
||||
.crumb-root { color: var(--hud-dim); }
|
||||
.crumb-root:hover{ color: var(--hud); text-shadow: 0 0 8px var(--hud-glow); }
|
||||
.crumb-here { color: var(--hud); text-shadow: 0 0 8px var(--hud-glow); cursor: default; }
|
||||
.crumbs .sep { color: var(--hud-dim); opacity: 0.6; }
|
||||
|
||||
.crumb-ctx, .crumb-clock { color: var(--hud); }
|
||||
.crumb-ctx .dim, .crumb-clock .dim { color: var(--hud-dim); margin-right: 8px; }
|
||||
|
||||
/* ────────────────────────────────────────────────────────────────
|
||||
* Webapp page shell — used by every non-overlay browser page
|
||||
* (/, /scenes, /onboard) so the crumb-bar and content sit at the
|
||||
* same viewport anchor on every page.
|
||||
* Overlays keep .overlay-body / .camera-hud-body for OBS sizing.
|
||||
* ──────────────────────────────────────────────────────────────── */
|
||||
body.page-shell {
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
padding: 32px 56px;
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
1
webapp/public/favicon.ico
Symbolic link
@@ -0,0 +1 @@
|
||||
../resources/img/favicon.ico
|
||||
1
webapp/public/games
Symbolic link
@@ -0,0 +1 @@
|
||||
../resources/games
|
||||
1
webapp/resources/games/olden-era/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Files here belongs to Unfrozen and are copies from https://wiki.hoodedhorse.com/Heroes_of_Might_and_Magic_Olden_Era - used only for game streaming purposes.
|
||||
BIN
webapp/resources/games/olden-era/factions/Dungeon.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
webapp/resources/games/olden-era/factions/Grove.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
webapp/resources/games/olden-era/factions/Hive.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
webapp/resources/games/olden-era/factions/Necropolis.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
webapp/resources/games/olden-era/factions/Schism.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
webapp/resources/games/olden-era/factions/Temple.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Enatee.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Glastor.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Kieran.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Lodos.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Motley.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Mouaren.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Rhea.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Sister Deira.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Stinger.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Typhona.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Dungeon/Ylwari.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Aeliniel.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Alluring Sh'a.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Aunt Daliar.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Echolily.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Eith.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Elder Tss'kish.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Faleor.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Gingertail.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Glacia.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Halon.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Mreowa.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Octavia.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Old Pilgrim.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Sullie.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/The Minstrel.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Vatawna.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
webapp/resources/games/olden-era/heroes/Grove/Vim.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Fleu.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Goldentongue.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Groo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Khariseth.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Leira.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Lo.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Maelstrom.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Mila.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Niev.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Nor.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Oriax.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Pauper.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Tavi.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
webapp/resources/games/olden-era/heroes/Hive/Xirr.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Adahn.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Bulwark.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Ethric.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Funerella.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Kel'Ghul.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Laura.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Mag.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Marl.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Natalida.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Onkos.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Tarius.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
webapp/resources/games/olden-era/heroes/Necropolis/Zam.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Blackhorn.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Dhüvri.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Jänhei.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Kwinri.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Mara Mat’ha.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Martyr Tho.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
webapp/resources/games/olden-era/heroes/Schism/Nihil.png
Normal file
|
After Width: | Height: | Size: 11 KiB |