// This file is automatically generated - edits will be lost! 'use server'; /** * @fileOverview Uses Google Imagen4 to generate custom TCG cards based on user parameters. * * - generateTCGCard - A function that handles the card generation process. * - GenerateTCGCardInput - The input type for the generateTCGCard function. * - GenerateTCGCardOutput - The return type for the generateTCGCard function. */ import { z } from 'genkit'; import { GoogleGenAI } from '@google/genai'; import { getUserApiKeys } from '@/lib/firestore'; import type { TCGGame } from '@/types'; import { getGameConfig } from '@/config/tcg-games'; const GenerateTCGCardInputSchema = z.object({ userId: z.string().min(1, "User ID is required"), game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), characterName: z.string().min(1, "Character name is required"), characterType: z.string().min(1, "Character type is required"), isSpecialArt: z.boolean(), isHolo: z.boolean(), backgroundDescription: z.string().min(1, "Background description is required"), characterDescription: z.string().min(1, "Character description is required"), language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']).default('imagen-4.0-ultra-generate-001'), // Pokemon-specific hp: z.number().optional(), attackName1: z.string().optional(), attackDamage1: z.number().optional(), attackName2: z.string().optional(), attackDamage2: z.number().optional(), weakness: z.string().optional(), resistance: z.string().optional(), retreatCost: z.number().optional(), // One Piece-specific power: z.number().optional(), cost: z.number().optional(), counter: z.number().optional(), color: z.string().optional(), lifePoints: z.number().optional(), // Lorcana-specific inkCost: z.number().optional(), strength: z.number().optional(), willpower: z.number().optional(), lore: z.number().optional(), inkable: z.boolean().optional(), // Magic-specific manaCost: z.string().optional(), cardType: z.string().optional(), subType: z.string().optional(), powerToughness: z.string().optional(), // Dragon Ball-specific combatPower: z.number().optional(), comboCost: z.number().optional(), comboEnergy: z.number().optional(), era: z.string().optional(), }); export type GenerateTCGCardInput = z.infer; const GenerateTCGCardOutputSchema = z.object({ imageBase64: z.string().describe("The generated card image as base64 string").optional(), prompt: z.string().describe("The prompt used to generate the card").optional(), error: z.string().describe("Error message if the card could not be generated").optional() }); export type GenerateTCGCardOutput = z.infer; export async function generateTCGCard(input: GenerateTCGCardInput): Promise { try { // Get user's API keys from Firestore const userApiKeys = await getUserApiKeys(input.userId); const apiKey = userApiKeys?.geminiApiKey || process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY; if (!apiKey) { return { error: 'Gemini API key is required. Please configure your API key in Settings or set GOOGLE_GENAI_API_KEY environment variable.' }; } const genAI = new GoogleGenAI({ apiKey: apiKey, }); // Generate the detailed prompt for the card const prompt = generateCardPrompt(input); console.log("Generated prompt:", prompt); // Check if using Gemini model (for generateContentStream) or Imagen model (for generateImages) const isGeminiModel = input.model.startsWith('gemini-'); if (isGeminiModel) { // Use generateContentStream for Gemini models const config = { responseModalities: ['IMAGE', 'TEXT'], imageConfig: { aspectRatio: '3:4', imageSize: '1K', }, }; const contents = [ { role: 'user' as const, parts: [ { text: prompt, }, ], }, ]; const response = await genAI.models.generateContentStream({ model: input.model, config, contents, }); let imageBase64: string | undefined; for await (const chunk of response) { if (!chunk.candidates || !chunk.candidates[0].content || !chunk.candidates[0].content.parts) { continue; } if (chunk.candidates?.[0]?.content?.parts?.[0]?.inlineData) { const inlineData = chunk.candidates[0].content.parts[0].inlineData; imageBase64 = inlineData.data || ''; break; // We only need the first image } } if (!imageBase64) { return { error: 'No image generated from Gemini model.' }; } return { imageBase64, prompt, }; } else { // Use generateImages for Imagen models const response = await genAI.models.generateImages({ model: input.model, prompt: prompt, config: { numberOfImages: 1, outputMimeType: 'image/jpeg', aspectRatio: '3:4' // TCG cards are roughly 3:4 aspect ratio }, }); if (!response?.generatedImages || response.generatedImages.length === 0) { return { error: 'No images generated.' }; } const generatedImage = response.generatedImages[0]; if (!generatedImage?.image?.imageBytes) { return { error: 'Generated image data is missing.' }; } return { imageBase64: generatedImage.image.imageBytes, prompt: prompt, }; } } catch (error: any) { console.error('Error generating TCG card:', error); return { error: `Failed to generate card: ${error?.message || 'Unknown error'}` }; } } function generateCardPrompt(params: GenerateTCGCardInput): string { const gameConfig = getGameConfig(params.game as TCGGame); const gameName = gameConfig.name; const { game, characterName, characterType, isSpecialArt, isHolo, backgroundDescription, characterDescription, language, } = params; const cardType = isSpecialArt ? "full art special variant" : "standard layout"; const holoEffect = isHolo ? `The card's surface has a "holo" effect, creating a shimmering, rainbow-like reflection that highlights the character and key elements.` : ""; const languageInstruction = language !== 'english' ? `Write all card information in ${language}. The character name, abilities, and text should be translated accordingly.` : ''; let gameSpecificPrompt = ''; switch (game) { case 'pokemon': gameSpecificPrompt = generatePokemonPrompt(params); break; case 'onepiece': gameSpecificPrompt = generateOnePiecePrompt(params); break; case 'lorcana': gameSpecificPrompt = generateLorcanaPrompt(params); break; case 'magic': gameSpecificPrompt = generateMagicPrompt(params); break; case 'dragonball': gameSpecificPrompt = generateDragonBallPrompt(params); break; } return `A ${cardType} collectible ${gameName} trading card, featuring "${characterName}". ${isSpecialArt ? "The illustration covers the entire card in a stunning full-art layout" : "The card has a standard vertical layout with a detailed illustration in the main area."} ${characterName}, a ${characterType} type, is depicted ${characterDescription}. The background of the illustration shows ${backgroundDescription}. ${holoEffect} ${gameSpecificPrompt} ${languageInstruction} The overall style should match official ${gameName} card design with proper fonts, layout, and professional quality artwork. Make sure all information is displayed clearly and legibly. The output should just be the card itself.`; } function generatePokemonPrompt(params: GenerateTCGCardInput): string { const { characterName, characterType, hp = 130, attackName1 = "Quick Attack", attackDamage1 = 60, attackName2 = "Special Move", attackDamage2 = 90, weakness = "Fighting", resistance = "Psychic", retreatCost = 2, } = params; return `The card layout includes: The top left corner displays "${characterName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${characterType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1}, deals ${attackDamage1} damage. The second attack is ${attackName2}, deals ${attackDamage2} damage. Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information.`; } function generateOnePiecePrompt(params: GenerateTCGCardInput): string { const { characterName, characterType, power = 5000, cost = 4, counter = 1000, lifePoints, } = params; const leaderInfo = lifePoints ? `As a Leader card, it displays ${lifePoints} life points in the top corner.` : ''; return `The card layout follows One Piece Card Game design: The character name "${characterName}" appears prominently at the top. The card shows a ${characterType} color indicator. The cost of ${cost} is displayed in the top left corner. The power value of ${power} is shown prominently. The counter value of ${counter} appears at the bottom. ${leaderInfo} The card includes attribute icons and a text box for abilities. The bottom features the One Piece Card Game logo and card number.`; } function generateLorcanaPrompt(params: GenerateTCGCardInput): string { const { characterName, characterType, inkCost = 3, strength = 2, willpower = 3, lore = 2, inkable = true, } = params; const inkableIndicator = inkable ? 'The card has an inkwell symbol in the bottom left, indicating it is inkable.' : 'No inkwell symbol appears, as this card is not inkable.'; return `The card layout follows Disney Lorcana design: "${characterName}" appears as the character name at the top. The ${characterType} ink color is indicated by the card's color scheme and symbol. The ink cost of ${inkCost} is displayed in a prominent circle in the top left corner. The card shows strength of ${strength} (shield icon), willpower of ${willpower} (hexagon icon), and lore value of ${lore} (diamond icon) clearly displayed. ${inkableIndicator} The card includes a text box for character abilities and flavor text in the Disney Lorcana style. The bottom features the Lorcana logo, card number, and rarity symbol.`; } function generateMagicPrompt(params: GenerateTCGCardInput): string { const { characterName, characterType, manaCost = '{2}{G}', cardType = 'Creature', subType = 'Elf Warrior', powerToughness = '3/3', } = params; return `The card layout follows Magic: The Gathering design: "${characterName}" appears as the card name at the top. The mana cost ${manaCost} is displayed in the top right corner using standard Magic mana symbols. The ${characterType} color frame is used. The type line reads "${cardType} — ${subType}". The main illustration takes up the center portion of the card. Below the illustration is a text box for abilities and flavor text. ${cardType.toLowerCase().includes('creature') ? `The bottom right corner displays the power/toughness of ${powerToughness}.` : ''} The bottom features the expansion symbol, card number, artist credit, and copyright information in standard Magic: The Gathering layout.`; } function generateDragonBallPrompt(params: GenerateTCGCardInput): string { const { characterName, characterType, combatPower = 20000, comboCost = 1, comboEnergy = 5000, era = 'Universe Survival Saga', } = params; return `The card layout follows Dragon Ball Super Card Game design: The character name "${characterName}" appears at the top in the distinctive Dragon Ball font. The card uses a ${characterType} color border and energy symbols. The combat power of ${combatPower} is prominently displayed in the top right corner. The combo cost shows ${comboCost} and combo energy displays ${comboEnergy}. The era "${era}" is indicated on the card. The card includes energy cost symbols, special traits, and a text box for card effects. The character illustration is dynamic and action-packed. The bottom features the Dragon Ball Super Card Game logo, card number, and rarity indicator.`; }