Files
infrapuzzle/projects/sasch40/frontend/scripts/generate_print_pdf.mjs
T

664 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});