card game
This commit is contained in:
226
card-game/generate_box.js
Normal file
226
card-game/generate_box.js
Normal file
@@ -0,0 +1,226 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { miniCardGroup } = require('./trox_card');
|
||||
|
||||
/* ============================================================
|
||||
TROX – Tuck Box Generator
|
||||
------------------------------------------------------------
|
||||
Box-Vorderseite zeigt ECHTE TROX-Karten (identisch zu den
|
||||
Spielkarten, via gemeinsames Modul trox_card.js), gefächert.
|
||||
Teile der hinteren Karten werden durch Überlappung verdeckt,
|
||||
der untere Bereich durch das TROX-Logo – genau wie gewünscht.
|
||||
|
||||
node generate_box.js front | wrap | both
|
||||
|
||||
HINWEIS DRUCK (TGC): exakte Stanzkontur "Poker Tuck Box (90 cards)"
|
||||
von der TGC-Produktseite laden und PANEL_W/PANEL_H/SPINE/FLAP unten
|
||||
daran angleichen – das Artwork skaliert automatisch mit.
|
||||
============================================================ */
|
||||
|
||||
const MODE = (process.argv[2] || 'both').toLowerCase();
|
||||
|
||||
const DPI = 300;
|
||||
const PANEL_W = Math.round(2.55 * DPI); // 765
|
||||
const PANEL_H = Math.round(3.55 * DPI); // 1065
|
||||
const SPINE = Math.round(0.90 * DPI); // 270
|
||||
const FLAP = Math.round(0.55 * DPI); // 165
|
||||
const BLEED = 37;
|
||||
const SAFE = 75;
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
FÄCHER-TUNING (hier anpassen):
|
||||
CARD_SCALE – Größe einer Einzelkarte relativ zur Panelbreite.
|
||||
Höher = größere Karten. Per Kommandozeile überschreibbar:
|
||||
node generate_box.js front 0.32
|
||||
FAN_SPREAD – wie weit der Fächer auseinandergeht (horizontaler Versatz).
|
||||
FAN_ARC – wie stark der Fächer-Bogen nach unten ausschwingt.
|
||||
FAN_TILT – maximaler Drehwinkel der äußeren Karten (Grad).
|
||||
FAN_CY – vertikale Lage des Fächermittelpunkts (Anteil der Höhe).
|
||||
Eine eingebaute Sicherung verkleinert den Fächer automatisch, falls er
|
||||
sonst aus dem goldenen Rahmen (Safe Zone) herausragen würde.
|
||||
------------------------------------------------------------ */
|
||||
const CLI_SCALE = parseFloat(process.argv[3]);
|
||||
const CARD_SCALE = !isNaN(CLI_SCALE) ? CLI_SCALE : 0.30; // vorher faktisch 0.42 -> jetzt kleiner
|
||||
const FAN_SPREAD = 0.50;
|
||||
const FAN_ARC = 0.11;
|
||||
const FAN_TILT = 24;
|
||||
const FAN_CY = 0.34;
|
||||
|
||||
const DEFS = `
|
||||
<defs>
|
||||
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#111827" />
|
||||
<stop offset="100%" stop-color="#1F2937" />
|
||||
</linearGradient>
|
||||
<linearGradient id="goldGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#D4AF37" />
|
||||
<stop offset="50%" stop-color="#F3E5AB" />
|
||||
<stop offset="100%" stop-color="#AA7C11" />
|
||||
</linearGradient>
|
||||
</defs>`;
|
||||
|
||||
// Gefächerte ECHTE Karten.
|
||||
// cx,cy = Fächermittelpunkt
|
||||
// cardW = Breite einer Karte in px
|
||||
// maxRight= rechte Grenze (Safe-Zone-Rand), darf nicht überschritten werden
|
||||
// Gibt {svg, fittedCardW} zurück; verkleinert cardW automatisch, falls nötig.
|
||||
function cardFan(cx, cy, cardW, panelW) {
|
||||
const REF_RATIO = 1069 / 694; // Höhe/Breite einer Karte
|
||||
const tiltRad = FAN_TILT * Math.PI / 180;
|
||||
|
||||
// Innenkante des goldenen Rahmens (Safe Zone) plus kleiner Puffer
|
||||
const margin = SAFE + 6;
|
||||
const leftLimit = margin;
|
||||
const rightLimit = panelW - margin;
|
||||
|
||||
// Funktion: rechte Außenkante des Fächers für ein gegebenes cardW
|
||||
function rightEdge(cw) {
|
||||
const dx = cw * FAN_SPREAD;
|
||||
const cardH = cw * REF_RATIO;
|
||||
// halbe Diagonale der gedrehten Außenkarte (worst case Ausdehnung)
|
||||
const halfSpanX = (Math.abs(Math.cos(tiltRad)) * cw + Math.abs(Math.sin(tiltRad)) * cardH) / 2;
|
||||
const outerCenterX = cx + 1.55 * dx;
|
||||
return outerCenterX + halfSpanX;
|
||||
}
|
||||
function leftEdge(cw) {
|
||||
const dx = cw * FAN_SPREAD;
|
||||
const cardH = cw * REF_RATIO;
|
||||
const halfSpanX = (Math.abs(Math.cos(tiltRad)) * cw + Math.abs(Math.sin(tiltRad)) * cardH) / 2;
|
||||
const outerCenterX = cx - 1.55 * dx;
|
||||
return outerCenterX - halfSpanX;
|
||||
}
|
||||
|
||||
// Auto-Fit: cardW so weit verkleinern, bis beide Außenkanten im Rahmen liegen
|
||||
let cw = cardW;
|
||||
let guard = 0;
|
||||
while ((rightEdge(cw) > rightLimit || leftEdge(cw) < leftLimit) && guard < 200) {
|
||||
cw *= 0.98;
|
||||
guard++;
|
||||
}
|
||||
|
||||
const dx = cw * FAN_SPREAD;
|
||||
const dy = cw * FAN_ARC;
|
||||
const t = FAN_TILT;
|
||||
const svg = `
|
||||
<g>
|
||||
${miniCardGroup('Gruen', '7', cx - 1.55 * dx, cy + 1.7 * dy, cw, -t)}
|
||||
${miniCardGroup('Schwarz', '12', cx - 0.52 * dx, cy + 0.3 * dy, cw, -t / 3)}
|
||||
${miniCardGroup('Rot', '17', cx + 0.52 * dx, cy + 0.3 * dy, cw, t / 3)}
|
||||
${miniCardGroup('Gelb', '4', cx + 1.55 * dx, cy + 1.7 * dy, cw, t)}
|
||||
</g>`;
|
||||
return { svg, fittedCardW: cw };
|
||||
}
|
||||
|
||||
function frontContent(w, h) {
|
||||
const cx = w / 2;
|
||||
// gewünschte Kartengröße aus CARD_SCALE; Auto-Fit hält sie im Rahmen
|
||||
const wantCardW = w * CARD_SCALE;
|
||||
const fan = cardFan(cx, h * FAN_CY, wantCardW, w);
|
||||
return `
|
||||
<rect width="${w}" height="${h}" fill="url(#bgGradient)" />
|
||||
<rect x="${SAFE}" y="${SAFE}" width="${w - 2 * SAFE}" height="${h - 2 * SAFE}" rx="14" fill="none" stroke="url(#goldGradient)" stroke-width="2" opacity="0.7" />
|
||||
|
||||
<!-- ECHTE TROX-Karten als Fächer (Mittelpunkt im oberen Drittel) -->
|
||||
${fan.svg}
|
||||
|
||||
<!-- TROX-Logo überdeckt den unteren Teil des Fächers -->
|
||||
<text x="${cx}" y="${h * 0.64}" font-family="'Helvetica Neue',Arial Black,Arial,sans-serif" font-weight="900" font-size="${Math.round(w * 0.20)}" fill="url(#goldGradient)" text-anchor="middle" letter-spacing="${Math.round(w * 0.01)}">TROX</text>
|
||||
<text x="${cx}" y="${h * 0.70}" font-family="'Helvetica Neue',Arial,sans-serif" font-size="${Math.round(w * 0.038)}" fill="#F3E5AB" text-anchor="middle" letter-spacing="4">TACTICAL CARD GAME</text>
|
||||
|
||||
<!-- Footer-Badge -->
|
||||
<rect x="${cx - w * 0.36}" y="${h * 0.85}" width="${w * 0.72}" height="${h * 0.06}" rx="8" fill="none" stroke="url(#goldGradient)" stroke-width="1" opacity="0.8" />
|
||||
<text x="${cx}" y="${h * 0.89}" font-family="'Helvetica Neue',Arial,sans-serif" font-size="${Math.round(w * 0.032)}" fill="#e5e7eb" text-anchor="middle" letter-spacing="1">2-6 Players | Ages 7+ | 80 Cards</text>`;
|
||||
}
|
||||
|
||||
function backContent(w, h) {
|
||||
const cx = w / 2;
|
||||
return `
|
||||
<rect width="${w}" height="${h}" fill="url(#bgGradient)" />
|
||||
<rect x="${SAFE}" y="${SAFE}" width="${w - 2 * SAFE}" height="${h - 2 * SAFE}" rx="14" fill="none" stroke="url(#goldGradient)" stroke-width="2" opacity="0.7" />
|
||||
<text x="${cx}" y="${SAFE + 80}" font-family="'Helvetica Neue',Arial,sans-serif" font-weight="bold" font-size="${Math.round(w * 0.06)}" fill="url(#goldGradient)" text-anchor="middle">HOW TO PLAY</text>
|
||||
<g font-family="'Helvetica Neue',Arial,sans-serif" font-size="${Math.round(w * 0.032)}" fill="#e5e7eb">
|
||||
<text x="${SAFE + 20}" y="${SAFE + 160}">Predict exactly how many tricks</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 205}">you will win each round.</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 275}">Hit your bid exactly -> +10 bonus.</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 320}">Miss it -> no bonus at all.</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 390}">Red (Crown) is the permanent</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 435}">trump and beats every suit.</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 505}">10 rounds. Cards rise then fall:</text>
|
||||
<text x="${SAFE + 20}" y="${SAFE + 550}">1-2-3-4-5-5-4-3-2-1</text>
|
||||
</g>
|
||||
<text x="${cx}" y="${h - SAFE - 120}" font-family="'Helvetica Neue',Arial,sans-serif" font-style="italic" font-size="${Math.round(w * 0.034)}" fill="#F3E5AB" text-anchor="middle">"Bid exactly. Win tricks.</text>
|
||||
<text x="${cx}" y="${h - SAFE - 80}" font-family="'Helvetica Neue',Arial,sans-serif" font-style="italic" font-size="${Math.round(w * 0.034)}" fill="#F3E5AB" text-anchor="middle">Rule the table."</text>`;
|
||||
}
|
||||
|
||||
function spineContent(w, h) {
|
||||
return `
|
||||
<rect width="${w}" height="${h}" fill="url(#bgGradient)" />
|
||||
<text x="${w / 2}" y="${h / 2}" font-family="'Helvetica Neue',Arial Black,sans-serif" font-weight="900" font-size="${Math.round(w * 0.5)}" fill="url(#goldGradient)" text-anchor="middle" dominant-baseline="middle" letter-spacing="${Math.round(w * 0.12)}" transform="rotate(90 ${w / 2} ${h / 2})">TROX</text>`;
|
||||
}
|
||||
|
||||
function buildFront() {
|
||||
const w = PANEL_W + 2 * BLEED;
|
||||
const h = PANEL_H + 2 * BLEED;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${h}" width="${w}" height="${h}">
|
||||
${DEFS}
|
||||
${frontContent(w, h)}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function buildWrap() {
|
||||
const bandW = PANEL_W + SPINE + PANEL_W + SPINE;
|
||||
const bandH = PANEL_H;
|
||||
const totalW = bandW + 2 * BLEED;
|
||||
const totalH = bandH + 2 * FLAP + 2 * BLEED;
|
||||
const ox = BLEED, oy = BLEED + FLAP;
|
||||
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${totalW} ${totalH}" width="${totalW}" height="${totalH}">
|
||||
${DEFS}
|
||||
<rect width="${totalW}" height="${totalH}" fill="#0b0f17" />
|
||||
|
||||
<g transform="translate(${ox}, ${BLEED})">
|
||||
<rect width="${PANEL_W}" height="${FLAP}" fill="url(#bgGradient)" opacity="0.85" />
|
||||
<path d="M ${PANEL_W * 0.12} 0 Q ${PANEL_W / 2} ${-FLAP * 0.5} ${PANEL_W * 0.88} 0" fill="url(#bgGradient)" opacity="0.6"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${ox}, ${oy})">
|
||||
<svg x="0" y="0" width="${PANEL_W}" height="${PANEL_H}" viewBox="0 0 ${PANEL_W} ${PANEL_H}">${DEFS}${frontContent(PANEL_W, PANEL_H)}</svg>
|
||||
<g transform="translate(${PANEL_W}, 0)">
|
||||
<svg x="0" y="0" width="${SPINE}" height="${PANEL_H}" viewBox="0 0 ${SPINE} ${PANEL_H}">${DEFS}${spineContent(SPINE, PANEL_H)}</svg>
|
||||
</g>
|
||||
<g transform="translate(${PANEL_W + SPINE}, 0)">
|
||||
<svg x="0" y="0" width="${PANEL_W}" height="${PANEL_H}" viewBox="0 0 ${PANEL_W} ${PANEL_H}">${DEFS}${backContent(PANEL_W, PANEL_H)}</svg>
|
||||
</g>
|
||||
<g transform="translate(${PANEL_W + SPINE + PANEL_W}, 0)">
|
||||
<svg x="0" y="0" width="${SPINE}" height="${PANEL_H}" viewBox="0 0 ${SPINE} ${PANEL_H}">${DEFS}${spineContent(SPINE, PANEL_H)}</svg>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g transform="translate(${ox}, ${oy + PANEL_H})">
|
||||
<rect width="${PANEL_W}" height="${FLAP}" fill="url(#bgGradient)" opacity="0.85" />
|
||||
</g>
|
||||
|
||||
<g stroke="#6b7280" stroke-width="1" stroke-dasharray="6 6" opacity="0.5" fill="none">
|
||||
<line x1="${ox}" y1="${oy}" x2="${ox + bandW}" y2="${oy}" />
|
||||
<line x1="${ox}" y1="${oy + PANEL_H}" x2="${ox + bandW}" y2="${oy + PANEL_H}" />
|
||||
<line x1="${ox + PANEL_W}" y1="${oy}" x2="${ox + PANEL_W}" y2="${oy + PANEL_H}" />
|
||||
<line x1="${ox + PANEL_W + SPINE}" y1="${oy}" x2="${ox + PANEL_W + SPINE}" y2="${oy + PANEL_H}" />
|
||||
<line x1="${ox + PANEL_W + SPINE + PANEL_W}" y1="${oy}" x2="${ox + PANEL_W + SPINE + PANEL_W}" y2="${oy + PANEL_H}" />
|
||||
</g>
|
||||
<text x="${totalW / 2}" y="${totalH - 12}" font-family="Arial" font-size="20" fill="#6b7280" text-anchor="middle">Faltlinien (gestrichelt) vor dem Upload entfernen - Maße an TGC-Tuckbox-Template angleichen</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
const outputDir = path.join(__dirname, 'box_export');
|
||||
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
console.log('TROX – Box wird generiert ...');
|
||||
if (MODE === 'front' || MODE === 'both') {
|
||||
fs.writeFileSync(path.join(outputDir, 'trox_box_front.svg'), buildFront());
|
||||
console.log(` Front-Panel -> box_export/trox_box_front.svg (${PANEL_W + 2 * BLEED}x${PANEL_H + 2 * BLEED})`);
|
||||
}
|
||||
if (MODE === 'wrap' || MODE === 'both') {
|
||||
fs.writeFileSync(path.join(outputDir, 'trox_box_wrap.svg'), buildWrap());
|
||||
console.log(` Box-Netz -> box_export/trox_box_wrap.svg`);
|
||||
}
|
||||
console.log('Fertig.');
|
||||
Reference in New Issue
Block a user