664 lines
22 KiB
JavaScript
664 lines
22 KiB
JavaScript
import fs from 'fs';
|
||
import path from 'path';
|
||
import puppeteer from 'puppeteer-core';
|
||
|
||
const BASE_DIR = '/Users/moritz/src/infrapuzzle/projects/sasch40';
|
||
const ARTIFACTS_DIR = '/Users/moritz/.gemini/antigravity/brain/26049ae8-3106-42e0-922f-356dbbfe162a';
|
||
|
||
// Helper to get base64 data URI
|
||
function getBase64Image(filePath) {
|
||
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(BASE_DIR, filePath);
|
||
if (!fs.existsSync(fullPath)) {
|
||
console.warn(`File not found: ${fullPath}`);
|
||
return '';
|
||
}
|
||
const ext = path.extname(fullPath).toLowerCase();
|
||
let mime = 'image/jpeg';
|
||
if (ext === '.png') mime = 'image/png';
|
||
if (ext === '.svg') mime = 'image/svg+xml';
|
||
const data = fs.readFileSync(fullPath);
|
||
return `data:${mime};base64,${data.toString('base64')}`;
|
||
}
|
||
|
||
async function generatePDF() {
|
||
console.log('🎨 Generating printable Mittelalter Retro PDF for Saschas Kronkorken Kreuzzug...');
|
||
|
||
// 1. Assets
|
||
const saschaPortrait = getBase64Image('frontend/public/assets/characters/sascha/portrait_v2.jpg');
|
||
const qrCode = getBase64Image('qrcode_game.png');
|
||
|
||
// Mini avatars for entourage strip
|
||
const entourage = [
|
||
{ name: 'Königin Verena', img: getBase64Image('frontend/public/assets/characters/verena/portrait_v2.jpg') },
|
||
{ name: 'Saskia', img: getBase64Image('frontend/public/assets/characters/saskia/portrait_v2.jpg') },
|
||
{ name: 'Maja', img: getBase64Image('frontend/public/assets/characters/maja/portrait_v2.jpg') },
|
||
{ name: 'Miriam', img: getBase64Image('frontend/public/assets/characters/miriam/portrait_v2.jpg') },
|
||
{ name: 'Markus (Vater)', img: getBase64Image('frontend/public/assets/characters/markus/portrait.jpg') },
|
||
{ name: 'NAT (Josef)', img: getBase64Image('frontend/public/assets/characters/nat/portrait_v2.jpg') },
|
||
{ name: 'Lutz (ZTUL)', img: getBase64Image('frontend/public/assets/characters/lutz/portrait_v2.jpg') },
|
||
{ name: 'Armend', img: getBase64Image('frontend/public/assets/characters/armend/portrait_v2.jpg') },
|
||
{ name: 'Amy', img: getBase64Image('frontend/public/assets/characters/amy/portrait.jpg') },
|
||
{ name: 'Mane', img: getBase64Image('frontend/public/assets/characters/mane/portrait_v2.jpg') },
|
||
{ name: 'Hansi', img: getBase64Image('frontend/public/assets/characters/hansi/portrait_v2.jpg') },
|
||
{ name: 'Mario', img: getBase64Image('frontend/public/assets/characters/mario/portrait_v2.jpg') },
|
||
{ name: 'Flo Maier', img: getBase64Image('frontend/public/assets/characters/flo_maier/portrait_v2.jpg') },
|
||
{ name: 'Pü', img: getBase64Image('frontend/public/assets/characters/pue/portrait_v2.jpg') },
|
||
{ name: 'Viktor', img: getBase64Image('frontend/public/assets/characters/viktor/portrait_v2.jpg') }
|
||
];
|
||
|
||
const htmlContent = `
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Saschas Kronkorken Kreuzzug – Proklamation zum 40. Geburtstag</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=MedievalSharp&family=Press+Start+2P&family=Cinzel:wght@700&display=swap" rel="stylesheet">
|
||
<style>
|
||
@page {
|
||
size: A4 portrait;
|
||
margin: 0;
|
||
}
|
||
*, *::before, *::after {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
body {
|
||
width: 210mm;
|
||
height: 297mm;
|
||
margin: 0;
|
||
padding: 6mm;
|
||
background: #1e0f07;
|
||
font-family: 'MedievalSharp', Georgia, serif;
|
||
color: #2b1810;
|
||
-webkit-print-color-adjust: exact;
|
||
print-color-adjust: exact;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
/* Heavy Carved Oak & Gold Outer Frame */
|
||
.outer-scroll-frame {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #3d2112 0%, #200f07 50%, #452414 100%);
|
||
padding: 3.5mm;
|
||
border: 4px solid #b45309;
|
||
box-shadow: 0 0 25px rgba(0, 0, 0, 0.85);
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Antique Parchment Inner Page */
|
||
.parchment-sheet {
|
||
width: 100%;
|
||
height: 100%;
|
||
background:
|
||
radial-gradient(circle at 50% 15%, rgba(255, 255, 255, 0.45) 0%, transparent 60%),
|
||
radial-gradient(circle at 50% 85%, rgba(138, 77, 30, 0.2) 0%, transparent 70%),
|
||
linear-gradient(180deg, #fbf5e6 0%, #f6e8cc 35%, #edd7ae 75%, #e2c694 100%);
|
||
border: 3px double #b45309;
|
||
outline: 2px solid #d4af37;
|
||
padding: 5mm 6mm;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
position: relative;
|
||
overflow: hidden;
|
||
box-shadow: inset 0 0 35px rgba(115, 60, 18, 0.35);
|
||
}
|
||
|
||
/* Corner Golden Brackets */
|
||
.corner {
|
||
position: absolute;
|
||
width: 20px;
|
||
height: 20px;
|
||
z-index: 10;
|
||
}
|
||
.c-tl { top: 2.5mm; left: 2.5mm; border-top: 3px solid #d4af37; border-left: 3px solid #d4af37; }
|
||
.c-tr { top: 2.5mm; right: 2.5mm; border-top: 3px solid #d4af37; border-right: 3px solid #d4af37; }
|
||
.c-bl { bottom: 2.5mm; left: 2.5mm; border-bottom: 3px solid #d4af37; border-left: 3px solid #d4af37; }
|
||
.c-br { bottom: 2.5mm; right: 2.5mm; border-bottom: 3px solid #d4af37; border-right: 3px solid #d4af37; }
|
||
|
||
/* Header Section */
|
||
.royal-header {
|
||
text-align: center;
|
||
margin-bottom: 2mm;
|
||
}
|
||
.proclamation-badge {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 7.5pt;
|
||
color: #92400e;
|
||
letter-spacing: 1.5px;
|
||
margin-bottom: 1.5mm;
|
||
text-transform: uppercase;
|
||
display: inline-block;
|
||
padding: 1.2mm 3.5mm;
|
||
background: rgba(212, 175, 55, 0.2);
|
||
border: 1.5px solid #b45309;
|
||
border-radius: 4px;
|
||
}
|
||
.main-game-title {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 15pt;
|
||
color: #451a03;
|
||
text-shadow: 2px 2px 0px #ffd700, 3px 3px 0px #78350f;
|
||
line-height: 1.35;
|
||
margin-bottom: 1.5mm;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.main-subtitle {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 11pt;
|
||
font-weight: 700;
|
||
color: #78350f;
|
||
letter-spacing: 1.5px;
|
||
text-transform: uppercase;
|
||
}
|
||
.heraldic-divider {
|
||
margin: 2mm 0 3mm 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 3mm;
|
||
color: #b45309;
|
||
font-size: 9pt;
|
||
}
|
||
.heraldic-line {
|
||
flex: 1;
|
||
height: 1.5px;
|
||
background: linear-gradient(90deg, transparent, #b45309, #d4af37, #b45309, transparent);
|
||
}
|
||
|
||
/* Hero Row: King Portrait + Chronicle Lore */
|
||
.hero-lore-row {
|
||
display: flex;
|
||
gap: 4mm;
|
||
align-items: stretch;
|
||
margin-bottom: 2.5mm;
|
||
}
|
||
.king-portrait-card {
|
||
width: 50mm;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
background: #fbf5e6;
|
||
padding: 2.5mm;
|
||
border: 2px solid #b45309;
|
||
box-shadow: 0 4px 12px rgba(92, 51, 23, 0.35);
|
||
border-radius: 3px;
|
||
}
|
||
.king-portrait-frame {
|
||
width: 100%;
|
||
height: 44mm;
|
||
border: 3px solid #d4af37;
|
||
outline: 2px solid #78350f;
|
||
overflow: hidden;
|
||
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
|
||
background: #111;
|
||
}
|
||
.king-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
image-rendering: pixelated;
|
||
}
|
||
.king-name-tag {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 6pt;
|
||
color: #78350f;
|
||
margin-top: 2mm;
|
||
text-align: center;
|
||
line-height: 1.3;
|
||
}
|
||
.king-sub-tag {
|
||
font-size: 7.5pt;
|
||
color: #5c3010;
|
||
text-align: center;
|
||
font-style: italic;
|
||
margin-top: 0.8mm;
|
||
}
|
||
|
||
/* Lore Narrative Box */
|
||
.lore-narrative-box {
|
||
flex: 1;
|
||
background: rgba(255, 255, 255, 0.45);
|
||
border: 1.5px solid #d4af37;
|
||
padding: 2.5mm 3.5mm;
|
||
border-radius: 3px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
box-shadow: inset 0 0 15px rgba(212, 175, 55, 0.15);
|
||
}
|
||
.lore-title {
|
||
font-family: 'Cinzel', serif;
|
||
font-size: 10.5pt;
|
||
font-weight: 700;
|
||
color: #78350f;
|
||
border-bottom: 1.5px solid #d4af37;
|
||
padding-bottom: 0.8mm;
|
||
margin-bottom: 1.5mm;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
.lore-p {
|
||
font-size: 8.5pt;
|
||
line-height: 1.32;
|
||
color: #2b1810;
|
||
margin-bottom: 1.5mm;
|
||
text-align: justify;
|
||
}
|
||
.lore-p strong {
|
||
color: #78350f;
|
||
}
|
||
.lore-highlight {
|
||
background: rgba(212, 175, 55, 0.18);
|
||
border-left: 3px solid #b45309;
|
||
padding: 1.2mm 2mm;
|
||
font-size: 8pt;
|
||
line-height: 1.3;
|
||
color: #522504;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* Quests Grid */
|
||
.quests-section {
|
||
margin-bottom: 2.5mm;
|
||
}
|
||
.section-ribbon {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 7pt;
|
||
color: #451a03;
|
||
text-align: center;
|
||
margin-bottom: 2mm;
|
||
letter-spacing: 0.5px;
|
||
background: linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.4), transparent);
|
||
padding: 0.8mm 0;
|
||
}
|
||
.quests-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 2mm;
|
||
}
|
||
.quest-card {
|
||
background: rgba(255, 255, 255, 0.55);
|
||
border: 1.5px solid #b45309;
|
||
border-radius: 3px;
|
||
padding: 2mm 2.5mm;
|
||
box-shadow: 0 2px 6px rgba(115, 60, 18, 0.15);
|
||
}
|
||
.quest-card-header {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 5.5pt;
|
||
color: #78350f;
|
||
margin-bottom: 1mm;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1.5mm;
|
||
}
|
||
.quest-card-desc {
|
||
font-size: 7.5pt;
|
||
line-height: 1.25;
|
||
color: #381c0c;
|
||
}
|
||
.quest-card-desc strong {
|
||
color: #78350f;
|
||
}
|
||
|
||
/* Action & QR Code Section */
|
||
.qr-action-banner {
|
||
background: linear-gradient(135deg, #3d1f0f 0%, #2a140a 100%);
|
||
color: #fef3c7;
|
||
border: 3px solid #d4af37;
|
||
border-radius: 4px;
|
||
padding: 2.5mm 3.5mm;
|
||
display: flex;
|
||
gap: 4mm;
|
||
align-items: center;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.45);
|
||
margin-bottom: 2.2mm;
|
||
}
|
||
.qr-frame {
|
||
background: #fef3c7;
|
||
padding: 1.8mm;
|
||
border: 2px solid #ffd700;
|
||
border-radius: 3px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-shadow: 0 0 10px rgba(255, 215, 0, 0.3);
|
||
}
|
||
.qr-img {
|
||
width: 29mm;
|
||
height: 29mm;
|
||
display: block;
|
||
image-rendering: pixelated;
|
||
}
|
||
.qr-tag {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 4.8pt;
|
||
color: #451a03;
|
||
margin-top: 1.2mm;
|
||
font-weight: bold;
|
||
}
|
||
.qr-instructions {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
.qr-action-title {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 8pt;
|
||
color: #ffd700;
|
||
text-shadow: 1px 1px 0 #000;
|
||
line-height: 1.3;
|
||
margin-bottom: 1.5mm;
|
||
}
|
||
.qr-steps {
|
||
list-style: none;
|
||
font-size: 7.5pt;
|
||
line-height: 1.35;
|
||
color: #fde68a;
|
||
}
|
||
.qr-steps li {
|
||
margin-bottom: 0.8mm;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1.8mm;
|
||
}
|
||
.qr-step-num {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 5pt;
|
||
background: #ffd700;
|
||
color: #451a03;
|
||
padding: 0.6mm 1.2mm;
|
||
border-radius: 2px;
|
||
font-weight: bold;
|
||
}
|
||
.qr-url-pill {
|
||
margin-top: 1.2mm;
|
||
display: inline-block;
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 6pt;
|
||
color: #38bdf8;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
border: 1px dashed #38bdf8;
|
||
padding: 1mm 2mm;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Entourage Cast Strip */
|
||
.entourage-strip {
|
||
background: rgba(255, 255, 255, 0.4);
|
||
border: 1px solid #d4af37;
|
||
border-radius: 3px;
|
||
padding: 1.5mm 2mm;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1mm;
|
||
}
|
||
.entourage-label {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 4.8pt;
|
||
color: #78350f;
|
||
text-align: center;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
.entourage-avatars {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.entourage-avatar-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.6mm;
|
||
}
|
||
.entourage-avatar-img {
|
||
width: 6.8mm;
|
||
height: 6.8mm;
|
||
border-radius: 50%;
|
||
border: 1.2px solid #b45309;
|
||
outline: 1px solid #d4af37;
|
||
object-fit: cover;
|
||
image-rendering: pixelated;
|
||
}
|
||
.entourage-name {
|
||
font-family: 'Press Start 2P', monospace;
|
||
font-size: 3pt;
|
||
color: #451a03;
|
||
white-space: nowrap;
|
||
text-align: center;
|
||
}
|
||
|
||
/* Royal Seal / Sign-off */
|
||
.royal-signoff {
|
||
text-align: center;
|
||
font-size: 6.5pt;
|
||
color: #78350f;
|
||
margin-top: 1mm;
|
||
font-style: italic;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="outer-scroll-frame">
|
||
<div class="parchment-sheet">
|
||
<!-- 4 Golden Corner Accents -->
|
||
<div class="corner c-tl"></div>
|
||
<div class="corner c-tr"></div>
|
||
<div class="corner c-bl"></div>
|
||
<div class="corner c-br"></div>
|
||
|
||
<!-- Header -->
|
||
<header class="royal-header">
|
||
<div class="proclamation-badge">👑 KÖNIGLICHE PROKLAMATION ZUM 40. GEBURTSTAG 👑</div>
|
||
<h1 class="main-game-title">SASCHAS KRONKORKEN KREUZZUG</h1>
|
||
<div class="main-subtitle">40 JAHRE BASS & GERSTENSAFT • HERZOGTUM HAUMDAUCHER</div>
|
||
<div class="heraldic-divider">
|
||
<div class="heraldic-line"></div>
|
||
<span>⚜️</span>
|
||
<span>✦</span>
|
||
<span>👑</span>
|
||
<span>✦</span>
|
||
<span>🍻</span>
|
||
<span>✦</span>
|
||
<span>⚔️</span>
|
||
<span>✦</span>
|
||
<span>⚜️</span>
|
||
<div class="heraldic-line"></div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Hero Lore: King Sascha I. Portrait + Story -->
|
||
<div class="hero-lore-row">
|
||
<div class="king-portrait-card">
|
||
<div class="king-portrait-frame">
|
||
<img src="${saschaPortrait}" alt="König Sascha I." class="king-img" />
|
||
</div>
|
||
<div class="king-name-tag">KÖNIG SASCHA I.</div>
|
||
<div class="king-sub-tag">Der 40-jährige Imperator</div>
|
||
</div>
|
||
|
||
<div class="lore-narrative-box">
|
||
<div class="lore-title">
|
||
<span>📜 DIE CHRONIK DES REICHES</span>
|
||
<span>ANNO 2026</span>
|
||
</div>
|
||
<p class="lore-p">
|
||
<strong>Hört, hört, wertes Feiervolk und edle Zechkumpanen!</strong><br>
|
||
Seit nunmehr 40 stolzen Jahren regiert <strong>König Sascha I.</strong> das Herzogtum Haumdaucher mit unerschütterlicher Kraft, seiner legendären dunklen Sonnenbrille und wummernden <strong>135-BPM-Techno-Bässen</strong> aus den Schlossmauern!
|
||
</p>
|
||
<p class="lore-p">
|
||
Doch kurz vor dem heutigen Thronjubiläum schlug das finstere Schicksal zu: Die königliche Schatzkammer wurde geplündert! Sämtliche <strong>40 geheiligten Kronkorken</strong> des Reiches liegen über ganz Haumdaucher verstreut – und Saschas geliebte Schwestern sind in den Wolkentürmen verschollen!
|
||
</p>
|
||
<div class="lore-highlight">
|
||
🚨 Die Lage ist dramatisch: Ohne Kronkorken kein kühles Helles, ohne Helles kein Bass – und ohne Bass keine Jubiläumsfeier! Das Schicksal der Regentschaft liegt in deinen Händen!
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quests Section: 4 Core Adventures -->
|
||
<div class="quests-section">
|
||
<div class="section-ribbon">⚔️ DEINE KÖNIGLICHEN MISSIONEN IM SPIEL ⚔️</div>
|
||
<div class="quests-grid">
|
||
<div class="quest-card">
|
||
<div class="quest-card-header">
|
||
<span>☁️</span>
|
||
<span>AKT II: SCHWESTERN-RETTUNG</span>
|
||
</div>
|
||
<p class="quest-card-desc">
|
||
Hüpfe im Arcade-Doodle-Jump von Wolke zu Wolke hinauf und befreie Saschas Schwestern <strong>Saskia, Maja & Miriam</strong> aus ihren Wolkentürmen!
|
||
</p>
|
||
</div>
|
||
|
||
<div class="quest-card">
|
||
<div class="quest-card-header">
|
||
<span>🍻</span>
|
||
<span>AKT III: DER SCHANKRAUM</span>
|
||
</div>
|
||
<p class="quest-card-desc">
|
||
Erkunde das Wirtshaus: Triff <strong>Königin Verena</strong> auf der Galerie, <strong>Lutz</strong> im Holzbottich, <strong>Armend</strong> am Schmiede-Amboss & öffne die geheime Luke zum VIP-Bunker!
|
||
</p>
|
||
</div>
|
||
|
||
<div class="quest-card">
|
||
<div class="quest-card-header">
|
||
<span>🐗</span>
|
||
<span>HOF-DUELLE: NAT & MARKUS</span>
|
||
</div>
|
||
<p class="quest-card-desc">
|
||
Besiege Hof-Barde <strong>NAT</strong> im <em>Wildschwein-Schnick-Schnack-Schnuck</em> und bluffe <strong>Markus (Vater)</strong> beim taiwanesischen <em>Liar's Dice (吹牛)</em> ab!
|
||
</p>
|
||
</div>
|
||
|
||
<div class="quest-card">
|
||
<div class="quest-card-header">
|
||
<span>🎸</span>
|
||
<span>AKT IV & V: KONZERT & URKUNDE</span>
|
||
</div>
|
||
<p class="quest-card-desc">
|
||
Rocke an der Minnesänger-Laute (Mittelalter Guitar Hero) mit <strong>Ritter Hansi</strong> die Schlossbühne und erhalte deine persönliche <strong>Highscore-Thronurkunde</strong>!
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action & QR Code Section -->
|
||
<div class="qr-action-banner">
|
||
<div class="qr-frame">
|
||
<img src="${qrCode}" alt="QR Code zum Spiel" class="qr-img" />
|
||
<span class="qr-tag">👑 SCAN & PLAY 👑</span>
|
||
</div>
|
||
|
||
<div class="qr-instructions">
|
||
<div class="qr-action-title">📱 JETZT SCANNEN & DIREKT MITFEIERN!</div>
|
||
<ul class="qr-steps">
|
||
<li>
|
||
<span class="qr-step-num">1</span>
|
||
<span>Kamera-App des Smartphones auf den QR-Code richten</span>
|
||
</li>
|
||
<li>
|
||
<span class="qr-step-num">2</span>
|
||
<span>Tippe auf den Link oder öffne direkt im Browser</span>
|
||
</li>
|
||
<li>
|
||
<span class="qr-step-num">3</span>
|
||
<span>Lautstärke aufdrehen für 135-BPM-Mittelalter-Soundtrack!</span>
|
||
</li>
|
||
<li>
|
||
<span class="qr-step-num">4</span>
|
||
<span>Kronkorken sammeln & den Party-Highscore knacken!</span>
|
||
</li>
|
||
</ul>
|
||
<div class="qr-url-pill">🌐 https://sasch40.haumdaucher.de</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Royal Entourage Strip -->
|
||
<div class="entourage-strip">
|
||
<div class="entourage-label">⚜️ DAS HAUMDAUCHER GEFOLGE – ALLE CHARAKTERE IM SPIEL ⚜️</div>
|
||
<div class="entourage-avatars">
|
||
${entourage.map(c => `
|
||
<div class="entourage-avatar-item" title="${c.name}">
|
||
<img src="${c.img}" alt="${c.name}" class="entourage-avatar-img" />
|
||
<span class="entourage-name">${c.name.split(' ')[0]}</span>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Royal Seal Sign-off -->
|
||
<div class="royal-signoff">
|
||
Besiegelt durch das königliche Hofamt Haumdaucher zum 40. Geburtstag von König Sascha I. • Anno 2026 • Code & Pixel von Mop
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
const htmlPath = path.join(BASE_DIR, 'Saschas_Kronkorken_Kreuzzug_Jubilaeum.html');
|
||
fs.writeFileSync(htmlPath, htmlContent, 'utf-8');
|
||
console.log(`✅ Saved HTML template to ${htmlPath}`);
|
||
|
||
// Launch Puppeteer to render pixel-perfect A4 PDF
|
||
const browser = await puppeteer.launch({
|
||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||
headless: 'new',
|
||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||
});
|
||
|
||
const page = await browser.newPage();
|
||
await page.setContent(htmlContent, { waitUntil: 'load', timeout: 25000 });
|
||
try {
|
||
await page.evaluateHandle('document.fonts.ready');
|
||
} catch (e) {}
|
||
await new Promise(r => setTimeout(r, 1000));
|
||
|
||
// Output paths
|
||
const pdfFileName = 'Saschas_Kronkorken_Kreuzzug_Jubilaeum.pdf';
|
||
const outPdf1 = path.join(BASE_DIR, pdfFileName);
|
||
const outPdf2 = path.join(BASE_DIR, 'frontend/public', pdfFileName);
|
||
const outPdf3 = path.join(ARTIFACTS_DIR, pdfFileName);
|
||
|
||
console.log('🖨️ Rendering A4 PDF...');
|
||
const pdfBuffer = await page.pdf({
|
||
format: 'A4',
|
||
printBackground: true,
|
||
margin: { top: '0mm', right: '0mm', bottom: '0mm', left: '0mm' },
|
||
preferCSSPageSize: true
|
||
});
|
||
|
||
fs.writeFileSync(outPdf1, pdfBuffer);
|
||
fs.writeFileSync(outPdf2, pdfBuffer);
|
||
fs.writeFileSync(outPdf3, pdfBuffer);
|
||
console.log(`✅ PDF written to:\n 1. ${outPdf1}\n 2. ${outPdf2}\n 3. ${outPdf3}`);
|
||
|
||
// Also take a high-res PNG preview of the printed page for visual review!
|
||
await page.setViewport({ width: 1240, height: 1754, deviceScaleFactor: 2 });
|
||
const previewPng = path.join(ARTIFACTS_DIR, 'Saschas_Kronkorken_Kreuzzug_Druck_Vorschau.png');
|
||
await page.screenshot({ path: previewPng, fullPage: true });
|
||
console.log(`📸 Rendered high-res preview screenshot to ${previewPng}`);
|
||
|
||
await browser.close();
|
||
console.log('🎉 PDF generation complete!');
|
||
}
|
||
|
||
generatePDF().catch(err => {
|
||
console.error('❌ Error generating PDF:', err);
|
||
process.exit(1);
|
||
});
|