Add fleet groups, agent screenshots, deploy guards, and Crucible polish.

This commit is contained in:
AetherForge
2026-06-02 20:51:52 -07:00
parent 01d76b3730
commit 41b5ec7a88
66 changed files with 1773 additions and 388 deletions

View File

@@ -180,6 +180,32 @@
min-height: 160px;
max-height: 320px;
background: #000;
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
/* Forge / Crucible target lock — gold heavy rain (matches forge progress vibe) */
.matrix-rain-wrap--intense {
border-top-color: rgba(255, 180, 40, 0.45);
border-bottom-color: rgba(255, 120, 0, 0.35);
box-shadow:
inset 0 0 28px rgba(255, 140, 0, 0.12),
0 0 18px rgba(255, 160, 0, 0.15);
}
.matrix-rain-wrap--intense .matrix-rain-scanlines {
background: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(40, 20, 0, 0.22) 1px,
rgba(40, 20, 0, 0.22) 2px
);
}
.matrix-rain-wrap--crucible.matrix-rain-wrap--intense {
box-shadow:
inset 0 0 32px rgba(255, 200, 80, 0.14),
0 0 22px rgba(255, 180, 50, 0.2);
}
.matrix-rain-canvas {

View File

@@ -1,8 +1,13 @@
import { useEffect, useRef } from 'react';
import { useWebSocket } from '../../hooks/useWebSocket';
import { useForge } from '../../context/ForgeContext';
import { useMatrixRain } from '../../context/MatrixRainContext';
import {
FORGE_RAIN_STRINGS,
pickMysticWord,
wordColumnSpan,
} from '../../help/matrixRainEffects';
// Full matrix alphabet: katakana + hex + braille dots for visual density
const KATAKANA =
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン';
const HEX = '0123456789ABCDEFabcdef';
@@ -14,36 +19,36 @@ const FONT_SIZE = 10;
interface Column {
y: number;
speed: number;
// occasionally carry a char from live data
liveSrc: string;
livePos: number;
}
const FORGE_STRINGS = [
'COMPILING', 'LINKING', 'GARBLE', 'GO BUILD', 'INJECT',
'STEALTH', 'PERSIST', 'ENCRYPT', 'OBFUSC', 'PACKAGE',
'WORKER', 'FORGE', 'SIGN', 'BUNDLE', 'AGENT',
'RANDOMX', 'STRATUM', 'C2CONN', 'DEPLOY',
];
interface WordDrop {
text: string;
colStart: number;
y: number;
speed: number;
}
export default function MatrixRain() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const wrapRef = useRef<HTMLDivElement>(null);
const { agents, recentShares, commandResults } = useWebSocket();
const { forging, stage } = useForge();
const { crucibleFocus } = useMatrixRain();
const forgingRef = useRef(false);
const crucibleRef = useRef(false);
const stageRef = useRef('');
forgingRef.current = forging;
crucibleRef.current = crucibleFocus;
stageRef.current = stage;
// ── Live data pool ──────────────────────────────────────────────────────────
// Collect strings from the fleet that will be injected character-by-character
// into the rain columns so real data scrolls through the matrix.
const livePoolRef = useRef<string[]>([]);
useEffect(() => {
const pool: string[] = [];
for (const a of agents) {
pool.push(a.id.replace(/-/g, '')); // stripped UUID
pool.push(a.id.replace(/-/g, ''));
if (a.hashrate_15s > 0) pool.push(`${a.hashrate_15s.toFixed(0)}H`);
if (a.ip) pool.push(a.ip.replace(/\./g, ''));
}
@@ -53,22 +58,24 @@ export default function MatrixRain() {
for (const r of (commandResults ?? []).slice(-5)) {
if (r.action) pool.push(r.action.toUpperCase().padEnd(8, '_'));
}
// When forging, flood the pool with build strings so they dominate the rain
if (forgingRef.current) {
pool.push(...FORGE_STRINGS);
if (stageRef.current) {
const intense = forgingRef.current || crucibleRef.current;
if (intense) {
pool.push(...FORGE_RAIN_STRINGS);
if (forgingRef.current && stageRef.current) {
pool.push(stageRef.current.replace(/[^A-Z0-9]/gi, '').toUpperCase().slice(0, 16));
}
if (crucibleRef.current) {
pool.push('CRUCIBLE', 'TARGET', 'EXECUTE', 'REMOTE');
}
}
livePoolRef.current = pool.length > 0 ? pool : ['AETHERFORGE', 'MINING', '00E5FF'];
}, [agents, recentShares, commandResults]);
}, [agents, recentShares, commandResults, forging, crucibleFocus]);
// ── Event log feed ──────────────────────────────────────────────────────────
// We inject one short log line per meaningful event, shown as a dim overlay
// row scrolling through the canvas.
const eventLogRef = useRef<{ text: string; alpha: number }[]>([]);
const prevShareLen = useRef(0);
const prevAgentLen = useRef(0);
const prevCmdSeq = useRef(0);
useEffect(() => {
const newEvents: string[] = [];
if (recentShares.length > prevShareLen.current) {
@@ -81,13 +88,28 @@ export default function MatrixRain() {
newEvents.push(`AGENT ONLINE ${a.name?.slice(0, 8) ?? '??'}`);
}
prevAgentLen.current = agents.length;
const results = commandResults ?? [];
if (results.length > 0) {
const latest = results[results.length - 1];
if (latest._seq > prevCmdSeq.current) {
prevCmdSeq.current = latest._seq;
const tag = latest.success ? 'CMD OK' : 'CMD FAIL';
newEvents.push(`${tag} ${latest.action?.toUpperCase() ?? '?'}`);
}
}
for (const ev of newEvents) {
eventLogRef.current.push({ text: ev, alpha: 1 });
if (eventLogRef.current.length > 6) eventLogRef.current.shift();
}
}, [recentShares, agents]);
}, [recentShares, agents, commandResults]);
const intenseMode = forging || crucibleFocus;
const wrapClass = intenseMode
? `matrix-rain-wrap matrix-rain-wrap--intense${crucibleFocus && !forging ? ' matrix-rain-wrap--crucible' : ''}${forging ? ' matrix-rain-wrap--forge' : ''}`
: 'matrix-rain-wrap';
// ── Canvas renderer ─────────────────────────────────────────────────────────
useEffect(() => {
const canvas = canvasRef.current;
const wrap = wrapRef.current;
@@ -95,7 +117,6 @@ export default function MatrixRain() {
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Resize canvas to match wrapper
const resize = () => {
const r = wrap.getBoundingClientRect();
canvas.width = Math.floor(r.width);
@@ -106,9 +127,11 @@ export default function MatrixRain() {
ro.observe(wrap);
let cols: Column[] = [];
const wordDropsRef: WordDrop[] = [];
const resetCols = () => {
const numCols = Math.max(1, Math.floor(canvas.width / FONT_SIZE));
cols = Array.from({ length: numCols }, (_, i) => ({
cols = Array.from({ length: numCols }, () => ({
y: Math.random() * -(canvas.height * 2),
speed: 0.3 + Math.random() * 0.55,
liveSrc: '',
@@ -117,7 +140,19 @@ export default function MatrixRain() {
};
resetCols();
// Periodically inject live data strings into random columns
const spawnWordDrop = () => {
if (cols.length < 4) return;
const text = pickMysticWord();
const span = wordColumnSpan(text);
const colStart = Math.floor(Math.random() * Math.max(1, cols.length - span));
wordDropsRef.push({
text,
colStart,
y: -span - 2,
speed: 0.35 + Math.random() * 0.25,
});
};
const injectInterval = setInterval(() => {
const pool = livePoolRef.current;
if (pool.length === 0 || cols.length === 0) return;
@@ -127,13 +162,51 @@ export default function MatrixRain() {
cols[colIdx].livePos = 0;
}, 180);
const wordInterval = setInterval(() => {
if (!forgingRef.current) spawnWordDrop();
}, 7000 + Math.random() * 5000);
spawnWordDrop();
let raf: number;
let lastTime = 0;
const drawWordDrop = (wd: WordDrop, speedMult: number, intense: boolean) => {
const H = canvas.height;
let lastCharIdx = wd.text.length - 1;
while (lastCharIdx >= 0 && wd.text[lastCharIdx] === ' ') lastCharIdx--;
let colIdx = 0;
for (let i = 0; i < wd.text.length; i++) {
const ch = wd.text[i];
if (ch === ' ') {
colIdx++;
continue;
}
const row = wd.y - (wd.text.length - 1 - i);
const py = row * FONT_SIZE;
if (py < -FONT_SIZE || py > H + FONT_SIZE) {
colIdx++;
continue;
}
const x = (wd.colStart + colIdx) * FONT_SIZE;
const isHead = i === lastCharIdx;
if (intense) {
ctx.fillStyle = isHead ? 'rgba(255,235,120,1)' : 'rgba(255,140,0,0.85)';
} else {
ctx.fillStyle = isHead ? 'rgba(220,120,255,1)' : 'rgba(140,0,200,0.55)';
}
ctx.fillText(ch, x, py);
colIdx++;
}
wd.y += wd.speed * speedMult;
};
const draw = (ts: number) => {
raf = requestAnimationFrame(draw);
const isForging = forgingRef.current;
const targetFps = isForging ? 50 : 24;
const intense = forgingRef.current || crucibleRef.current;
const targetFps = intense ? 50 : 24;
const msPerFrame = 1000 / targetFps;
if (ts - lastTime < msPerFrame) return;
lastTime = ts;
@@ -141,21 +214,18 @@ export default function MatrixRain() {
const W = canvas.width;
const H = canvas.height;
// Forge mode: less fade = longer glowing trails; normal: quick fade
ctx.fillStyle = isForging ? 'rgba(0,0,0,0.10)' : 'rgba(0,0,0,0.18)';
ctx.fillStyle = intense ? 'rgba(0,0,0,0.10)' : 'rgba(0,0,0,0.18)';
ctx.fillRect(0, 0, W, H);
ctx.font = `${FONT_SIZE}px 'Courier New', monospace`;
// Speed multiplier: 3× faster while forging
const speedMult = isForging ? 3.2 : 1.0;
const speedMult = intense ? 3.2 : 1.0;
for (let i = 0; i < cols.length; i++) {
const col = cols[i];
const x = i * FONT_SIZE;
const y = col.y;
// Pick character: live data char or random alphabet
let ch: string;
if (col.liveSrc && col.livePos < col.liveSrc.length) {
ch = col.liveSrc[col.livePos];
@@ -164,8 +234,7 @@ export default function MatrixRain() {
ch = ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
}
if (isForging) {
// Forge palette: bright amber/orange head, orange body
if (intense) {
ctx.fillStyle = 'rgba(255,220,80,0.98)';
ctx.fillText(ch, x, y * FONT_SIZE);
if (y > 1) {
@@ -173,21 +242,19 @@ export default function MatrixRain() {
ctx.fillText(
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
x,
(y - 1) * FONT_SIZE,
(y - 1) * FONT_SIZE
);
}
// Extra mid-column glyph density during forge
if (Math.random() < 0.12) {
if (Math.random() < 0.14) {
const dimY = Math.floor(Math.random() * Math.max(1, y - 2));
ctx.fillStyle = 'rgba(255,120,0,0.35)';
ctx.fillStyle = 'rgba(255,120,0,0.4)';
ctx.fillText(
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
x,
dimY * FONT_SIZE,
dimY * FONT_SIZE
);
}
} else {
// Normal palette: white head, cyan-green body
ctx.fillStyle = 'rgba(255,255,255,0.95)';
ctx.fillText(ch, x, y * FONT_SIZE);
if (y > 1) {
@@ -195,7 +262,7 @@ export default function MatrixRain() {
ctx.fillText(
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
x,
(y - 1) * FONT_SIZE,
(y - 1) * FONT_SIZE
);
}
if (Math.random() < 0.04) {
@@ -204,7 +271,7 @@ export default function MatrixRain() {
ctx.fillText(
ALPHABET[Math.floor(Math.random() * ALPHABET.length)],
x,
dimY * FONT_SIZE,
dimY * FONT_SIZE
);
}
}
@@ -218,18 +285,29 @@ export default function MatrixRain() {
}
}
// ── Event log overlay — bottom of canvas ──────────────────────────
for (let w = wordDropsRef.length - 1; w >= 0; w--) {
drawWordDrop(wordDropsRef[w], speedMult, intense);
if (wordDropsRef[w].y * FONT_SIZE > H + 40) {
wordDropsRef.splice(w, 1);
}
}
if (wordDropsRef.length < 3 && Math.random() < 0.02) {
spawnWordDrop();
}
const logs = eventLogRef.current;
const lineH = FONT_SIZE + 2;
ctx.font = `${FONT_SIZE - 1}px 'Courier New', monospace`;
const isForging2 = forgingRef.current;
for (let j = 0; j < logs.length; j++) {
const entry = logs[logs.length - 1 - j];
const oy = H - 6 - j * lineH;
if (oy < 0) break;
const logColor = isForging2
const fail = entry.text.includes('FAIL') || entry.text.includes('REJECT');
const logColor = intense
? `rgba(255,160,0,${(entry.alpha * 0.75).toFixed(2)})`
: `rgba(0,255,65,${(entry.alpha * 0.55).toFixed(2)})`;
: fail
? `rgba(255,80,80,${(entry.alpha * 0.65).toFixed(2)})`
: `rgba(0,255,65,${(entry.alpha * 0.55).toFixed(2)})`;
ctx.fillStyle = logColor;
ctx.fillText(`> ${entry.text}`, 4, oy);
entry.alpha = Math.max(0, entry.alpha - 0.003);
@@ -241,16 +319,15 @@ export default function MatrixRain() {
return () => {
cancelAnimationFrame(raf);
clearInterval(injectInterval);
clearInterval(wordInterval);
ro.disconnect();
};
}, []);
return (
<div ref={wrapRef} className="matrix-rain-wrap" aria-hidden="true">
<div ref={wrapRef} className={wrapClass} aria-hidden="true">
<canvas ref={canvasRef} className="matrix-rain-canvas" />
{/* Scanline overlay for authentic CRT feel */}
<div className="matrix-rain-scanlines" />
{/* Top and bottom vignette fades */}
<div className="matrix-rain-vignette-top" />
<div className="matrix-rain-vignette-btm" />
</div>