Terminal-Design für U19 Spielstand-Version

This commit is contained in:
DylanBarnes 2026-07-15 15:49:18 +00:00
parent 2fc4bf3889
commit 92c485d86f

View file

@ -14,11 +14,234 @@ def get_db():
return db return db
BASE_CSS = """... (unverändert, deine bestehende CSS bleibt) ...""" BASE_CSS = """
BASE_JS = """... (unverändert) ...""" @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500&display=swap');
:root {
--void: #030507;
--panel: #0a1512;
--panel-2: #0d1a16;
--line: rgba(127, 219, 182, 0.22);
--text: #d7fbe8;
--text-dim: #77988b;
--amber: #ffb454;
--amber-dim: #8a5e2a;
--danger: #ff6b5e;
--success: #4ade9a;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(ellipse at 50% -10%, rgba(74, 222, 154, 0.06), transparent 60%),
var(--void);
color: var(--text);
font-family: 'IBM Plex Sans', sans-serif;
position: relative;
overflow-x: hidden;
}
/* feine Scanlines über der gesamten Seite */
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
rgba(255,255,255,0.015) 0px,
rgba(255,255,255,0.015) 1px,
transparent 2px,
transparent 3px
);
z-index: 2;
mix-blend-mode: overlay;
}
/* Vignette */
body::after {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.55) 100%);
z-index: 1;
}
.shell {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 2.5rem 1rem;
position: relative;
z-index: 3;
}
.console-strip {
width: min(760px, 100%);
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-dim);
border: 1px solid var(--line);
border-bottom: none;
border-radius: 10px 10px 0 0;
padding: 0.55rem 1rem;
background: var(--panel-2);
}
.console-strip .dot {
display: inline-block;
width: 6px; height: 6px;
border-radius: 50%;
background: var(--success);
margin-right: 0.4em;
box-shadow: 0 0 6px var(--success);
}
.card {
width: min(760px, 100%);
background: var(--panel);
border: 1px solid var(--line);
border-radius: 0 0 10px 10px;
padding: 2.2rem 2.2rem 2.4rem;
box-shadow: 0 30px 80px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.03);
}
.eyebrow {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--amber);
margin: 0 0 0.6rem;
}
.card h1 {
font-family: 'IBM Plex Mono', monospace;
margin: 0 0 1rem;
font-size: 1.7rem;
font-weight: 600;
color: var(--text);
letter-spacing: 0.01em;
}
.card h1::after {
content: "_";
color: var(--amber);
animation: blink 1.1s steps(1) infinite;
}
@keyframes blink { 50% { opacity: 0; } }
.card p {
line-height: 1.75;
color: #b9d6c9;
font-size: 1rem;
}
.actions, .links {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-top: 1.5rem;
}
.btn, .card a, .card button {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
letter-spacing: 0.04em;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 0.4em;
background: transparent;
color: var(--amber);
border: 1px solid var(--amber-dim);
border-radius: 6px;
padding: 0.65rem 1rem;
text-decoration: none;
cursor: pointer;
transition: all .18s ease;
}
.btn::before, .card a::before, .card button::before { content: ""; opacity: .6; }
.btn:hover, .card a:hover, .card button:hover {
background: rgba(255,180,84,0.08);
border-color: var(--amber);
color: #ffd08a;
}
.btn.secondary, .card a.secondary {
color: var(--text-dim);
border-color: var(--line);
}
.btn.secondary:hover, .card a.secondary:hover {
color: var(--text);
border-color: rgba(127,219,182,0.5);
background: rgba(127,219,182,0.06);
}
form {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: center;
margin-top: 1.2rem;
}
form::before {
content: ">";
font-family: 'IBM Plex Mono', monospace;
color: var(--success);
font-weight: 700;
}
input {
flex: 1 1 220px;
font-family: 'IBM Plex Mono', monospace;
border: 1px solid var(--line);
border-radius: 6px;
padding: 0.7rem 0.9rem;
background: #050b09;
color: var(--text);
}
input::placeholder { color: #4d6a5f; }
input:focus {
outline: none;
border-color: var(--success);
box-shadow: 0 0 0 3px rgba(74,222,154,0.15);
}
.status-line {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-dim);
margin-top: 1.4rem;
padding-top: 1rem;
border-top: 1px dashed var(--line);
}
@media (prefers-reduced-motion: reduce) {
.card h1::after { animation: none; opacity: 1; }
}
"""
BASE_JS = """
window.addEventListener('DOMContentLoaded', () => {
const firstInput = document.querySelector('input');
if (firstInput) firstInput.focus();
});
"""
def render_page(title, body_html): def render_page(title, body_html, eyebrow=None):
spieler = session.get("spieler")
status = f'<span><span class="dot"></span>{html.escape(spieler)}</span>' if spieler else '<span><span class="dot"></span>NOTSTROM AKTIV</span>'
eyebrow_html = f'<p class="eyebrow">{html.escape(eyebrow)}</p>' if eyebrow else ""
return f"""<!doctype html> return f"""<!doctype html>
<html lang=\"de\"> <html lang=\"de\">
<head> <head>
@ -29,7 +252,12 @@ def render_page(title, body_html):
</head> </head>
<body> <body>
<main class=\"shell\"> <main class=\"shell\">
<div class=\"console-strip\">
{status}
<span>SCHIFF-OS // NOTBETRIEB</span>
</div>
<section class=\"card\"> <section class=\"card\">
{eyebrow_html}
{body_html} {body_html}
</section> </section>
</main> </main>
@ -66,12 +294,10 @@ def start():
).fetchone() ).fetchone()
if s: if s:
weiterspielen_html = f""" weiterspielen_html = f"""
<p>Willkommen zurück, {html.escape(session['spieler'])}!</p>
<div class="actions"> <div class="actions">
<a class="btn" href="/app/raum/{s['raum_id']}">Weiterspielen</a> <a href="/app/raum/{s['raum_id']}">Weiterspielen</a>
<a class="btn secondary" href="/app/reset">Neues Abenteuer</a> <a class="secondary" href="/app/reset">Neues Abenteuer</a>
</div> </div>
<hr>
""" """
db.close() db.close()
@ -80,12 +306,12 @@ def start():
<p>Du bist der einzige Überlebende an Bord. Erkunde das Schiff und aktiviere das Notsignal.</p> <p>Du bist der einzige Überlebende an Bord. Erkunde das Schiff und aktiviere das Notsignal.</p>
{weiterspielen_html} {weiterspielen_html}
<form method="post"> <form method="post">
<input name="name" placeholder="Dein Name" autocomplete="off"> <input name="name" placeholder="Spieler-ID eingeben" autocomplete="off">
<button type="submit">Abenteuer starten</button> <button type="submit">Anmelden</button>
</form> </form>
<p><small>{anzahl['n']} Abenteurer waren schon hier.</small></p> <p class="status-line">{anzahl['n']} Abenteurer waren schon an Bord.</p>
""" """
return render_page("Verlassenes Raumschiff", body) return render_page("Verlassenes Raumschiff", body, eyebrow="SCHIFF-OS v2.4 // BOOT")
@app.route("/raum/<int:id>", methods=["GET", "POST"]) @app.route("/raum/<int:id>", methods=["GET", "POST"])
@ -93,7 +319,6 @@ def start():
def raum(id): def raum(id):
db = get_db() db = get_db()
# Spielstand fortschreiben, sobald jemand angemeldet ist
if "spieler" in session: if "spieler" in session:
db.execute( db.execute(
"UPDATE spielstand SET raum_id=? WHERE spieler=?", "UPDATE spielstand SET raum_id=? WHERE spieler=?",
@ -107,12 +332,11 @@ def raum(id):
return render_page( return render_page(
"Raum nicht gefunden", "Raum nicht gefunden",
'<h1>Raum nicht gefunden</h1><p>Der angeforderte Raum existiert nicht.</p>' '<h1>Raum nicht gefunden</h1><p>Der angeforderte Raum existiert nicht.</p>'
'<div class="actions"><a class="btn secondary" href="/app/">Zurück zum Start</a></div>' '<div class="actions"><a class="secondary" href="/app/">Zurück zum Start</a></div>',
eyebrow="FEHLER"
) )
unterwegs_html = "" eyebrow = f"SEKTOR {id:02d}"
if "spieler" in session:
unterwegs_html = f'<p><small>Unterwegs als: {html.escape(session["spieler"])}</small></p>'
if r["raetsel_frage"]: if r["raetsel_frage"]:
if request.method == "POST": if request.method == "POST":
@ -121,26 +345,27 @@ def raum(id):
return render_page( return render_page(
"Notsignal aktiviert", "Notsignal aktiviert",
'<h1>Notsignal erfolgreich aktiviert</h1><p>Rettung ist unterwegs.</p>' '<h1>Notsignal erfolgreich aktiviert</h1><p>Rettung ist unterwegs.</p>'
'<div class="actions"><a class="btn" href="/app/">Zurück zum Start</a></div>' '<div class="actions"><a href="/app/">Zurück zum Start</a></div>',
eyebrow="MISSION ERFÜLLT"
) )
db.close() db.close()
return render_page( return render_page(
"Falscher Code", "Falscher Code",
'<h1>Falscher Code</h1><p>Der KI-Kern reagiert nicht.</p>' '<h1>Falscher Code</h1><p>Der KI-Kern reagiert nicht.</p>'
'<div class="actions"><a class="btn secondary" href="/app/raum/' + str(id) + '">nochmal versuchen</a></div>' '<div class="actions"><a class="secondary" href="/app/raum/' + str(id) + '">nochmal versuchen</a></div>',
eyebrow=eyebrow
) )
db.close() db.close()
body = f""" body = f"""
<h1>{html.escape(r['name'])}</h1> <h1>{html.escape(r['name'])}</h1>
<p>{html.escape(r['beschreibung'])}</p> <p>{html.escape(r['beschreibung'])}</p>
{unterwegs_html}
<form method="post"> <form method="post">
<input id="antwort" name="antwort" placeholder="Antwort eingeben" autocomplete="off"> <input id="antwort" name="antwort" placeholder="Antwort eingeben" autocomplete="off">
<button type="submit">OK</button> <button type="submit">Senden</button>
</form> </form>
""" """
return render_page(html.escape(r["name"]), body) return render_page(html.escape(r["name"]), body, eyebrow=eyebrow)
ausgaenge = db.execute( ausgaenge = db.execute(
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,) "SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,)
@ -150,19 +375,18 @@ def raum(id):
body = f""" body = f"""
<h1>{html.escape(r['name'])}</h1> <h1>{html.escape(r['name'])}</h1>
<p>{html.escape(r['beschreibung'])}</p> <p>{html.escape(r['beschreibung'])}</p>
{unterwegs_html}
<div class="links"> <div class="links">
""" """
for a in ausgaenge: for a in ausgaenge:
body += f'<a class="btn secondary" href="/app/raum/{a["nach_raum"]}">{html.escape(a["richtung"])}</a>' body += f'<a class="secondary" href="/app/raum/{a["nach_raum"]}">{html.escape(a["richtung"])}</a>'
body += "</div>" body += "</div>"
return render_page(html.escape(r["name"]), body) return render_page(html.escape(r["name"]), body, eyebrow=eyebrow)
@app.route("/reset") @app.route("/reset")
@app.route("/app/reset") @app.route("/app/reset")
def reset(): def reset():
session.pop("spieler", None) # Session vergessen (DB bleibt!) session.pop("spieler", None)
return redirect("/app/") return redirect("/app/")