magicciv/tools/sprite-generation/engine/prompts.py
2026-03-26 01:06:57 -07:00

585 lines
24 KiB
Python

"""Prompt templates and composition logic for sprite generation via Stable Diffusion.
All data is module-level constants. All functions are pure — no side effects, no file I/O.
Target model: juggernaut-xl-v9 (SDXL).
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# Style prefixes by category
# ---------------------------------------------------------------------------
STYLE_PREFIXES: dict[str, str] = {
"terrain": (
"top-down bird's-eye view looking straight down at the ground, "
"seamless tileable terrain texture for a hex-grid strategy game, "
"hand-painted fantasy art style like Civilization V or Heroes of Might and Magic, "
"NO perspective, NO horizon, NO sky, NO buildings, NO characters, "
"flat overhead satellite view of natural ground surface, "
"masterpiece, best quality, game asset"
),
"biome_grid": (
"top-down bird's-eye view looking straight down at the ground, "
"seamless tileable terrain texture for a hex-grid strategy game, "
"hand-painted fantasy art style like Civilization V or Heroes of Might and Magic, "
"NO perspective, NO horizon, NO sky, NO buildings, NO characters, "
"flat overhead satellite view of natural ground surface, "
"masterpiece, best quality, game asset"
),
"units": (
"isometric game character sprite on transparent background, "
"camera looking down from above at 60 degree angle, "
"you see the TOP OF THE HEAD and shoulders from above, "
"character appears short and foreshortened from the high camera, "
"like an Age of Empires II unit or Diablo II character sprite, "
"single small figure, full body visible, painted digital art, "
"clean edges ready to layer over terrain, "
"NO front-facing portrait, NO eye contact with camera, "
"transparent PNG background, masterpiece, best quality"
),
"buildings": (
"isometric building on transparent background, "
"camera looking down from above at 45-degree angle, "
"you can see the ROOF and TWO WALLS of the building, "
"NOT front-facing, NOT a facade, camera is elevated looking down, "
"single small building, painted digital art, "
"like Age of Empires II or Civilization V building, "
"clean edges ready to layer over terrain, "
"transparent PNG background, masterpiece, best quality"
),
"resources": (
"top-down overhead view of a natural resource deposit on the ground, "
"terrain overlay sprite for a hex-grid strategy game, "
"the resource is visible as a natural feature embedded in the earth, "
"like Civilization V resource icons visible on hex tiles when revealed, "
"painted fantasy game art, transparent background around the deposit, "
"small concentrated feature, NOT a full terrain texture, "
"masterpiece, best quality, game map overlay sprite"
),
"improvements": (
"single tile improvement on transparent background, "
"isometric view from above, small area of cultivated or developed land, "
"ONE simple feature like a small farm field or mine entrance, "
"like a Civilization V tile improvement overlaid on terrain, "
"painted fantasy game art, clean simple composition, "
"NOT a complex scene with multiple buildings, just ONE improvement, "
"transparent PNG background, masterpiece, best quality"
),
"spells": (
"magical spell effect icon on dark background, "
"abstract magical energy as the subject NOT a character, "
"glowing energy, particles, runes, or elemental force, "
"like a spell icon from Diablo or World of Warcraft ability bar, "
"circular or square icon composition, centered, "
"NO person, NO character, NO angel, just pure magical energy, "
"vivid colors on black background, masterpiece, best quality"
),
"edges": (
"seamless terrain edge transition, top-down bird's-eye view, "
"gradient blend between two terrain types for hex-grid tile edges, "
"painted fantasy art style, NO perspective, NO horizon, "
"masterpiece, best quality, game asset"
),
"ui": (
"clean fantasy game UI icon, simple flat design with subtle depth, "
"single symbolic icon centered on transparent background, "
"bold readable silhouette, like a Civilization V yield or tech icon, "
"masterpiece, best quality, game UI element"
),
}
# ---------------------------------------------------------------------------
# Negative prompts by category
# ---------------------------------------------------------------------------
NEGATIVES: dict[str, str] = {
"terrain": (
"text, watermark, blurry, low quality, anime, 3d render, photo, "
"perspective view, horizon, sky, clouds, sun, moon, "
"person, character, creature, animal, building, structure, house, "
"hexagon pattern, geometric shape, abstract, honeycomb, border, frame, UI"
),
"biome_grid": (
"text, watermark, blurry, low quality, anime, 3d render, photo, "
"perspective view, horizon, sky, clouds, sun, moon, "
"person, character, creature, animal, building, structure, house, "
"hexagon pattern, geometric shape, abstract, honeycomb, border, frame, UI"
),
"units": (
"text, watermark, blurry, low quality, anime, photo, "
"white background, solid background, background scenery, terrain, landscape, sky, horizon, "
"front-facing, looking at camera, portrait, headshot, "
"multiple characters, crowd, group, army, "
"hexagon, geometric, border, frame, UI, card frame"
),
"buildings": (
"text, watermark, blurry, low quality, anime, photo, "
"white background, solid background, "
"front elevation, straight-on view, architectural drawing, blueprint, "
"terrain, landscape, sky, horizon, person, character, "
"multiple buildings, city, town, village, street, "
"hexagon, geometric, abstract, border, frame, UI"
),
"resources": (
"text, watermark, blurry, low quality, anime, photo, "
"dark background, black background, solid background, border, frame, "
"texture, pattern, seamless, tileable, "
"person, character, building, landscape, horizon, sky, "
"multiple objects, collage, grid, collection, "
"hexagon, geometric, abstract, UI"
),
"improvements": (
"text, watermark, blurry, low quality, anime, photo, "
"white background, "
"perspective view, horizon, sky, person, character, "
"multiple buildings, city, village, complex scene, "
"hexagon, geometric, abstract, border, frame, UI"
),
"spells": (
"text, watermark, blurry, low quality, anime, photo, "
"person, character, human, angel, figure, face, body, wings, "
"terrain, landscape, building, scenery, "
"hexagon, geometric, border, frame, UI, card frame"
),
"edges": (
"text, watermark, blurry, low quality, anime, 3d render, photo, "
"perspective view, horizon, sky, person, building, creature, "
"hexagon pattern, geometric shape, abstract, border, frame, UI"
),
"ui": (
"text, watermark, blurry, low quality, 3d render, photo, anime, "
"landscape, terrain, character, building, realistic, "
"border, frame, busy background"
),
}
# ---------------------------------------------------------------------------
# Dimension modifier prompts
# ---------------------------------------------------------------------------
RACE_AESTHETICS: dict[str, str] = {
"high_elves": "elegant elven architecture, pointed crystalline spires, silver-blue and white palette, graceful organic curves, luminous materials",
"humans": "medieval human architecture, practical sturdy design, heraldic banners, warm stone and timber, copper and gold accents",
"dwarves": "dwarven stonework, massive carved rock, rune-inscribed walls, copper and bronze fittings, underground fortress aesthetic",
"orcs": "orcish construction, bone and leather decorations, tribal totems, dark iron spikes, rough-hewn wood and stone",
}
GENDER_MODIFIERS: dict[str, str] = {
"male": "male, masculine build",
"female": "female, feminine build",
}
QUALITY_MODIFIERS: dict[str, dict[int, str]] = {
"terrain": {
1: "sparse nascent, recently formed, thin patchy coverage, young ecosystem",
2: "developing, growing, partial coverage, maturing",
3: "established standard, typical healthy, mature ecosystem",
4: "flourishing, dense rich coverage, thriving biodiversity",
5: "ancient peak ecosystem, pristine old-growth, dense lush primordial",
},
"units": {
1: "raw recruit, basic equipment, worn leather, simple weapons",
2: "trained soldier, decent equipment, serviceable armor",
3: "veteran warrior, quality equipment, well-maintained gear, battle-scarred",
4: "elite champion, masterwork equipment, ornate armor, commanding presence",
5: "legendary hero, legendary equipment, radiant armor, aura of power",
},
"buildings": {
1: "crude construction, basic materials, partially built, scaffolding visible",
2: "functional construction, standard materials, well-maintained",
3: "well-built, quality stonework, mature, established, decorated",
4: "grand construction, fine materials, ornate details, impressive",
5: "magnificent masterwork, legendary craftsmanship, monumental, glowing with power",
},
"resources": {
1: "sparse small deposit, barely visible, traces",
2: "modest deposit, visible but limited",
3: "standard healthy deposit, clearly visible, moderate abundance",
4: "rich abundant deposit, impressive quantity, gleaming",
5: "legendary massive deposit, overwhelming abundance, radiant with power",
},
"spells": {
1: "faint weak magical effect, dim glow, minor energy",
2: "moderate magical effect, visible energy, growing power",
3: "strong magical effect, vivid energy, impressive display",
4: "powerful magical effect, intense radiance, devastating force",
5: "cataclysmic magical effect, blinding energy, reality-warping power, legendary",
},
"improvements": {
1: "crude basic construction, rough materials, newly placed",
2: "functional improvement, standard construction",
3: "well-developed, quality materials, efficient, established",
},
}
SCHOOL_AESTHETICS: dict[str, str] = {
"life": "golden divine light, white marble, angelic motifs, holy symbols, pristine and sacred",
"death": "dark gothic stone, skulls and bones, purple-black energy, necromantic symbols, eerie green glow",
"chaos": "volcanic rock and fire, red-orange flames, demonic runes, molten metal, aggressive spiky architecture",
"nature": "living wood and vines, green leaves, druidic symbols, stone circles, natural organic forms",
"aether": "crystalline blue-white, floating arcane runes, ethereal glow, geometric precision, translucent materials",
}
COMBAT_TYPE_FLAVORS: dict[str, str] = {
"melee": "armored warrior, close combat stance, weapon drawn",
"ranged": "ranged fighter, bow or crossbow ready, quiver",
"cavalry": "mounted on horse or beast, charging pose",
"siege": "large war machine, siege engine, heavy wood and iron",
"flying": "winged creature in flight, soaring pose",
"specialist": "support character, magical or tactical equipment",
"civilian": "non-combat civilian, traveler clothes, pack and supplies",
"sea": "naval vessel, ship on water, sails and rigging",
"archon": "powerful magical caster, arcane energy swirling, ethereal presence",
}
KEYWORD_FLAVORS: dict[str, str] = {
"undead": "undead skeletal, decaying, dark necromantic energy",
"flying": "wings spread, soaring, aerial",
"fire_breath": "breathing fire, flames, volcanic",
"magic_immune": "runic armor, anti-magic wards, iron and mithril",
"holy_aura": "divine golden glow, holy radiance",
"invisible": "shadowy, semi-transparent, stealthy",
"poison": "venomous, toxic green dripping, noxious",
"regeneration": "healing glow, green restoration energy",
"first_strike": "lightning-fast, blur of motion, deadly speed",
"trample": "massive, ground-shaking, unstoppable force",
"life_drain": "dark tendrils of energy, soul-absorbing purple glow",
"wall_breaker": "massive battering force, siege-breaking power",
}
VARIANT_MODIFIERS: list[str] = [
"",
", dramatic lighting, high contrast",
", soft diffused light, painterly",
", vibrant saturated colors, sharp detail",
", warm golden-hour light",
", cool moonlit atmosphere",
", cinematic depth of field",
", bold silhouette, rim lighting",
]
# ---------------------------------------------------------------------------
# Biome grid fragment arrays
# ---------------------------------------------------------------------------
BIOME_TEMP: dict[int, str] = {
0: "frozen arctic, perpetual ice, deep winter",
1: "frigid cold, frost and bare earth, harsh winter",
2: "cool temperate, mild seasons, gentle weather",
3: "warm subtropical, rich growth, long summers",
4: "scorching tropical, intense heat, blazing sun",
}
BIOME_MOIST: dict[int, str] = {
0: "parched bone-dry arid, no water, cracked earth",
1: "dry sparse moisture, dusty, occasional rain",
2: "moderate rainfall, balanced seasons",
3: "lush wet, frequent rain, damp air",
4: "saturated waterlogged, standing water, constant rain",
}
BIOME_ELEV: dict[str, str] = {
"lowland": "flat lowland plains, gentle terrain",
"highland": "elevated rocky highland plateau, exposed stone",
"alpine": "steep alpine mountain slope, thin air, windswept",
}
BIOME_VEGETATION: dict[tuple[int, int], str] = {
(4, 0): "golden sand dunes with shadow ridges, scattered rocks, heat shimmer",
(4, 1): "sandy terrain with scattered thorny scrub bushes and rocks",
(4, 2): "golden savanna with acacia trees and tall yellow grass patches",
(4, 3): "tropical canopy from above, round broad-leaf treetops, vines",
(4, 4): "dense tangled jungle canopy, layered green leaves, vine bridges",
(3, 0): "dry steppe with dead grass clumps and cracked brown earth",
(3, 1): "sparse grassland with patches of green and brown bare earth",
(3, 2): "lush green meadow with wildflower patches and a winding dirt path",
(3, 3): "deciduous forest canopy from above, round green treetops, dappled shadows",
(3, 4): "dark dense rainforest canopy, moss and fern visible between trees",
(2, 0): "windswept grey-brown rocky ground with dead stubble and gravel",
(2, 1): "grey-green scrubland with hardy low bushes on rocky terrain",
(2, 2): "cool green meadow with short grass and morning dew droplets",
(2, 3): "conifer forest from above, triangular dark-green pine treetops",
(2, 4): "murky bog with dark water, moss patches, dead tree stumps",
(1, 0): "frozen grey wasteland, cracked permafrost, scattered frost crystals",
(1, 1): "frozen tundra with pale lichen spots on grey-blue ground",
(1, 2): "frost-covered yellowed grass on frozen brown-grey earth",
(1, 3): "frozen wetland, ice-crusted brown reed stalks in dark water",
(1, 4): "frozen black marsh, thin cracked ice sheets over dark water",
(0, 0): "white snow drifts with subtle blue shadows, wind-carved ripples",
(0, 1): "flat snow plain with wind patterns and a few exposed rocks",
(0, 2): "snow field with frozen plant tips poking through white surface",
(0, 3): "blue-white pack ice with visible pressure ridges and cracks",
(0, 4): "deep blue glacial ice surface, ancient, compressed, massive",
}
BIOME_GROUND: dict[tuple[int, int], str] = {
(4, 0): "smooth tan sand",
(4, 1): "sandy brown soil",
(4, 2): "warm reddish-brown earth",
(4, 3): "dark rich soil under leaf litter",
(4, 4): "dark mud under shallow water",
(3, 0): "hard packed light brown dirt",
(3, 1): "dusty grey-brown soil",
(3, 2): "dark brown-green earth",
(3, 3): "brown leaf litter on dark soil",
(3, 4): "dark wet peat",
(2, 0): "grey gravel and frozen dirt",
(2, 1): "grey rocky soil",
(2, 2): "dark damp earth",
(2, 3): "brown needle-covered forest floor",
(2, 4): "waterlogged dark peat",
(1, 0): "grey frozen permafrost",
(1, 1): "blue-grey frozen ground",
(1, 2): "brown frozen soil",
(1, 3): "dark frozen mud",
(1, 4): "black frozen water",
(0, 0): "white compacted snow",
(0, 1): "white wind-packed snow",
(0, 2): "snow over frozen ground",
(0, 3): "smooth blue-white ice",
(0, 4): "deep blue ice",
}
BIOME_PALETTE: dict[int, str] = {
0: "MANDATORY COLOR: white and pale blue ONLY, ice crystal silver, frozen winter, NO warm colors NO brown NO green",
1: "MANDATORY COLOR: cold grey-blue and frost-white, muted desaturated, winter palette, NO warm tones",
2: "MANDATORY COLOR: cool greens and earth browns, balanced natural colors",
3: "MANDATORY COLOR: rich warm greens and golden-yellow, lush vibrant",
4: "MANDATORY COLOR: intense hot golden-yellow, burnt orange, tropical vivid, NO blue NO cool tones",
}
# ---------------------------------------------------------------------------
# Generation and target sizes
# ---------------------------------------------------------------------------
_GENERATION_SIZES: dict[str, tuple[int, int]] = {
"terrain": (1024, 512),
"biome_grid": (1024, 512),
"edges": (832, 512),
"units": (512, 512),
"buildings": (512, 512),
"resources": (512, 512),
"improvements": (512, 512),
"spells": (512, 512),
"ui": (256, 256),
}
_TARGET_SIZES: dict[str, tuple[int, int]] = {
"terrain": (384, 332),
"biome_grid": (384, 332),
"edges": (384, 332),
"units": (256, 256),
"buildings": (128, 128),
"resources": (64, 64),
"improvements": (64, 64),
"spells": (128, 128),
"ui": (64, 64),
}
# ---------------------------------------------------------------------------
# Default quality ranges by category
# ---------------------------------------------------------------------------
_DEFAULT_QUALITY_RANGES: dict[str, tuple[int, int] | None] = {
"terrain": (1, 5),
"biome_grid": (1, 5),
"resources": (1, 5),
"spells": (1, 5),
"improvements": (1, 3),
"edges": None,
"ui": None,
}
# ---------------------------------------------------------------------------
# Functions
# ---------------------------------------------------------------------------
def compose_prompt(
category: str,
entity_data: dict,
dimensions: dict | None = None,
) -> str:
"""Build the full prompt for a sprite.
SDXL weights early tokens most heavily, so composition order is:
1. SUBJECT FIRST — what the image depicts (name + visual description)
2. Category-specific attributes (combat type, school, race, keywords)
3. Style/perspective constraints (the style prefix)
4. Quality modifier
"""
dims = dimensions or {}
parts: list[str] = []
# 1 — SUBJECT FIRST (most important tokens for SDXL)
name = entity_data.get("name", "")
description = entity_data.get("description", "")
if name:
parts.append(name)
if description:
parts.append(description[:120])
# 2 — category-specific attributes
if category == "units":
combat_type = entity_data.get("combat_type", "")
if combat_type and combat_type in COMBAT_TYPE_FLAVORS:
parts.append(COMBAT_TYPE_FLAVORS[combat_type])
keywords: list[str] = entity_data.get("keywords", [])
for kw in keywords:
flavor = KEYWORD_FLAVORS.get(kw)
if flavor:
parts.append(flavor)
# school aesthetic
school = entity_data.get("school") or dims.get("school")
if school and school in SCHOOL_AESTHETICS:
parts.append(SCHOOL_AESTHETICS[school])
# 4 — race aesthetic
race = dims.get("race")
if race and race in RACE_AESTHETICS:
parts.append(RACE_AESTHETICS[race])
# gender modifier
gender = dims.get("gender")
if gender and gender in GENDER_MODIFIERS:
parts.append(GENDER_MODIFIERS[gender])
# 3 — STYLE PREFIX (perspective + art style constraints come after subject)
prefix = STYLE_PREFIXES.get(category, "")
if prefix:
parts.append(prefix)
# 4 — quality modifier
quality = dims.get("quality")
if quality is not None:
qual_category = category
if qual_category == "biome_grid":
qual_category = "terrain"
qual_table = QUALITY_MODIFIERS.get(qual_category)
if qual_table and quality in qual_table:
parts.append(qual_table[quality])
return ", ".join(parts)
def compose_biome_prompt(
temp: int,
moist: int,
elev: str,
quality: int,
) -> str | None:
"""Build prompt for a biome grid cell.
Returns None if the combination is unrealistic (filtered by is_valid_biome).
"""
if not is_valid_biome(temp, moist, elev):
return None
parts: list[str] = []
# Style prefix
parts.append(STYLE_PREFIXES["biome_grid"])
# Vegetation
veg = BIOME_VEGETATION.get((temp, moist))
if veg:
parts.append(veg)
# Ground material
ground = BIOME_GROUND.get((temp, moist))
if ground:
parts.append(ground)
# Elevation
elev_desc = BIOME_ELEV.get(elev)
if elev_desc:
parts.append(elev_desc)
# Quality (terrain table)
qual_table = QUALITY_MODIFIERS.get("terrain")
if qual_table and quality in qual_table:
parts.append(qual_table[quality])
# Palette (mandatory color directive)
palette = BIOME_PALETTE.get(temp)
if palette:
parts.append(palette)
return ", ".join(parts)
def get_negative(category: str) -> str:
"""Get the negative prompt for a category."""
return NEGATIVES.get(category, "")
def get_variant_modifier(variant_index: int) -> str:
"""Get the style modifier for variant N (cycles through VARIANT_MODIFIERS)."""
return VARIANT_MODIFIERS[variant_index % len(VARIANT_MODIFIERS)]
def is_valid_biome(temp: int, moist: int, elev: str) -> bool:
"""Check if a biome combination is realistic.
Filter rules:
- No frozen(0) + saturated(4) + alpine
- No scorching(4) + alpine
"""
if temp == 0 and moist == 4 and elev == "alpine":
return False
if temp == 4 and elev == "alpine":
return False
return True
def get_quality_range(
category: str,
entity_data: dict | None = None,
) -> tuple[int, int] | None:
"""Get the quality range for an entity.
If entity_data has a 'quality_range' field, use that instead of defaults.
"""
if entity_data and "quality_range" in entity_data:
qr = entity_data["quality_range"]
return (qr[0], qr[1])
# Categories with fixed defaults
if category in _DEFAULT_QUALITY_RANGES:
return _DEFAULT_QUALITY_RANGES[category]
# Units — range depends on type
if category == "units":
if entity_data is None:
return (1, 3)
school = entity_data.get("school")
entity_type = entity_data.get("type", "")
if entity_type == "wild" or entity_data.get("is_wild"):
return (1, 3)
if school and school not in ("mundane", ""):
return (2, 5)
return (1, 3)
# Buildings — range depends on tier/type
if category == "buildings":
if entity_data is None:
return (1, 3)
entity_type = entity_data.get("type", "")
if entity_type in ("advanced", "wonder"):
return (3, 5)
return (1, 3)
return None
def get_generation_size(category: str) -> tuple[int, int]:
"""Get the generation resolution (width, height) for a category."""
return _GENERATION_SIZES.get(category, (512, 512))
def get_target_size(category: str) -> tuple[int, int]:
"""Get the final sprite size (width, height) for a category."""
return _TARGET_SIZES.get(category, (64, 64))