675 lines
23 KiB
TypeScript
675 lines
23 KiB
TypeScript
|
|
import { GoogleGenAI } from "@google/genai";
|
||
|
|
|
||
|
|
const apiKey = process.env.API_KEY;
|
||
|
|
const baseUrl = process.env.BASE_URL;
|
||
|
|
|
||
|
|
const ai = new GoogleGenAI({
|
||
|
|
apiKey: apiKey || '',
|
||
|
|
...(baseUrl && { httpOptions: { baseUrl } })
|
||
|
|
});
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Helper to extract MIME type and base64 data from a data URL.
|
||
|
|
*/
|
||
|
|
const extractBase64Data = (base64String: string): { mimeType: string; data: string } => {
|
||
|
|
// Regex to capture mime type and data
|
||
|
|
const matches = base64String.match(/^data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+);base64,(.+)$/);
|
||
|
|
if (matches && matches.length === 3) {
|
||
|
|
return {
|
||
|
|
mimeType: matches[1],
|
||
|
|
data: matches[2]
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback for strings that might just be the base64 data or simple split
|
||
|
|
const split = base64String.split(',');
|
||
|
|
return {
|
||
|
|
mimeType: 'image/png', // Default fallback
|
||
|
|
data: split.length > 1 ? split[1] : split[0]
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determines the best aspect ratio for the requested grid layout.
|
||
|
|
*
|
||
|
|
* 2x2 -> 1:1 (Square)
|
||
|
|
* 2x3 -> 16:9 (Wide) - 4:3 is okay too, but 16:9 gives more width for separation
|
||
|
|
* 2x4 -> 16:9 (Wide)
|
||
|
|
* 3x3 -> 1:1 (Square)
|
||
|
|
*/
|
||
|
|
const getBestAspectRatio = (rows: number, cols: number): string => {
|
||
|
|
const ratio = cols / rows;
|
||
|
|
|
||
|
|
if (ratio >= 1.8) return "16:9"; // e.g. 2x4 = 2.0
|
||
|
|
if (ratio >= 1.3) return "4:3"; // e.g. 2x3 = 1.5 (16:9 is also fine here, but 4:3 works)
|
||
|
|
if (ratio <= 0.7) return "3:4";
|
||
|
|
if (ratio <= 0.5) return "9:16";
|
||
|
|
|
||
|
|
return "1:1"; // Default for 2x2, 3x3
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a sprite sheet using the original image and the action prompt.
|
||
|
|
* If no image is provided, generates from text description only.
|
||
|
|
*/
|
||
|
|
export const generateSpriteSheet = async (
|
||
|
|
originalBase64: string | null,
|
||
|
|
action: string,
|
||
|
|
rows: number = 2,
|
||
|
|
cols: number = 3,
|
||
|
|
characterDescription?: string
|
||
|
|
): Promise<string> => {
|
||
|
|
if (!apiKey) throw new Error("API Key not found");
|
||
|
|
|
||
|
|
const totalFrames = rows * cols;
|
||
|
|
const aspectRatio = getBestAspectRatio(rows, cols);
|
||
|
|
|
||
|
|
// Build prompt based on whether we have a reference image
|
||
|
|
const hasImage = originalBase64 !== null;
|
||
|
|
|
||
|
|
const prompt = hasImage
|
||
|
|
? `
|
||
|
|
Generate a clean 2D Sprite Sheet.
|
||
|
|
|
||
|
|
INPUT ACTION: ${action}
|
||
|
|
|
||
|
|
LAYOUT CONFIGURATION:
|
||
|
|
- GRID: ${rows} rows by ${cols} columns.
|
||
|
|
- TOTAL SPRITES: ${totalFrames}
|
||
|
|
- STYLE: Flat illustration, white background.
|
||
|
|
|
||
|
|
CRITICAL RULES:
|
||
|
|
1. **STRICT GRID**: You MUST draw exactly ${rows} rows and ${cols} columns.
|
||
|
|
2. **NO NUMBERS**: Do NOT include any numbers, text, arrows, or guide lines.
|
||
|
|
3. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
|
||
|
|
4. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
|
||
|
|
5. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
|
||
|
|
|
||
|
|
Output ONLY the image on a solid white background.
|
||
|
|
`
|
||
|
|
: `
|
||
|
|
Generate a clean 2D Sprite Sheet from scratch.
|
||
|
|
|
||
|
|
CHARACTER DESCRIPTION: ${characterDescription || '一个可爱的卡通角色'}
|
||
|
|
INPUT ACTION: ${action}
|
||
|
|
|
||
|
|
LAYOUT CONFIGURATION:
|
||
|
|
- GRID: ${rows} rows by ${cols} columns.
|
||
|
|
- TOTAL SPRITES: ${totalFrames}
|
||
|
|
- STYLE: Flat illustration, white background, 2D cartoon style.
|
||
|
|
|
||
|
|
CRITICAL RULES:
|
||
|
|
1. **STRICT GRID**: You MUST draw exactly ${rows} rows and ${cols} columns.
|
||
|
|
2. **NO NUMBERS**: Do NOT include any numbers, text, arrows, or guide lines.
|
||
|
|
3. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
|
||
|
|
4. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
|
||
|
|
5. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
|
||
|
|
6. **ANIMATION FLOW**: Each frame should show a smooth progression of the action.
|
||
|
|
|
||
|
|
Output ONLY the image on a solid white background.
|
||
|
|
`;
|
||
|
|
|
||
|
|
// Build content parts
|
||
|
|
const parts: any[] = [];
|
||
|
|
|
||
|
|
if (hasImage) {
|
||
|
|
const { mimeType, data } = extractBase64Data(originalBase64);
|
||
|
|
parts.push({
|
||
|
|
inlineData: {
|
||
|
|
mimeType: mimeType,
|
||
|
|
data: data
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
parts.push({ text: prompt });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await ai.models.generateContent({
|
||
|
|
model: 'gemini-2.5-flash-image',
|
||
|
|
contents: {
|
||
|
|
parts: parts,
|
||
|
|
},
|
||
|
|
config: {
|
||
|
|
temperature: 0.2,
|
||
|
|
imageConfig: {
|
||
|
|
aspectRatio: aspectRatio
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Extract the image from the response
|
||
|
|
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||
|
|
if (part.inlineData) {
|
||
|
|
return `data:image/png;base64,${part.inlineData.data}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
|
||
|
|
if (textPart && textPart.text) {
|
||
|
|
console.warn("Model returned text instead of image:", textPart.text);
|
||
|
|
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("生成失败:未返回图像数据");
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("Generation failed", error);
|
||
|
|
throw new Error(error.message || "生成失败,请重试");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a character portrait/illustration from text description.
|
||
|
|
*/
|
||
|
|
export const generateCharacterPortrait = async (
|
||
|
|
characterDescription: string,
|
||
|
|
style: string = '2D卡通风格',
|
||
|
|
aspectRatio: '1:1' | '3:4' | '9:16' = '3:4'
|
||
|
|
): Promise<string> => {
|
||
|
|
if (!apiKey) throw new Error("API Key not found");
|
||
|
|
|
||
|
|
const prompt = `
|
||
|
|
Generate a single character portrait illustration.
|
||
|
|
|
||
|
|
CHARACTER DESCRIPTION: ${characterDescription}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
1. Full body or upper body character portrait
|
||
|
|
2. Clean, professional illustration style
|
||
|
|
3. Transparent or solid color background
|
||
|
|
4. High quality, detailed artwork
|
||
|
|
5. The character should be centered in the image
|
||
|
|
6. No text, watermarks, or extra elements
|
||
|
|
|
||
|
|
Output a single, high-quality character illustration.
|
||
|
|
`;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await ai.models.generateContent({
|
||
|
|
model: 'gemini-2.5-flash-image',
|
||
|
|
contents: {
|
||
|
|
parts: [{ text: prompt }],
|
||
|
|
},
|
||
|
|
config: {
|
||
|
|
temperature: 0.7,
|
||
|
|
imageConfig: {
|
||
|
|
aspectRatio: aspectRatio
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||
|
|
if (part.inlineData) {
|
||
|
|
return `data:image/png;base64,${part.inlineData.data}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
|
||
|
|
if (textPart && textPart.text) {
|
||
|
|
console.warn("Model returned text instead of image:", textPart.text);
|
||
|
|
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("生成失败:未返回图像数据");
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("Portrait generation failed", error);
|
||
|
|
throw new Error(error.message || "立绘生成失败,请重试");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate a 4-direction sprite sheet (4 rows x 4 cols).
|
||
|
|
* Each row represents a direction: Down, Left, Right, Up
|
||
|
|
* Each row has 4 animation frames for walking/running.
|
||
|
|
*/
|
||
|
|
export const generateDirectionalSpriteSheet = async (
|
||
|
|
originalBase64: string | null,
|
||
|
|
action: string = '行走',
|
||
|
|
characterDescription?: string
|
||
|
|
): Promise<string> => {
|
||
|
|
if (!apiKey) throw new Error("API Key not found");
|
||
|
|
|
||
|
|
const hasImage = originalBase64 !== null;
|
||
|
|
|
||
|
|
const prompt = hasImage
|
||
|
|
? `
|
||
|
|
Generate a 4-direction character sprite sheet for game development.
|
||
|
|
|
||
|
|
INPUT ACTION: ${action}
|
||
|
|
|
||
|
|
LAYOUT CONFIGURATION:
|
||
|
|
- GRID: 4 rows by 4 columns (16 sprites total)
|
||
|
|
- Row 1: Downward walking animation - Character facing down the screen, 4 consecutive frames
|
||
|
|
- Row 2: Leftward walking animation - Character facing left of the screen, 4 consecutive frames
|
||
|
|
- Row 3: Rightward walking animation - Character facing right of the screen, 4 consecutive frames
|
||
|
|
- Row 4: Upward walking animation - Character facing up the screen, 4 consecutive frames
|
||
|
|
- STYLE: Flat illustration, white background, pixel art or 2D game style.
|
||
|
|
|
||
|
|
CRITICAL RULES:
|
||
|
|
1. **STRICT GRID**: You MUST draw exactly 4 rows and 4 columns.
|
||
|
|
2. **DIRECTION ORDER**: Down, Left, Right, Up (top to bottom).
|
||
|
|
3. **NO NUMBERS/TEXT**: Do NOT include any numbers, text, arrows, or guide lines.
|
||
|
|
4. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
|
||
|
|
5. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
|
||
|
|
6. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
|
||
|
|
7. **ANIMATION FLOW**: Each row shows smooth walking/running animation in that direction.
|
||
|
|
|
||
|
|
Output ONLY the image on a solid white background.
|
||
|
|
`
|
||
|
|
: `
|
||
|
|
Generate a 4-direction character sprite sheet for game development from scratch.
|
||
|
|
|
||
|
|
CHARACTER DESCRIPTION: ${characterDescription || '一个可爱的卡通角色'}
|
||
|
|
INPUT ACTION: ${action}
|
||
|
|
|
||
|
|
LAYOUT CONFIGURATION:
|
||
|
|
- GRID: 4 rows by 4 columns (16 sprites total)
|
||
|
|
- Row 1: Downward walking animation - Character facing down the screen, 4 consecutive frames
|
||
|
|
- Row 2: Leftward walking animation - Character facing left of the screen, 4 consecutive frames
|
||
|
|
- Row 3: Rightward walking animation - Character facing right of the screen, 4 consecutive frames
|
||
|
|
- Row 4: Upward walking animation - Character facing up the screen, 4 consecutive frames
|
||
|
|
- STYLE: Flat illustration, white background, 2D cartoon/game style.
|
||
|
|
|
||
|
|
CRITICAL RULES:
|
||
|
|
1. **STRICT GRID**: You MUST draw exactly 4 rows and 4 columns.
|
||
|
|
2. **DIRECTION ORDER**: Down, Left, Right, Up (top to bottom).
|
||
|
|
3. **NO NUMBERS/TEXT**: Do NOT include any numbers, text, arrows, or guide lines.
|
||
|
|
4. **ISOLATION**: Draw each character SMALLER than the grid cell. Leave clear white space around every character.
|
||
|
|
5. **NO OVERLAP**: Characters must NOT touch the imaginary grid lines.
|
||
|
|
6. **CONSISTENCY**: Keep the character size and proportions identical in every frame.
|
||
|
|
7. **ANIMATION FLOW**: Each row shows smooth walking/running animation in that direction.
|
||
|
|
|
||
|
|
Output ONLY the image on a solid white background.
|
||
|
|
`;
|
||
|
|
|
||
|
|
const parts: any[] = [];
|
||
|
|
|
||
|
|
if (hasImage) {
|
||
|
|
const { mimeType, data } = extractBase64Data(originalBase64);
|
||
|
|
parts.push({
|
||
|
|
inlineData: {
|
||
|
|
mimeType: mimeType,
|
||
|
|
data: data
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
parts.push({ text: prompt });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await ai.models.generateContent({
|
||
|
|
model: 'gemini-2.5-flash-image',
|
||
|
|
contents: {
|
||
|
|
parts: parts,
|
||
|
|
},
|
||
|
|
config: {
|
||
|
|
temperature: 0.2,
|
||
|
|
imageConfig: {
|
||
|
|
aspectRatio: "1:1"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||
|
|
if (part.inlineData) {
|
||
|
|
return `data:image/png;base64,${part.inlineData.data}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
|
||
|
|
if (textPart && textPart.text) {
|
||
|
|
console.warn("Model returned text instead of image:", textPart.text);
|
||
|
|
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("生成失败:未返回图像数据");
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("Directional sprite sheet generation failed", error);
|
||
|
|
throw new Error(error.message || "序列帧生成失败,请重试");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Map types for game map generation
|
||
|
|
*/
|
||
|
|
export type MapType = 'rpg' | 'tilemap' | 'platformer' | 'strategy' | 'world';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate game maps of various types
|
||
|
|
*/
|
||
|
|
export const generateGameMap = async (
|
||
|
|
mapType: MapType,
|
||
|
|
description: string,
|
||
|
|
style: string = '像素风格'
|
||
|
|
): Promise<string> => {
|
||
|
|
if (!apiKey) throw new Error("API Key not found");
|
||
|
|
|
||
|
|
const mapPrompts: Record<MapType, string> = {
|
||
|
|
rpg: `
|
||
|
|
Generate a top-down RPG game map scene.
|
||
|
|
|
||
|
|
SCENE DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Top-down/bird's eye view perspective
|
||
|
|
- Include terrain features (grass, paths, water, trees, buildings)
|
||
|
|
- Game-ready art style with clear boundaries
|
||
|
|
- Rich details but not cluttered
|
||
|
|
- Suitable for character movement
|
||
|
|
- No UI elements, text, or markers
|
||
|
|
|
||
|
|
Output a single cohesive map image.
|
||
|
|
`,
|
||
|
|
tilemap: `
|
||
|
|
Generate a tilemap sprite sheet for game development.
|
||
|
|
|
||
|
|
THEME: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
LAYOUT: 8x8 grid of tiles (64 tiles total)
|
||
|
|
|
||
|
|
TILE CATEGORIES TO INCLUDE:
|
||
|
|
- Ground tiles (grass, dirt, sand, stone)
|
||
|
|
- Water tiles (with edges/transitions)
|
||
|
|
- Path/road tiles (straight, corners, intersections)
|
||
|
|
- Decoration tiles (flowers, rocks, bushes)
|
||
|
|
- Building tiles (walls, roofs, doors, windows)
|
||
|
|
- Tree/forest tiles
|
||
|
|
- Transition tiles between different terrains
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Each tile must be clearly separated
|
||
|
|
- Tiles should be seamlessly tileable
|
||
|
|
- Consistent art style across all tiles
|
||
|
|
- White or transparent gaps between tiles
|
||
|
|
- No text, numbers, or labels
|
||
|
|
|
||
|
|
Output ONLY the tilemap sprite sheet.
|
||
|
|
`,
|
||
|
|
platformer: `
|
||
|
|
Generate a side-scrolling platformer game level background.
|
||
|
|
|
||
|
|
SCENE DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Side view perspective
|
||
|
|
- Multiple parallax layers suggested (foreground, midground, background)
|
||
|
|
- Include platforms, obstacles, and environmental elements
|
||
|
|
- Clear ground/floor area for character to walk on
|
||
|
|
- Atmospheric and immersive
|
||
|
|
- Game-ready, not too detailed to distract from gameplay
|
||
|
|
- No characters, UI, or text
|
||
|
|
|
||
|
|
Output a wide panoramic scene image.
|
||
|
|
`,
|
||
|
|
strategy: `
|
||
|
|
Generate a grid-based strategy/tactics game battle map.
|
||
|
|
|
||
|
|
SCENE DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clear grid pattern (hexagonal or square)
|
||
|
|
- Various terrain types with different tactical value
|
||
|
|
- Include obstacles, cover positions, high ground
|
||
|
|
- Bird's eye view perspective
|
||
|
|
- Balanced layout suitable for tactical combat
|
||
|
|
- Clear visual distinction between terrain types
|
||
|
|
- No units, characters, or UI elements
|
||
|
|
|
||
|
|
Output a tactical battle map image.
|
||
|
|
`,
|
||
|
|
world: `
|
||
|
|
Generate a fantasy world map / continent overview.
|
||
|
|
|
||
|
|
DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Cartographic/illustrated map style
|
||
|
|
- Include continents, islands, oceans
|
||
|
|
- Show mountain ranges, forests, deserts, rivers
|
||
|
|
- Mark major cities/locations with simple icons (no text)
|
||
|
|
- Compass rose or decorative border optional
|
||
|
|
- Fantasy/medieval cartography aesthetic
|
||
|
|
- Parchment or clean background
|
||
|
|
- No modern elements
|
||
|
|
|
||
|
|
Output a complete world map image.
|
||
|
|
`
|
||
|
|
};
|
||
|
|
|
||
|
|
const prompt = mapPrompts[mapType];
|
||
|
|
|
||
|
|
// Choose aspect ratio based on map type
|
||
|
|
const aspectRatios: Record<MapType, string> = {
|
||
|
|
rpg: '1:1',
|
||
|
|
tilemap: '1:1',
|
||
|
|
platformer: '16:9',
|
||
|
|
strategy: '1:1',
|
||
|
|
world: '4:3'
|
||
|
|
};
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await ai.models.generateContent({
|
||
|
|
model: 'gemini-2.5-flash-image',
|
||
|
|
contents: {
|
||
|
|
parts: [{ text: prompt }],
|
||
|
|
},
|
||
|
|
config: {
|
||
|
|
temperature: 0.7,
|
||
|
|
imageConfig: {
|
||
|
|
aspectRatio: aspectRatios[mapType]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||
|
|
if (part.inlineData) {
|
||
|
|
return `data:image/png;base64,${part.inlineData.data}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
|
||
|
|
if (textPart && textPart.text) {
|
||
|
|
console.warn("Model returned text instead of image:", textPart.text);
|
||
|
|
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("生成失败:未返回图像数据");
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("Map generation failed", error);
|
||
|
|
throw new Error(error.message || "地图生成失败,请重试");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Item types for game item generation
|
||
|
|
*/
|
||
|
|
export type ItemType = 'weapon' | 'armor' | 'potion' | 'material' | 'treasure' | 'accessory';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generate game items (weapons, armor, potions, etc.)
|
||
|
|
*/
|
||
|
|
export const generateGameItem = async (
|
||
|
|
itemType: ItemType,
|
||
|
|
description: string,
|
||
|
|
style: string = '像素风格',
|
||
|
|
quantity: 'single' | 'set' = 'single'
|
||
|
|
): Promise<string> => {
|
||
|
|
if (!apiKey) throw new Error("API Key not found");
|
||
|
|
|
||
|
|
const itemPrompts: Record<ItemType, string> = {
|
||
|
|
weapon: `
|
||
|
|
Generate game weapon sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
|
||
|
|
|
||
|
|
WEAPON DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
${quantity === 'set' ? `
|
||
|
|
LAYOUT: 2 rows x 3 columns grid (6 weapons total)
|
||
|
|
IMPORTANT: All 6 weapons must be the SAME TYPE as described above.
|
||
|
|
Generate 6 variations/upgrades of "${description}" - different colors, materials, enchantments, or quality levels.
|
||
|
|
Example: If description is "fire sword", generate 6 different fire swords with varying flame effects.
|
||
|
|
` : ''}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clean isolated weapon on transparent/white background
|
||
|
|
- Game-ready icon style
|
||
|
|
- Clear silhouette and details
|
||
|
|
- Consistent lighting (top-left light source)
|
||
|
|
- No hands, characters, or extra elements
|
||
|
|
- Professional game asset quality
|
||
|
|
|
||
|
|
Output ${quantity === 'set' ? 'a grid of 6 similar weapons with variations' : 'a single weapon'} image.
|
||
|
|
`,
|
||
|
|
armor: `
|
||
|
|
Generate game armor/equipment sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
|
||
|
|
|
||
|
|
ARMOR DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
${quantity === 'set' ? `
|
||
|
|
LAYOUT: 2 rows x 3 columns grid (6 pieces total)
|
||
|
|
IMPORTANT: All 6 pieces must be the SAME TYPE as described above.
|
||
|
|
Generate 6 variations/upgrades of "${description}" - different colors, materials, or quality levels.
|
||
|
|
Example: If description is "knight helmet", generate 6 different knight helmets with varying designs.
|
||
|
|
` : ''}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clean isolated equipment on transparent/white background
|
||
|
|
- Game-ready icon style
|
||
|
|
- Clear details and textures
|
||
|
|
- Consistent lighting
|
||
|
|
- No characters wearing them
|
||
|
|
- Professional game asset quality
|
||
|
|
|
||
|
|
Output ${quantity === 'set' ? 'a grid of 6 similar armor pieces with variations' : 'a single armor piece'} image.
|
||
|
|
`,
|
||
|
|
potion: `
|
||
|
|
Generate game potion/consumable sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
|
||
|
|
|
||
|
|
ITEM DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
${quantity === 'set' ? `
|
||
|
|
LAYOUT: 2 rows x 3 columns grid (6 items total)
|
||
|
|
IMPORTANT: All 6 items must be the SAME TYPE as described above.
|
||
|
|
Generate 6 variations of "${description}" - different sizes, fill levels, or potency indicators.
|
||
|
|
Example: If description is "health potion", generate 6 health potions (small/medium/large, different fill levels).
|
||
|
|
` : ''}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clean isolated items on transparent/white background
|
||
|
|
- Game-ready icon style
|
||
|
|
- Glowing/magical effects where appropriate
|
||
|
|
- Clear bottle/container shapes
|
||
|
|
- Professional game asset quality
|
||
|
|
|
||
|
|
Output ${quantity === 'set' ? 'a grid of 6 similar consumables with variations' : 'a single consumable'} image.
|
||
|
|
`,
|
||
|
|
material: `
|
||
|
|
Generate game crafting material sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
|
||
|
|
|
||
|
|
MATERIAL DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
${quantity === 'set' ? `
|
||
|
|
LAYOUT: 2 rows x 3 columns grid (6 materials total)
|
||
|
|
IMPORTANT: All 6 materials must be the SAME TYPE as described above.
|
||
|
|
Generate 6 variations of "${description}" - different quantities, quality levels, or processing stages.
|
||
|
|
Example: If description is "iron ore", generate 6 iron ores (raw/refined, different sizes, different purities).
|
||
|
|
` : ''}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clean isolated materials on transparent/white background
|
||
|
|
- Game-ready icon style
|
||
|
|
- Natural textures and details
|
||
|
|
- Clear identification of material type
|
||
|
|
- Professional game asset quality
|
||
|
|
|
||
|
|
Output ${quantity === 'set' ? 'a grid of 6 similar materials with variations' : 'a single material'} image.
|
||
|
|
`,
|
||
|
|
treasure: `
|
||
|
|
Generate game treasure/container sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
|
||
|
|
|
||
|
|
TREASURE DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
${quantity === 'set' ? `
|
||
|
|
LAYOUT: 2 rows x 3 columns grid (6 items total)
|
||
|
|
IMPORTANT: All 6 items must be the SAME TYPE as described above.
|
||
|
|
Generate 6 variations of "${description}" - different states, sizes, or value levels.
|
||
|
|
Example: If description is "treasure chest", generate 6 treasure chests (closed/open, wooden/golden, small/large).
|
||
|
|
` : ''}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clean isolated items on transparent/white background
|
||
|
|
- Game-ready icon style
|
||
|
|
- Rich, valuable appearance
|
||
|
|
- Gold/metallic sheen where appropriate
|
||
|
|
- Professional game asset quality
|
||
|
|
|
||
|
|
Output ${quantity === 'set' ? 'a grid of 6 similar treasures with variations' : 'a single treasure'} image.
|
||
|
|
`,
|
||
|
|
accessory: `
|
||
|
|
Generate game accessory sprite${quantity === 'set' ? 's (6 variations in a 2x3 grid)' : ''}.
|
||
|
|
|
||
|
|
ACCESSORY DESCRIPTION: ${description}
|
||
|
|
ART STYLE: ${style}
|
||
|
|
|
||
|
|
${quantity === 'set' ? `
|
||
|
|
LAYOUT: 2 rows x 3 columns grid (6 items total)
|
||
|
|
IMPORTANT: All 6 items must be the SAME TYPE as described above.
|
||
|
|
Generate 6 variations of "${description}" - different colors, gem types, or enchantment effects.
|
||
|
|
Example: If description is "magic ring", generate 6 magic rings with different gem colors and effects.
|
||
|
|
` : ''}
|
||
|
|
|
||
|
|
REQUIREMENTS:
|
||
|
|
- Clean isolated accessories on transparent/white background
|
||
|
|
- Game-ready icon style
|
||
|
|
- Magical glow effects where appropriate
|
||
|
|
- Fine details on jewelry
|
||
|
|
- Professional game asset quality
|
||
|
|
|
||
|
|
Output ${quantity === 'set' ? 'a grid of 6 similar accessories with variations' : 'a single accessory'} image.
|
||
|
|
`
|
||
|
|
};
|
||
|
|
|
||
|
|
const prompt = itemPrompts[itemType];
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await ai.models.generateContent({
|
||
|
|
model: 'gemini-2.5-flash-image',
|
||
|
|
contents: {
|
||
|
|
parts: [{ text: prompt }],
|
||
|
|
},
|
||
|
|
config: {
|
||
|
|
temperature: 0.7,
|
||
|
|
imageConfig: {
|
||
|
|
aspectRatio: quantity === 'set' ? '4:3' : '1:1'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const part of response.candidates?.[0]?.content?.parts || []) {
|
||
|
|
if (part.inlineData) {
|
||
|
|
return `data:image/png;base64,${part.inlineData.data}`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const textPart = response.candidates?.[0]?.content?.parts?.find(p => p.text);
|
||
|
|
if (textPart && textPart.text) {
|
||
|
|
console.warn("Model returned text instead of image:", textPart.text);
|
||
|
|
throw new Error("生成失败:模型拒绝了请求 (可能是安全策略)");
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new Error("生成失败:未返回图像数据");
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error("Item generation failed", error);
|
||
|
|
throw new Error(error.message || "道具生成失败,请重试");
|
||
|
|
}
|
||
|
|
};
|