Merz leck Eier: MTG troll site
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
.vercel
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
|
||||||
|
COPY --from=build /app/.next/standalone ./
|
||||||
|
COPY --from=build /app/.next/static ./.next/static
|
||||||
|
COPY --from=build /app/public ./public
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
9
README.md
Normal file
9
README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Merz leck Eier 🥚🥚
|
||||||
|
|
||||||
|
Satirical Magic: The Gathering troll site. Next.js 15, no extra deps.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy: the Dockerfile builds a standalone server on port 3000 (Dokploy: point it at this repo, build type Dockerfile, port 3000). Also works as-is on Vercel.
|
||||||
40
app/CardView.tsx
Normal file
40
app/CardView.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Card } from "./cards";
|
||||||
|
|
||||||
|
function Symbol({ s }: { s: string }) {
|
||||||
|
return <span className={`mana mana-${s === "€" ? "E" : s}`}>{s}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderText(line: string) {
|
||||||
|
return line.split(/(\{[^}]+\})/g).map((part, i) =>
|
||||||
|
part.startsWith("{") ? <Symbol key={i} s={part.slice(1, -1)} /> : part,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardView({ card, big = false }: { card: Card; big?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className={`card c-${card.color} ${big ? "big" : ""}`}>
|
||||||
|
<div className="card-inner">
|
||||||
|
<div className="bar title">
|
||||||
|
<span>{card.name}</span>
|
||||||
|
<span className="cost">
|
||||||
|
{card.cost.map((c, i) => (
|
||||||
|
<Symbol key={i} s={c} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="art">{card.art}</div>
|
||||||
|
<div className="bar type">
|
||||||
|
<span>{card.type}</span>
|
||||||
|
<span className={`rarity r-${card.rarity}`}>◆</span>
|
||||||
|
</div>
|
||||||
|
<div className="textbox">
|
||||||
|
{card.text.map((t, i) => (
|
||||||
|
<p key={i}>{renderText(t)}</p>
|
||||||
|
))}
|
||||||
|
{card.flavor && <p className="flavor">{card.flavor}</p>}
|
||||||
|
</div>
|
||||||
|
{card.pt && <div className="pt">{card.pt}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
app/Game.tsx
Normal file
125
app/Game.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { CardView } from "./CardView";
|
||||||
|
import { COMMANDER, DECK, type Card } from "./cards";
|
||||||
|
|
||||||
|
const shuffle = <T,>(a: T[]) => [...a].sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
|
const LOG_LINES = [
|
||||||
|
"Die Umfragewerte sinken. Merz macht eine Pressekonferenz über Bierdeckel.",
|
||||||
|
"Merz tappt Sauerland. Niemand ist überrascht.",
|
||||||
|
"Das Volk wirkt „Leck Eier“. Es ist sehr effektiv.",
|
||||||
|
"Merz versucht zu mulliganen, aber die Koalition sagt nein.",
|
||||||
|
"BlackRock ruft an. Merz geht natürlich ran.",
|
||||||
|
"Merz zieht eine Karte. Es ist wieder ein Wahlversprechen. Er wirft es ab.",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Game() {
|
||||||
|
const [library, setLibrary] = useState<Card[]>([]);
|
||||||
|
const [hand, setHand] = useState<Card[]>([]);
|
||||||
|
const [grave, setGrave] = useState<Card[]>([]);
|
||||||
|
const [polls, setPolls] = useState(25);
|
||||||
|
const [eggs, setEggs] = useState(0);
|
||||||
|
const [log, setLog] = useState<string[]>(["Merz, Leck Eier betritt das Schlachtfeld. Aus einem Privatjet."]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLibrary(shuffle(DECK));
|
||||||
|
try {
|
||||||
|
setEggs(Number(localStorage.getItem("eggs") ?? 0));
|
||||||
|
} catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const say = (line: string) => setLog((l) => [line, ...l].slice(0, 6));
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
if (library.length === 0) {
|
||||||
|
say("Deine Bibliothek ist leer. Wie das Rentenkonzept.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [top, ...rest] = library;
|
||||||
|
setLibrary(rest);
|
||||||
|
setHand((h) => [...h, top]);
|
||||||
|
say(`Du ziehst „${top.name}“.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cast(i: number) {
|
||||||
|
const card = hand[i];
|
||||||
|
setHand((h) => h.filter((_, j) => j !== i));
|
||||||
|
setGrave((g) => [card, ...g]);
|
||||||
|
const dmg = card.name === "Leck Eier" ? 10 : Math.ceil(Math.random() * 4);
|
||||||
|
setPolls((p) => Math.max(0, p - dmg));
|
||||||
|
say(`Du wirkst „${card.name}“. Merz verliert ${dmg}% Zustimmung.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mulligan() {
|
||||||
|
setLibrary(shuffle(DECK));
|
||||||
|
setHand([]);
|
||||||
|
setGrave([]);
|
||||||
|
setPolls(25);
|
||||||
|
say("Mulligan! Neue Hand, gleicher Kanzler.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function lick() {
|
||||||
|
const n = eggs + 1;
|
||||||
|
setEggs(n);
|
||||||
|
try {
|
||||||
|
localStorage.setItem("eggs", String(n));
|
||||||
|
} catch {}
|
||||||
|
say(LOG_LINES[Math.floor(Math.random() * LOG_LINES.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="board">
|
||||||
|
<div className="commander">
|
||||||
|
<div className="label">Commander</div>
|
||||||
|
<CardView card={COMMANDER} big />
|
||||||
|
<div className="life">
|
||||||
|
<span>Umfragewerte</span>
|
||||||
|
<strong className={polls === 0 ? "dead" : ""}>{polls}%</strong>
|
||||||
|
<div className="meter">
|
||||||
|
<div style={{ width: `${(polls / 25) * 100}%` }} />
|
||||||
|
</div>
|
||||||
|
{polls === 0 && <p className="win">🏆 Du hast gewonnen. Neuwahlen!</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="side">
|
||||||
|
<div className="buttons">
|
||||||
|
<button onClick={draw}>🃏 Karte ziehen ({library.length})</button>
|
||||||
|
<button onClick={mulligan} className="ghost">
|
||||||
|
🔄 Mulligan
|
||||||
|
</button>
|
||||||
|
<button onClick={lick} className="egg">
|
||||||
|
🥚 Leck Eier ({eggs})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ol className="log">
|
||||||
|
{log.map((l, i) => (
|
||||||
|
<li key={i + l}>{l}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<div className="grave">
|
||||||
|
⚰️ Friedhof: {grave.length ? grave.map((c) => c.name).join(", ") : "leer (wie die Staatskasse)"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Deine Hand {hand.length > 0 && <small>— klick eine Karte zum Wirken</small>}</h2>
|
||||||
|
{hand.length === 0 ? (
|
||||||
|
<p className="empty">Noch keine Karten. Zieh eine, bevor Merz es privatisiert.</p>
|
||||||
|
) : (
|
||||||
|
<div className="hand">
|
||||||
|
{hand.map((c, i) => (
|
||||||
|
<button key={i + c.name} className="card-btn" onClick={() => cast(i)}>
|
||||||
|
<CardView card={c} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
162
app/cards.ts
Normal file
162
app/cards.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
export type Color = "B" | "W" | "G" | "R" | "U" | "C" | "gold";
|
||||||
|
|
||||||
|
export type Card = {
|
||||||
|
name: string;
|
||||||
|
cost: string[];
|
||||||
|
type: string;
|
||||||
|
text: string[];
|
||||||
|
flavor?: string;
|
||||||
|
pt?: string;
|
||||||
|
color: Color;
|
||||||
|
rarity: "common" | "uncommon" | "rare" | "mythic";
|
||||||
|
art: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMMANDER: Card = {
|
||||||
|
name: "Merz, Leck Eier",
|
||||||
|
cost: ["€", "€", "€", "B"],
|
||||||
|
type: "Legendäre Kreatur — Sauerländer Millionär",
|
||||||
|
text: [
|
||||||
|
"Fliegend (nur im Privatjet)",
|
||||||
|
"Bierdeckel — {T}: Jeder Spieler mit mehr als einer Million Lebenspunkten zahlt keine Steuern.",
|
||||||
|
"Wenn Merz, Leck Eier ins Spiel kommt, vergiss alle Wahlversprechen.",
|
||||||
|
],
|
||||||
|
flavor: "„Ich gehöre zur gehobenen Mittelschicht.“",
|
||||||
|
pt: "1/1.000.000",
|
||||||
|
color: "gold",
|
||||||
|
rarity: "mythic",
|
||||||
|
art: "🧓✈️💶",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DECK: Card[] = [
|
||||||
|
{
|
||||||
|
name: "Schuldenbremse lösen",
|
||||||
|
cost: ["X", "U"],
|
||||||
|
type: "Spontanzauber",
|
||||||
|
text: [
|
||||||
|
"Tu so, als hättest du nie etwas anderes gesagt.",
|
||||||
|
"Ziehe X Karten, wobei X gleich 500 Milliarden ist.",
|
||||||
|
],
|
||||||
|
flavor: "Im Wahlkampf war das noch ein Konter.",
|
||||||
|
color: "U",
|
||||||
|
rarity: "rare",
|
||||||
|
art: "🔓💸",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "BlackRock-Aufsichtsrat",
|
||||||
|
cost: ["4", "B"],
|
||||||
|
type: "Artefakt",
|
||||||
|
text: [
|
||||||
|
"Zu Beginn deines Versorgungssegments erhältst du einen Aktienspielstein.",
|
||||||
|
"Opfere einen Rentner: Erhalte 3 Aktienspielsteine.",
|
||||||
|
],
|
||||||
|
flavor: "Unabhängig von Lobbyinteressen. Garantiert.",
|
||||||
|
color: "B",
|
||||||
|
rarity: "rare",
|
||||||
|
art: "🏦📈",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Privatjet zur Hochzeit",
|
||||||
|
cost: ["6"],
|
||||||
|
type: "Artefakt — Fahrzeug",
|
||||||
|
text: ["Fliegend", "Besatzung 1 (nur Liberale)", "Sylt ist ein legales Ziel für diese Karte."],
|
||||||
|
flavor: "Die Bahn war leider ausgebucht. Seit 1994.",
|
||||||
|
pt: "4/4",
|
||||||
|
color: "C",
|
||||||
|
rarity: "uncommon",
|
||||||
|
art: "🛩️💍",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bierdeckel",
|
||||||
|
cost: ["1"],
|
||||||
|
type: "Artefakt — Ausrüstung",
|
||||||
|
text: [
|
||||||
|
"Die ausgerüstete Kreatur kann ihre Steuererklärung in einem Zug machen.",
|
||||||
|
"Ausrüsten {0}. Nur für Spitzenverdiener.",
|
||||||
|
],
|
||||||
|
flavor: "Seit 2003 in Entwicklung.",
|
||||||
|
color: "C",
|
||||||
|
rarity: "common",
|
||||||
|
art: "🍺🧾",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Brandmauer",
|
||||||
|
cost: ["2", "W"],
|
||||||
|
type: "Kreatur — Mauer",
|
||||||
|
text: [
|
||||||
|
"Verteidiger",
|
||||||
|
"Die Brandmauer hat Widerstandskraft 0, solange eine Abstimmung im Januar stattfindet.",
|
||||||
|
],
|
||||||
|
flavor: "Stabil wie ein Bierdeckel im Regen.",
|
||||||
|
pt: "0/10",
|
||||||
|
color: "W",
|
||||||
|
rarity: "common",
|
||||||
|
art: "🧱🔥",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sauerland",
|
||||||
|
cost: [],
|
||||||
|
type: "Standardland — Sauerland",
|
||||||
|
text: ["{T}: Erzeuge {€}{€}.", "Sauerland kommt getappt ins Spiel, genau wie der Kanzler."],
|
||||||
|
color: "G",
|
||||||
|
rarity: "common",
|
||||||
|
art: "🌲🏔️",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Leck Eier",
|
||||||
|
cost: ["R", "R"],
|
||||||
|
type: "Spontanzauber",
|
||||||
|
text: [
|
||||||
|
"Neutralisiere einen Wahlversprechen-Zauber deiner Wahl.",
|
||||||
|
"Rückblende {Volksmund}",
|
||||||
|
],
|
||||||
|
flavor: "— Die Bevölkerung, einstimmig",
|
||||||
|
color: "R",
|
||||||
|
rarity: "mythic",
|
||||||
|
art: "🥚🥚",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Friedrichs Kaffeefahrt",
|
||||||
|
cost: ["3", "B"],
|
||||||
|
type: "Hexerei",
|
||||||
|
text: [
|
||||||
|
"Jeder Gegner wirft eine Karte ab und kauft eine Heizdecke.",
|
||||||
|
"Du erhältst 2 Lebenspunkte pro verkaufter Heizdecke.",
|
||||||
|
],
|
||||||
|
color: "B",
|
||||||
|
rarity: "uncommon",
|
||||||
|
art: "☕🛋️",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Agenda 2030 Rückkehr",
|
||||||
|
cost: ["2", "W", "B"],
|
||||||
|
type: "Verzauberung",
|
||||||
|
text: [
|
||||||
|
"Alle Arbeiter-Kreaturen erhalten -1/-1 und Wachsamkeit (Überstunden).",
|
||||||
|
"Kreaturen mit Stärke unter 1.000.000 können nicht angreifen.",
|
||||||
|
],
|
||||||
|
flavor: "„Wir müssen wieder mehr arbeiten.“ — gesagt von einem Aufsichtsrat.",
|
||||||
|
color: "gold",
|
||||||
|
rarity: "rare",
|
||||||
|
art: "⏰🛠️",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Kanzlertunnel",
|
||||||
|
cost: ["5", "U", "U"],
|
||||||
|
type: "Hexerei",
|
||||||
|
text: ["Du hast diesen Zug keine Ahnung, was im Land passiert.", "Nimm einen zusätzlichen Zug nach Davos."],
|
||||||
|
color: "U",
|
||||||
|
rarity: "rare",
|
||||||
|
art: "🕳️🏔️",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Eierlikör der Einheit",
|
||||||
|
cost: ["G"],
|
||||||
|
type: "Spontanzauber",
|
||||||
|
text: ["Alle Spieler erhalten 1 Lebenspunkt und vergessen die letzte Regierungserklärung."],
|
||||||
|
flavor: "Wirkt besser als jede Reform.",
|
||||||
|
color: "G",
|
||||||
|
rarity: "common",
|
||||||
|
art: "🥃🥚",
|
||||||
|
},
|
||||||
|
];
|
||||||
137
app/globals.css
Normal file
137
app/globals.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
@import url("https://fonts.googleapis.com/css2?family=Cinzel:wght@700;900&family=Crimson+Text:ital,wght@0,400;0,700;1,400&display=swap");
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #120d0a;
|
||||||
|
--fg: #f3e9d2;
|
||||||
|
--muted: #b8a88a;
|
||||||
|
--gold: #e8b64c;
|
||||||
|
--red: #d8452f;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; }
|
||||||
|
body {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse at top, #3a2413 0%, transparent 60%),
|
||||||
|
repeating-linear-gradient(45deg, #140f0b 0 12px, #110c09 12px 24px);
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: "Crimson Text", Georgia, serif;
|
||||||
|
font-size: 18px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main { max-width: 1100px; margin: 0 auto; padding: 24px 16px 64px; }
|
||||||
|
h1, h2, .bar, button, .label { font-family: "Cinzel", serif; }
|
||||||
|
h2 { color: var(--gold); margin: 48px 0 16px; font-size: 1.5rem; }
|
||||||
|
h2 small { color: var(--muted); font-size: 0.9rem; font-weight: 400; }
|
||||||
|
|
||||||
|
.hero { text-align: center; padding: 32px 0 16px; }
|
||||||
|
.kicker { color: var(--muted); text-transform: uppercase; letter-spacing: 0.15em; font-size: 0.8rem; }
|
||||||
|
.hero h1 {
|
||||||
|
font-size: clamp(3rem, 12vw, 7rem);
|
||||||
|
margin: 8px 0;
|
||||||
|
line-height: 1;
|
||||||
|
background: linear-gradient(180deg, #fff3c4, var(--gold) 55%, #8a5a12);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
filter: drop-shadow(0 4px 0 #000);
|
||||||
|
}
|
||||||
|
.hero h1 span { font-style: italic; color: var(--red); -webkit-text-fill-color: var(--red); }
|
||||||
|
.sub { max-width: 620px; margin: 0 auto; color: var(--muted); font-size: 1.15rem; }
|
||||||
|
|
||||||
|
/* board */
|
||||||
|
.board { display: grid; grid-template-columns: auto 1fr; gap: 32px; margin-top: 32px; align-items: start; }
|
||||||
|
@media (max-width: 760px) { .board { grid-template-columns: 1fr; justify-items: center; } }
|
||||||
|
.label { color: var(--gold); text-align: center; margin-bottom: 8px; letter-spacing: 0.2em; font-size: 0.8rem; }
|
||||||
|
.life { margin-top: 16px; text-align: center; }
|
||||||
|
.life span { color: var(--muted); display: block; font-size: 0.9rem; }
|
||||||
|
.life strong { font-family: "Cinzel", serif; font-size: 2.5rem; color: var(--gold); }
|
||||||
|
.life strong.dead { color: var(--red); }
|
||||||
|
.meter { height: 10px; background: #2b1e14; border-radius: 99px; overflow: hidden; margin-top: 4px; }
|
||||||
|
.meter div { height: 100%; background: linear-gradient(90deg, var(--red), var(--gold)); transition: width 0.4s; }
|
||||||
|
.win { color: var(--gold); font-size: 1.3rem; animation: pulse 1s infinite alternate; }
|
||||||
|
@keyframes pulse { to { transform: scale(1.08); } }
|
||||||
|
|
||||||
|
.side { width: 100%; }
|
||||||
|
.buttons { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
|
button {
|
||||||
|
cursor: pointer; border: 2px solid var(--gold); background: linear-gradient(#5a3b16, #2e1d0a);
|
||||||
|
color: var(--fg); padding: 12px 18px; border-radius: 10px; font-size: 1rem; font-weight: 700;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
button:hover { transform: translateY(-2px); }
|
||||||
|
button:active { transform: translateY(1px); }
|
||||||
|
button.ghost { background: transparent; }
|
||||||
|
button.egg { border-color: var(--red); background: linear-gradient(#7a2517, #3c0f08); }
|
||||||
|
|
||||||
|
.log { list-style: none; padding: 16px; margin: 16px 0; background: #0009; border: 1px solid #4a3620; border-radius: 10px; min-height: 180px; }
|
||||||
|
.log li { padding: 4px 0; border-bottom: 1px dashed #3a2a18; color: var(--muted); }
|
||||||
|
.log li:first-child { color: var(--fg); }
|
||||||
|
.grave { color: var(--muted); font-style: italic; }
|
||||||
|
.empty { color: var(--muted); font-style: italic; }
|
||||||
|
|
||||||
|
.hand, .gallery { display: flex; flex-wrap: wrap; gap: 16px; }
|
||||||
|
.gallery { justify-content: center; }
|
||||||
|
.card-btn { all: unset; cursor: pointer; transition: transform 0.15s; }
|
||||||
|
.card-btn:hover { transform: translateY(-10px) rotate(-2deg); }
|
||||||
|
.card-btn:focus-visible { outline: 3px solid var(--gold); border-radius: 14px; }
|
||||||
|
|
||||||
|
/* card */
|
||||||
|
.card {
|
||||||
|
--frame: #bbb; --box: #eee;
|
||||||
|
width: 250px; aspect-ratio: 63 / 88; padding: 10px; border-radius: 14px;
|
||||||
|
background: #111; box-shadow: 0 10px 30px #000a; color: #111; font-size: 13px;
|
||||||
|
}
|
||||||
|
.card.big { width: 300px; font-size: 15px; }
|
||||||
|
.card-inner {
|
||||||
|
height: 100%; display: flex; flex-direction: column; gap: 6px; padding: 8px;
|
||||||
|
border-radius: 8px; background: var(--frame); position: relative;
|
||||||
|
}
|
||||||
|
.bar {
|
||||||
|
display: flex; justify-content: space-between; align-items: center; gap: 6px;
|
||||||
|
background: linear-gradient(#fff8, #fff2); border: 2px solid #0006; border-radius: 6px;
|
||||||
|
padding: 3px 7px; font-weight: 700; font-size: 0.85em; box-shadow: inset 0 1px 0 #fff8;
|
||||||
|
}
|
||||||
|
.title span:first-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.cost { display: flex; gap: 2px; flex-shrink: 0; }
|
||||||
|
.art {
|
||||||
|
flex: 1 1 42%; min-height: 0; display: grid; place-items: center; font-size: 3.4em;
|
||||||
|
border: 2px solid #0008; border-radius: 3px;
|
||||||
|
background: radial-gradient(circle at 30% 30%, #fff6, transparent 60%), linear-gradient(135deg, #0003, #0000), var(--box);
|
||||||
|
}
|
||||||
|
.textbox {
|
||||||
|
flex: 1 1 40%; background: #f6efdd; border: 2px solid #0006; border-radius: 3px;
|
||||||
|
padding: 6px 8px; overflow: auto; line-height: 1.25;
|
||||||
|
}
|
||||||
|
.textbox p { margin: 0 0 5px; }
|
||||||
|
.flavor { font-style: italic; color: #444; border-top: 1px solid #0002; padding-top: 4px; }
|
||||||
|
.pt {
|
||||||
|
position: absolute; right: 6px; bottom: 2px; background: var(--frame); border: 2px solid #0008;
|
||||||
|
border-radius: 6px; padding: 1px 8px; font-family: "Cinzel", serif; font-weight: 900; font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.rarity { font-size: 1.1em; }
|
||||||
|
.r-common { color: #111; } .r-uncommon { color: #9fb3c0; } .r-rare { color: #d4a938; }
|
||||||
|
.r-mythic { color: #e0561b; text-shadow: 0 0 4px #ff9a3c; }
|
||||||
|
|
||||||
|
.mana {
|
||||||
|
display: inline-grid; place-items: center; width: 1.35em; height: 1.35em; border-radius: 50%;
|
||||||
|
font-family: "Cinzel", serif; font-weight: 900; font-size: 0.8em; color: #111;
|
||||||
|
background: #cbc2bf; border: 1px solid #0007; vertical-align: middle; margin: 0 1px;
|
||||||
|
}
|
||||||
|
.mana-B { background: #a69f9d; } .mana-W { background: #fffbd5; } .mana-U { background: #aae0fa; }
|
||||||
|
.mana-R { background: #f9aa8f; } .mana-G { background: #9bd3ae; } .mana-E { background: #f2c94c; }
|
||||||
|
.mana-T { background: #cbc2bf; }
|
||||||
|
|
||||||
|
.c-B { --frame: linear-gradient(160deg, #4a4543, #1d1a19); --box: #6b5f5a; }
|
||||||
|
.c-W { --frame: linear-gradient(160deg, #f5f0dc, #cfc6a8); --box: #efe6c8; }
|
||||||
|
.c-U { --frame: linear-gradient(160deg, #3d7fb8, #1a4166); --box: #79add6; }
|
||||||
|
.c-R { --frame: linear-gradient(160deg, #d0492f, #7a1f10); --box: #ec8a6b; }
|
||||||
|
.c-G { --frame: linear-gradient(160deg, #3d8a55, #1a4a2a); --box: #7cbf8e; }
|
||||||
|
.c-C { --frame: linear-gradient(160deg, #b9b3ad, #7b746d); --box: #ccc6be; }
|
||||||
|
.c-gold { --frame: linear-gradient(160deg, #f0d27a, #a67a1c); --box: #f5dc92; }
|
||||||
|
.c-B .pt, .c-U .pt, .c-R .pt, .c-G .pt { color: #fff; }
|
||||||
|
|
||||||
|
.rules ol { color: var(--muted); line-height: 1.7; }
|
||||||
|
footer { margin-top: 64px; padding-top: 24px; border-top: 1px solid #3a2a18; color: #7d6e55; font-size: 0.85rem; text-align: center; }
|
||||||
20
app/layout.tsx
Normal file
20
app/layout.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Merz leck Eier",
|
||||||
|
description: "Das offizielle Commander-Deck der Bundesregierung. Satire.",
|
||||||
|
icons: { icon: "/favicon.svg" },
|
||||||
|
openGraph: {
|
||||||
|
title: "Merz leck Eier 🥚🥚",
|
||||||
|
description: "Legendäre Kreatur — Sauerländer Millionär. Satire.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="de">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
app/page.tsx
Normal file
46
app/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { CardView } from "./CardView";
|
||||||
|
import { DECK } from "./cards";
|
||||||
|
import { Game } from "./Game";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<header className="hero">
|
||||||
|
<p className="kicker">Commander-Format · Bundestagsedition · Nicht turnierlegal</p>
|
||||||
|
<h1>
|
||||||
|
Merz <span>leck</span> Eier
|
||||||
|
</h1>
|
||||||
|
<p className="sub">
|
||||||
|
Das einzige Magic-Deck, in dem der Commander mit 1.000.000 Widerstandskraft startet und trotzdem jede
|
||||||
|
Abstimmung verliert.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<Game />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Die komplette Decklist</h2>
|
||||||
|
<div className="gallery">
|
||||||
|
{DECK.map((c) => (
|
||||||
|
<CardView key={c.name} card={c} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="rules">
|
||||||
|
<h2>Regeln</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Merz startet mit 25% Umfragewerten. Bring sie auf 0.</li>
|
||||||
|
<li>Jede gewirkte Karte kostet ihn Zustimmung. „Leck Eier“ kostet ihn 10%.</li>
|
||||||
|
<li>Merz darf keinen Mulligan nehmen. Das darfst nur du.</li>
|
||||||
|
<li>Bierdeckel zählen nicht als Steuererklärung (außer für Merz).</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
Satire. Keine echten Zitate außer denen, die er wirklich gesagt hat. Magic: The Gathering ist eine Marke von
|
||||||
|
Wizards of the Coast — die haben damit nichts zu tun und wollen es auch nicht.
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
next.config.mjs
Normal file
2
next.config.mjs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
export default { output: "standalone" };
|
||||||
1063
package-lock.json
generated
Normal file
1063
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "merz-leck-eier",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^15.5.0",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"typescript": "^5"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/favicon.svg
Normal file
1
public/favicon.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><text y="52" font-size="52">🥚</text></svg>
|
||||||
|
After Width: | Height: | Size: 106 B |
40
tsconfig.json
Normal file
40
tsconfig.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user