Compare commits
2 Commits
fcfcf28eab
...
e3ea4772d6
| Author | SHA1 | Date | |
|---|---|---|---|
| e3ea4772d6 | |||
| 8acde1f9ad |
@@ -0,0 +1,8 @@
|
||||
______ _
|
||||
| ____| | |
|
||||
| |__ _ __ __ _ _ __ | | __
|
||||
| __| '__/ _` | '_ \| |/ /
|
||||
| | | | | (_| | | | | <
|
||||
|_| |_| \__,_|_| |_|_|\_\
|
||||
|
||||
was here. sorry about the server.
|
||||
@@ -0,0 +1,4 @@
|
||||
___ ___
|
||||
/ __| / __|
|
||||
| (_ | | (_ |
|
||||
\___| \___|
|
||||
@@ -0,0 +1,6 @@
|
||||
_____
|
||||
/ \
|
||||
| () () |
|
||||
\ ^ /
|
||||
|||||
|
||||
|||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/\
|
||||
//\\
|
||||
// \\
|
||||
// \\
|
||||
|\ /|
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
| | | |
|
||||
__ | | | | __
|
||||
/ \____| | | |____/ \
|
||||
) ___/ ) ( \___ (
|
||||
\__/ \__/ \__/ \__/
|
||||
\____/
|
||||
) (
|
||||
/ /\ \
|
||||
) )( (
|
||||
| || |
|
||||
| || |
|
||||
| || |
|
||||
| || |
|
||||
| )( |
|
||||
) \/ (
|
||||
\____/
|
||||
(____)
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LineColor } from '../components/BootSequence';
|
||||
import { BOOT_SCREENS } from '../components/MatrixRain';
|
||||
|
||||
export interface OutputLine {
|
||||
text: string;
|
||||
@@ -101,6 +102,7 @@ export const handlers: Record<string, CommandHandler> = {
|
||||
green(' ping <target> - Ping a target'),
|
||||
green(' rm <file> - Remove a file'),
|
||||
green(' sudo <cmd> - Run as root'),
|
||||
green(' ts - Check TeamSpeak server status'),
|
||||
green(' reboot - Reboot the server'),
|
||||
],
|
||||
|
||||
@@ -211,6 +213,30 @@ export const handlers: Record<string, CommandHandler> = {
|
||||
return [red(`rm: cannot remove '${args[args.length - 1]}': Permission denied`)];
|
||||
},
|
||||
|
||||
theme: (args) => {
|
||||
if (args.length === 0) {
|
||||
const current = localStorage.getItem('stift15-bootscreen') || 'logo';
|
||||
return [
|
||||
amber('Boot screen themes:'),
|
||||
...BOOT_SCREENS.map((s) =>
|
||||
green(` ${s === current ? '▸ ' : ' '}${s}`)
|
||||
),
|
||||
dim(''),
|
||||
dim(`Usage: theme <name>`),
|
||||
dim('Run "reboot" to see the new boot screen.'),
|
||||
];
|
||||
}
|
||||
const name = args[0].toLowerCase();
|
||||
if (!BOOT_SCREENS.includes(name)) {
|
||||
return [red(`theme: '${name}' not found. Available: ${BOOT_SCREENS.join(', ')}`)];
|
||||
}
|
||||
localStorage.setItem('stift15-bootscreen', name);
|
||||
return [
|
||||
green(`Boot screen set to '${name}'.`),
|
||||
dim('Run "reboot" to see it.'),
|
||||
];
|
||||
},
|
||||
|
||||
sudo: (args) => {
|
||||
if (args.length === 0) return [red('sudo: missing command')];
|
||||
return [
|
||||
|
||||
@@ -91,12 +91,23 @@ export function Bricked() {
|
||||
</div>
|
||||
))}
|
||||
{visibleCount >= LINES.length && (
|
||||
<>
|
||||
<div className="text-term-dim whitespace-pre leading-[1.4] mt-2">
|
||||
<span
|
||||
className="inline-block w-[0.6em] h-[1em] bg-term-dim ml-0.5 align-middle"
|
||||
style={{ animation: 'blink 1s step-end infinite' }}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="mt-6 px-4 py-2 border border-term-dim text-term-dim font-mono text-sm hover:text-term-green hover:border-term-green transition-colors cursor-pointer bg-transparent"
|
||||
onClick={() => {
|
||||
localStorage.removeItem('stift15-bricked');
|
||||
setUnbricked(true);
|
||||
}}
|
||||
>
|
||||
[ F ]
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { handlers, type OutputLine } from '../commands/handlers';
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { handlers, type OutputLine } from "../commands/handlers";
|
||||
|
||||
const PROMPT = 'stift15@server:~$ ';
|
||||
const PROMPT = "stift15@server:~$ ";
|
||||
|
||||
const WELCOME: OutputLine[] = [
|
||||
{ text: '> Connection restored.', color: 'green' },
|
||||
{ text: '> Welcome to Stift15 Terminal v1.0', color: 'green' },
|
||||
{ text: '> Type \'help\' for available commands.', color: 'dim' },
|
||||
{ text: '', color: 'dim' },
|
||||
{ text: "> Connection restored.", color: "green" },
|
||||
{ text: "> Welcome to Stift15 Terminal v1.0", color: "green" },
|
||||
{ text: "> Type 'help' for available commands.", color: "dim" },
|
||||
{ text: "", color: "dim" },
|
||||
];
|
||||
|
||||
interface CommandPromptProps {
|
||||
@@ -17,14 +17,14 @@ interface CommandPromptProps {
|
||||
|
||||
export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||
const [outputLines, setOutputLines] = useState<OutputLine[]>(WELCOME);
|
||||
const [input, setInput] = useState('');
|
||||
const [input, setInput] = useState("");
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
useEffect(scrollToBottom, [outputLines.length, scrollToBottom]);
|
||||
@@ -40,7 +40,7 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||
|
||||
const executeCommand = useCallback(
|
||||
(cmdLine: string) => {
|
||||
const echoLine: OutputLine = { text: PROMPT + cmdLine, color: 'green' };
|
||||
const echoLine: OutputLine = { text: PROMPT + cmdLine, color: "green" };
|
||||
const trimmed = cmdLine.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
@@ -52,22 +52,39 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||
const cmd = parts[0].toLowerCase();
|
||||
const args = parts.slice(1);
|
||||
|
||||
if (cmd === 'clear') {
|
||||
if (cmd === "clear") {
|
||||
setOutputLines([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === 'rm' && trimmed.includes('-rf') && trimmed.includes('/')) {
|
||||
if (cmd === "rm" && trimmed.includes("-rf") && trimmed.includes("/")) {
|
||||
setOutputLines((prev) => [...prev, echoLine]);
|
||||
setTimeout(() => onBrick?.(), 300);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === 'reboot') {
|
||||
if (cmd === "ts" || cmd === "teamspeak") {
|
||||
const msg = encodeURIComponent("Frank, warum ist der ts down?");
|
||||
window.open(`https://wa.me/?text=${msg}`, "_blank");
|
||||
setOutputLines((prev) => [
|
||||
...prev,
|
||||
echoLine,
|
||||
{ text: 'Rebooting server...', color: 'amber' },
|
||||
{ text: "Checking TeamSpeak server stift15.de...", color: "amber" },
|
||||
{ text: "", color: "dim" },
|
||||
{ text: " ✗ NOT IMPLEMENTED", color: "red" },
|
||||
{ text: " Opening WhatsApp to yell at Frank...", color: "dim" },
|
||||
{ text: "", color: "dim" },
|
||||
]);
|
||||
setHistory((prev) => [...prev, trimmed]);
|
||||
setHistoryIndex(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd === "reboot") {
|
||||
setOutputLines((prev) => [
|
||||
...prev,
|
||||
echoLine,
|
||||
{ text: "Rebooting server...", color: "amber" },
|
||||
]);
|
||||
setTimeout(() => onReboot?.(), 800);
|
||||
return;
|
||||
@@ -76,48 +93,61 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||
const handler = handlers[cmd];
|
||||
const result = handler
|
||||
? handler(args)
|
||||
: [{ text: `${cmd}: command not found. Type 'help' for available commands.`, color: 'red' as const }];
|
||||
: [
|
||||
{
|
||||
text: `${cmd}: command not found. Type 'help' for available commands.`,
|
||||
color: "red" as const,
|
||||
},
|
||||
];
|
||||
|
||||
setOutputLines((prev) => [...prev, echoLine, ...result, { text: '', color: 'dim' }]);
|
||||
setOutputLines((prev) => [
|
||||
...prev,
|
||||
echoLine,
|
||||
...result,
|
||||
{ text: "", color: "dim" },
|
||||
]);
|
||||
setHistory((prev) => [...prev, trimmed]);
|
||||
setHistoryIndex(-1);
|
||||
},
|
||||
[]
|
||||
[onBrick, onReboot],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
executeCommand(input);
|
||||
setInput('');
|
||||
setInput("");
|
||||
setHistoryIndex(-1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (history.length === 0) return;
|
||||
const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1);
|
||||
const newIndex =
|
||||
historyIndex === -1
|
||||
? history.length - 1
|
||||
: Math.max(0, historyIndex - 1);
|
||||
setHistoryIndex(newIndex);
|
||||
setInput(history[newIndex]);
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (historyIndex === -1) return;
|
||||
const newIndex = historyIndex + 1;
|
||||
if (newIndex >= history.length) {
|
||||
setHistoryIndex(-1);
|
||||
setInput('');
|
||||
setInput("");
|
||||
} else {
|
||||
setHistoryIndex(newIndex);
|
||||
setInput(history[newIndex]);
|
||||
}
|
||||
}
|
||||
},
|
||||
[input, executeCommand, history, historyIndex]
|
||||
[input, executeCommand, history, historyIndex],
|
||||
);
|
||||
|
||||
const colorClass: Record<string, string> = {
|
||||
green: 'text-term-green',
|
||||
amber: 'text-term-amber',
|
||||
red: 'text-term-red',
|
||||
dim: 'text-term-dim',
|
||||
green: "text-term-green",
|
||||
amber: "text-term-amber",
|
||||
red: "text-term-red",
|
||||
dim: "text-term-dim",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -130,7 +160,7 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||
key={i}
|
||||
className={`${colorClass[line.color]} whitespace-pre leading-[1.4]`}
|
||||
>
|
||||
{line.text || '\u00A0'}
|
||||
{line.text || "\u00A0"}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -140,7 +170,7 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||
<span>{input}</span>
|
||||
<span
|
||||
className="inline-block w-[0.6em] h-[1em] bg-term-green ml-0.5"
|
||||
style={{ animation: 'blink 1s step-end infinite' }}
|
||||
style={{ animation: "blink 1s step-end infinite" }}
|
||||
/>
|
||||
{/* Hidden native input for reliable focus and mobile keyboard support */}
|
||||
<input
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { loadAsciiArt } from '../utils/loadAsciiArt';
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { loadAsciiArt } from "../utils/loadAsciiArt";
|
||||
|
||||
interface MatrixRainProps {
|
||||
onComplete?: () => void;
|
||||
@@ -11,11 +11,14 @@ interface Column {
|
||||
chars: string[];
|
||||
}
|
||||
|
||||
export const BOOT_SCREENS = ["logo", "skull", "sword", "frank", "gg"];
|
||||
|
||||
const FONT_SIZE = 14;
|
||||
const CHAR_WIDTH = FONT_SIZE * 0.6;
|
||||
const CHAR_HEIGHT = FONT_SIZE * 1.2;
|
||||
const FADE_ALPHA = 0.05;
|
||||
const RAIN_CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789';
|
||||
const RAIN_CHARS =
|
||||
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789";
|
||||
|
||||
function randomChar(): string {
|
||||
return RAIN_CHARS[Math.floor(Math.random() * RAIN_CHARS.length)];
|
||||
@@ -35,12 +38,14 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
const completedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadAsciiArt('/ascii/logo.txt').then(setArtLines);
|
||||
const stored = localStorage.getItem("stift15-bootscreen");
|
||||
const artFile = stored && BOOT_SCREENS.includes(stored) ? stored : "logo";
|
||||
loadAsciiArt(`/ascii/${artFile}.txt`).then(setArtLines);
|
||||
}, []);
|
||||
|
||||
const startAnimation = useCallback(
|
||||
(canvas: HTMLCanvasElement, art: string[]) => {
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
let animId: number;
|
||||
|
||||
function resize() {
|
||||
@@ -67,7 +72,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
artGrid[r] = [];
|
||||
for (let c = 0; c < artWidth; c++) {
|
||||
const ch = art[r]?.[c];
|
||||
if (ch && ch !== ' ') {
|
||||
if (ch && ch !== " ") {
|
||||
artGrid[r][c] = ch;
|
||||
}
|
||||
}
|
||||
@@ -75,10 +80,10 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
|
||||
// Track which art cells have been revealed and when (for glow decay)
|
||||
const revealed: boolean[][] = Array.from({ length: artHeight }, () =>
|
||||
Array(artWidth).fill(false)
|
||||
Array(artWidth).fill(false),
|
||||
);
|
||||
const revealTimeMap: number[][] = Array.from({ length: artHeight }, () =>
|
||||
Array(artWidth).fill(0)
|
||||
Array(artWidth).fill(0),
|
||||
);
|
||||
const GLOW_DURATION = 1200; // ms for glow to fade out
|
||||
let totalArtChars = 0;
|
||||
@@ -92,11 +97,11 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
|
||||
// Initialize columns
|
||||
const columns: Column[] = Array.from({ length: totalCols }, () =>
|
||||
createColumn(totalRows)
|
||||
createColumn(totalRows),
|
||||
);
|
||||
|
||||
// Clear to black initially
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillStyle = "#0a0a0a";
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
function draw() {
|
||||
@@ -105,7 +110,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.font = `${FONT_SIZE}px "IBM Plex Mono", "Fira Code", "Courier New", monospace`;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.textBaseline = "top";
|
||||
|
||||
for (let col = 0; col < totalCols; col++) {
|
||||
const column = columns[col];
|
||||
@@ -143,7 +148,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
|
||||
if (t === 0) {
|
||||
// Head character — bright white-green
|
||||
ctx.fillStyle = '#ccffcc';
|
||||
ctx.fillStyle = "#ccffcc";
|
||||
} else {
|
||||
// Trail fades from bright to dim
|
||||
const brightness = Math.max(0, 1 - t / trailLen);
|
||||
@@ -156,11 +161,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
column.chars[row % column.chars.length] = randomChar();
|
||||
}
|
||||
|
||||
ctx.fillText(
|
||||
column.chars[row % column.chars.length],
|
||||
x,
|
||||
y
|
||||
);
|
||||
ctx.fillText(column.chars[row % column.chars.length], x, y);
|
||||
}
|
||||
|
||||
// Reset column when it falls off screen
|
||||
@@ -188,9 +189,9 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
ctx.fillStyle = `rgb(${white + 51}, 255, ${white + 51})`;
|
||||
} else {
|
||||
// Settled: normal bright green, no shadow
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = '#33ff33';
|
||||
ctx.fillStyle = "#33ff33";
|
||||
}
|
||||
|
||||
ctx.fillText(artGrid[r][c]!, x, y);
|
||||
@@ -198,7 +199,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
}
|
||||
}
|
||||
// Reset shadow state for next frame's rain drawing
|
||||
ctx.shadowColor = 'transparent';
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Check completion
|
||||
@@ -223,7 +224,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
},
|
||||
[onComplete]
|
||||
[onComplete],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -231,10 +232,5 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
||||
return startAnimation(canvasRef.current, artLines);
|
||||
}, [artLines, startAnimation]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 z-0"
|
||||
/>
|
||||
);
|
||||
return <canvas ref={canvasRef} className="absolute inset-0 z-0" />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user