Scripts, scenes, bridges, bots and more

This commit is contained in:
Jakub Zych
2026-04-26 23:08:48 +02:00
parent a34cd5fab7
commit fb5aaed9e7
32 changed files with 2985 additions and 4382 deletions

View File

@@ -19,9 +19,18 @@
--display: 'DejaVu Sans', 'Liberation Sans', 'Helvetica', 'Arial', sans-serif;
--pad-y: 40px;
--pad-x: 72px;
/* Camera frame size — match this in OBS when positioning the camera source */
--cam-w: 480px;
--cam-h: 270px;
/* Camera frame transform — auto-synced from OBS at runtime via
GetSceneItemTransform + SceneItemTransformChanged. These defaults
are only the first-paint fallback; once the WS connects the box
snaps to wherever the Camera source actually is in the Game scene. */
--cam-x: 1855px;
--cam-y: 128px;
--cam-w: 580px;
--cam-h: 326px;
/* Visual outset around the camera so the cyan border + corner ticks
breathe and stay visible on all four sides (rather than getting
overdrawn by the camera image). Applied symmetrically. */
--cam-pad: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -55,32 +64,44 @@
.hud > .group:nth-child(2) { justify-self: center; }
.hud > .group:nth-child(3) { justify-self: end; }
.hud .dim { color: var(--hud-dim); }
.hud .group > span { display: inline-flex; align-items: center; gap: 10px; }
.hud .led {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 10px var(--accent);
margin-right: 10px;
vertical-align: middle;
/* Small upward nudge: flex centers on line-box mid, but caps-only text
reads centered around cap-height mid which sits a touch higher. */
margin-top: -0.08em;
animation: rec-blink 1.6s ease-in-out infinite;
}
/* The ▮/▯ glyphs in DejaVu Sans Mono are designed centered on x-height,
not cap-height, so against all-caps SIGNAL their visual middle sits low.
Lift by ~(cap-mid x-mid) ≈ 0.10em to put the glyph center on cap center. */
#signal {
display: inline-block;
transform: translateY(-0.10em);
}
@keyframes rec-blink {
0%, 55% { opacity: 1; }
65%, 100% { opacity: 0.2; }
}
/* ───────── Camera frame (top-right) ─────────
Decorative only — drop your camera OBS source ON TOP of this frame
(positioned to match --cam-w / --cam-h). The frame has its own dark
backdrop + a centered placeholder so you can SEE where to put the
camera; once the camera source covers it, the placeholder disappears. */
/* ───────── Camera frame ─────────
Decorative chrome drawn UNDER the OBS Camera scene item. The four
--cam-* CSS vars are auto-synced from the Camera item's transform via
OBS WebSocket (see syncCamera below), so wherever you drag/resize the
camera in OBS, this frame snaps to match. The dark backdrop + centered
placeholder show through only until the WS connects or while the camera
is hidden. Requires the HUD browser source to render at canvas
resolution (2560×1336) so 1 CSS px == 1 canvas px. */
.camera-frame {
position: absolute;
top: calc(var(--pad-y) + 60px); /* clear of the HUD */
right: var(--pad-x);
width: var(--cam-w);
height: var(--cam-h);
top: calc(var(--cam-y) - var(--cam-pad));
left: calc(var(--cam-x) - var(--cam-pad));
width: calc(var(--cam-w) + var(--cam-pad) * 2);
height: calc(var(--cam-h) + var(--cam-pad) * 2);
pointer-events: none;
background: rgba(6, 10, 18, 0.85);
border: 1.5px solid rgba(79, 210, 255, 0.55);
@@ -200,28 +221,6 @@
100% { opacity: 1.00; }
}
/* ───────── Subtle CRT overlays (lighter than Landing — game must stay readable) ───────── */
#static {
position: absolute; inset: 0;
width: 100%; height: 100%;
pointer-events: none;
opacity: 0.022;
mix-blend-mode: screen;
image-rendering: pixelated;
}
.scanlines {
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.13) 3px,
rgba(0,0,0,0.13) 4px
);
opacity: 0.40;
mix-blend-mode: multiply;
}
</style>
</head>
<body>
@@ -253,8 +252,8 @@
<path d="M46 18 L60 10 L60 38 L46 30 Z" stroke="#4fd2ff" stroke-width="2" stroke-linejoin="round"/>
<circle cx="14" cy="14" r="2" fill="#e63a2e"/>
</svg>
<span class="ph-text">PLACE CAMERA HERE </span>
<span class="ph-coords">2008, 100 · 480 × 270</span>
<span class="ph-text">WAITING FOR CAMERA —</span>
<span class="ph-coords">auto-syncs from OBS scene transform</span>
</div>
</div>
@@ -284,9 +283,6 @@
</div>
</footer>
<canvas id="static"></canvas>
<div class="scanlines"></div>
<!-- Manifest from Project Loading. Same loading.js the Project Loading scene
uses, so the Game scene picks up the game / subtitle / camera / mic the
streamer already entered. -->
@@ -344,23 +340,6 @@
}
tickElapsed(); setInterval(tickElapsed, 1000);
// ── Static noise (very subtle; gameplay must remain readable) ──
const cv = document.getElementById('static'), ctx = cv.getContext('2d');
const W = 320, H = 180;
cv.width = W; cv.height = H;
const img = ctx.createImageData(W, H);
function drawNoise() {
const d = img.data;
for (let i = 0; i < d.length; i += 4) {
const v = (Math.random() * 255) | 0;
d[i] = d[i+1] = d[i+2] = v;
d[i+3] = 255;
}
ctx.putImageData(img, 0, 0);
}
drawNoise();
setInterval(drawNoise, 1000 / 8);
// ── OBS WS: music sync + live camera visibility ──
// URL params let you rename the camera source / scene without editing JS:
// ?camera=Webcam&scene=Game
@@ -376,7 +355,21 @@
const npTitle = document.getElementById('np-title');
const npTime = document.getElementById('np-time');
const camEl = document.getElementById('cam');
const rootStyle = document.documentElement.style;
function setCameraVisible(visible) { camEl.classList.toggle('off', !visible); }
// Map an OBS sceneItemTransform into the four CSS vars driving the frame.
// Assumes alignment 5 (top-left) so positionX/Y is the top-left corner —
// OBS's default for newly added items, and what the Camera source uses.
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;
@@ -387,6 +380,9 @@
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) {
// Source not found — keep manifest value, don't block other features.
console.warn(`[game] camera sync init failed (${CAM_SCENE}/${CAM_SOURCE}):`, err.message);
@@ -394,10 +390,12 @@
}
obs.addEventListener('event', (ev) => {
const d = ev.detail || {};
if (d.eventType === 'SceneItemEnableStateChanged'
&& d.eventData?.sceneName === CAM_SCENE
&& d.eventData?.sceneItemId === camItemId) {
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);
}
});
}