mirror of
https://github.com/xavidop/cardex.git
synced 2026-08-29 00:01:18 +00:00
320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
// 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<typeof GenerateTCGCardInputSchema>;
|
|
|
|
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<typeof GenerateTCGCardOutputSchema>;
|
|
|
|
export async function generateTCGCard(input: GenerateTCGCardInput): Promise<GenerateTCGCardOutput> {
|
|
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.`;
|
|
}
|