slot_workflow.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. """Slot game workflow helpers.
  2. This module turns a small set of form choices into the manifest format that the
  3. existing generation pipeline already understands. It is deterministic on
  4. purpose: the text-model step can be added later, but the UI should already have a
  5. stable "define game -> manifest -> generate" path.
  6. """
  7. import copy
  8. import re
  9. import slot_math
  10. DEFAULT_STYLE_BY_THEME = {
  11. "jelly": "cute 3D-rendered translucent jelly creatures, glossy gummy texture, soft subsurface highlights, warm candy-land color palette, mobile slot game asset, centered, clean edges",
  12. "fruit": "bright juicy fruit slot game, glossy 3D fruit icons, fresh arcade colors, mobile game asset, centered, clean edges",
  13. "egypt": "golden ancient egypt treasure slot game, polished gems, warm sandstone and turquoise palette, premium mobile game asset, centered, clean edges",
  14. "pirate": "playful pirate treasure slot game, glossy gold coins, tropical ocean colors, stylized 3D mobile game asset, centered, clean edges",
  15. "pirate_jelly": "cute 3D pirate candy slot game, translucent jelly pirate mascot, glossy gummy texture, tropical island ocean background, wooden pirate UI frame, treasure chest, bright gold coins, colorful candy letters, high-saturation casual mobile casino art, soft rounded shapes, polished 3D render, clean mobile game asset, centered, clean edges",
  16. "cyber": "neon cyber arcade slot game, glowing glass panels, electric blue and magenta palette, premium mobile game asset, centered, clean edges",
  17. }
  18. THEME_LABELS = {
  19. "jelly": "果冻糖果",
  20. "fruit": "水果乐园",
  21. "egypt": "埃及宝藏",
  22. "pirate": "海盗金币",
  23. "pirate_jelly": "海盗糖果",
  24. "cyber": "霓虹赛博",
  25. }
  26. VOLATILITY_RULES = {
  27. "low": {
  28. "hitFrequencyFeel": "high",
  29. "minMatch": 3,
  30. "payMultiplier": 0.82,
  31. "bonusChanceLabel": "frequent_small",
  32. "cascadeMultiplierStep": 1,
  33. "maxCascadeMultiplier": 4,
  34. },
  35. "medium": {
  36. "hitFrequencyFeel": "medium",
  37. "minMatch": 4,
  38. "payMultiplier": 1.0,
  39. "bonusChanceLabel": "balanced",
  40. "cascadeMultiplierStep": 1,
  41. "maxCascadeMultiplier": 6,
  42. },
  43. "high": {
  44. "hitFrequencyFeel": "low",
  45. "minMatch": 5,
  46. "payMultiplier": 1.35,
  47. "bonusChanceLabel": "rare_large",
  48. "cascadeMultiplierStep": 2,
  49. "maxCascadeMultiplier": 10,
  50. },
  51. }
  52. FEEDBACK_RULES = {
  53. "quiet": {
  54. "spinDurationSec": 0.32,
  55. "dropTimeSec": 0.24,
  56. "clearPopScale": 1.18,
  57. "screenShake": False,
  58. "bigWinThresholdBet": 18,
  59. "vfxDensity": "light",
  60. },
  61. "standard": {
  62. "spinDurationSec": 0.45,
  63. "dropTimeSec": 0.30,
  64. "clearPopScale": 1.30,
  65. "screenShake": False,
  66. "bigWinThresholdBet": 25,
  67. "vfxDensity": "medium",
  68. },
  69. "loud": {
  70. "spinDurationSec": 0.58,
  71. "dropTimeSec": 0.36,
  72. "clearPopScale": 1.42,
  73. "screenShake": True,
  74. "bigWinThresholdBet": 35,
  75. "vfxDensity": "heavy",
  76. },
  77. }
  78. BASE_SYMBOLS = {
  79. "jelly": [
  80. ("jelly_blue", "a round blue blueberry jelly creature mascot with big friendly eyes and a happy open smile, tiny arms raised"),
  81. ("jelly_pink", "a pink strawberry jelly creature with sparkling eyes and a small leaf on top, cheerful pose"),
  82. ("jelly_green", "a green apple jelly creature with rosy cheeks and a cute grin"),
  83. ("jelly_orange", "an orange citrus jelly creature with bright eyes and little stubby hands"),
  84. ("jelly_purple", "a purple grape jelly creature, slightly wobbly, shy smile, sparkles"),
  85. ("jelly_lemon", "a yellow lemon jelly creature with a wide excited smile and star-shaped eyes"),
  86. ("jelly_choco", "a glossy chocolate jelly creature with caramel swirls and warm happy eyes"),
  87. ("jelly_rainbow", "a rainbow gradient jelly creature, extra glossy and translucent, joyful expression, premium special character"),
  88. ("symbol_coin", "a shiny golden coin symbol with a smiling jelly face embossed, glossy game icon"),
  89. ("symbol_seven", "a glossy candy-styled lucky number seven symbol, red and gold, jelly texture, game slot icon"),
  90. ],
  91. "fruit": [
  92. ("symbol_cherry", "a glossy cherry pair slot symbol with cute eyes, premium 3D mobile game icon"),
  93. ("symbol_lemon", "a bright lemon slice slot symbol, glossy and juicy, cute arcade style"),
  94. ("symbol_grape", "a purple grape cluster slot symbol, shiny 3D fruit icon"),
  95. ("symbol_watermelon", "a watermelon wedge slot symbol with juicy shine and clean edges"),
  96. ("symbol_orange", "an orange citrus slot symbol, glossy mobile game icon"),
  97. ("symbol_bell", "a golden bell slot symbol with fruit stickers and soft glow"),
  98. ("symbol_coin", "a shiny golden coin fruit slot symbol"),
  99. ("symbol_seven", "a red lucky seven fruit slot symbol with gold trim"),
  100. ],
  101. "pirate": [
  102. ("symbol_treasure_chest", "a glossy pirate treasure chest overflowing with bright gold coins, premium mobile slot icon"),
  103. ("symbol_pirate_hat", "a black pirate captain hat with gold trim and skull emblem, polished 3D slot symbol"),
  104. ("symbol_parrot", "a cute colorful pirate parrot, bright tropical mobile slot symbol, clean edges"),
  105. ("symbol_compass", "a golden pirate compass with blue glass face, polished 3D slot icon"),
  106. ("symbol_map_scatter", "a rolled treasure map with red X mark, parchment and gold trim, glossy slot symbol, no text"),
  107. ("symbol_anchor", "a shiny gold anchor wrapped with rope, pirate treasure slot icon"),
  108. ("symbol_ship", "a cute miniature pirate ship with black sails on blue waves, polished 3D icon"),
  109. ("symbol_gem", "a bright blue jewel gemstone on gold coins, premium pirate slot icon"),
  110. ("symbol_cannon", "a cute stylized pirate cannon with gold details, mobile slot symbol"),
  111. ("symbol_coin", "a shiny golden pirate coin with skull emboss, glossy slot icon"),
  112. ],
  113. "pirate_jelly": [
  114. ("jelly_pirate_red", "a red translucent jelly pirate mascot with a black skull pirate hat, winking, glossy gummy texture, cute mobile slot symbol"),
  115. ("jelly_pirate_pink", "a pink jelly candy pirate mascot holding a tiny gold coin, cheerful face, glossy 3D gummy texture"),
  116. ("symbol_treasure_chest", "a purple and gold pirate treasure chest overflowing with bright gold coins, glossy 3D mobile slot icon"),
  117. ("symbol_coin_multiplier", "a glowing golden coin multiplier badge reading 20x style, no small text, premium hold and win coin symbol"),
  118. ("symbol_pirate_hat", "a black pirate hat with skull emblem, candy glossy trim, cute mobile game slot icon"),
  119. ("symbol_parrot", "a colorful cute pirate parrot, glossy 3D mobile slot symbol, clean edges"),
  120. ("symbol_compass", "a golden pirate compass with blue glass, polished 3D icon"),
  121. ("symbol_map_scatter", "a rolled treasure map scatter symbol with red X, glossy parchment and gold trim, no tiny text"),
  122. ("symbol_anchor", "a candy-colored pirate anchor with gold rope, glossy 3D icon"),
  123. ("symbol_ship", "a cute miniature pirate ship on blue ocean waves, polished mobile slot icon"),
  124. ],
  125. }
  126. UI_ART = [
  127. ("cover", False, "1024x1536", "vertical mobile game cover key art, title-safe top area, hero mascot and theme world, premium app store style, no small text"),
  128. ("loading_screen", False, "1024x1536", "vertical mobile game loading screen background, theme world, clear center area for progress bar, no characters, no text"),
  129. ("bg_main", False, "1024x1536", "vertical mobile slot game background, clear top area for logo, central reel area, bright themed world, no characters, no text"),
  130. ("logo", True, "1536x1024", "actual readable mobile slot game title logo text, big bubble letters spelling the game title, thick gold outline, glossy themed fill, sparkles around letters, no empty frame, no badge without text, transparent background"),
  131. ("reel_frame", True, "1024x1024", "rounded rectangle slot machine reel frame, glowing border, hollow transparent center, clean mobile game UI element"),
  132. ("btn_spin", True, "1024x1024", "large round glossy spin button with circular arrows icon, premium 3D mobile game button, transparent background"),
  133. ("btn_round", True, "1024x1024", "small round glossy secondary mobile game UI button, blank center, transparent background"),
  134. ("btn_minus", True, "1024x1024", "small glossy circular minus button with a clear minus icon, mobile game UI, transparent background"),
  135. ("btn_plus", True, "1024x1024", "small glossy circular plus button with a clear plus icon, mobile game UI, transparent background"),
  136. ("btn_turbo", True, "1024x1024", "small glossy turbo lightning button icon, mobile slot game UI, transparent background"),
  137. ("btn_auto", True, "1024x1024", "small glossy auto spin button icon with circular arrow, mobile slot game UI, transparent background"),
  138. ("hud_pill", True, "1024x1024", "horizontal rounded pill shaped mobile game HUD panel, glossy dark translucent material, blank, transparent background"),
  139. ("coin", True, "1024x1024", "single shiny golden coin particle sprite, round clean edges, mobile game VFX asset, transparent background"),
  140. ("menu_panel", True, "1024x1024", "large rounded mobile game menu panel frame, glossy themed material, blank center, transparent background"),
  141. ("settings_panel", True, "1024x1024", "mobile game settings popup panel frame, glossy themed material, blank center, transparent background"),
  142. ("paytable_panel", True, "1024x1024", "mobile slot paytable popup panel frame, glossy themed material, blank center, transparent background"),
  143. ("win_popup", True, "1024x1024", "big win popup frame, glossy gold and candy highlights, blank center, transparent background"),
  144. ("free_spin_badge", True, "1024x1024", "free spins bonus badge, glossy premium mobile slot UI, blank center, transparent background"),
  145. ]
  146. def slugify(value):
  147. value = (value or "slot-game").strip().lower()
  148. value = re.sub(r"[^a-z0-9]+", "-", value).strip("-")
  149. return value or "slot-game"
  150. def clamp_int(value, default, lo, hi):
  151. try:
  152. value = int(value)
  153. except (TypeError, ValueError):
  154. value = default
  155. return max(lo, min(hi, value))
  156. def build_symbol_rules(symbols):
  157. count = max(1, len(symbols))
  158. rules = []
  159. for index, (sid, _prompt) in enumerate(symbols):
  160. if sid == "wild" or sid.startswith("wild"):
  161. role = "wild"
  162. tier = "special"
  163. weight = 3
  164. elif sid == "scatter" or "scatter" in sid:
  165. role = "scatter"
  166. tier = "special"
  167. weight = 2
  168. elif sid in ("coin_cash", "collect") or "coin" in sid:
  169. role = "bonus"
  170. tier = "bonus"
  171. weight = 3
  172. else:
  173. role = "regular"
  174. pct = index / count
  175. if pct < 0.2:
  176. tier = "premium"
  177. elif pct < 0.55:
  178. tier = "mid"
  179. else:
  180. tier = "low"
  181. weight = max(6, 22 - index)
  182. rules.append({"id": sid, "role": role, "tier": tier, "weight": weight})
  183. return rules
  184. def build_paytable(symbol_rules, volatility):
  185. vol = VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])
  186. mult = vol["payMultiplier"]
  187. tier_base = {"premium": 10, "mid": 6, "low": 3, "bonus": 2, "special": 0}
  188. paytable = {}
  189. for item in symbol_rules:
  190. base = tier_base.get(item["tier"], 3)
  191. if item["role"] == "wild":
  192. paytable[item["id"]] = {"3": 0, "4": 0, "5": 0, "6": 0, "note": "substitutes_regular_symbols"}
  193. elif item["role"] == "scatter":
  194. paytable[item["id"]] = {"3": 4, "4": 12, "5": 40, "6": 100, "note": "pays_anywhere_and_triggers_free_spins"}
  195. else:
  196. paytable[item["id"]] = {
  197. "3": round(base * mult, 2),
  198. "4": round(base * 2.4 * mult, 2),
  199. "5": round(base * 6.0 * mult, 2),
  200. "6": round(base * 14.0 * mult, 2),
  201. }
  202. return paytable
  203. def clamp_float(value, default, lo, hi):
  204. try:
  205. value = float(value)
  206. except (TypeError, ValueError):
  207. value = default
  208. if value > 1:
  209. value = value / 100.0
  210. return max(lo, min(hi, value))
  211. def build_slot_config(data):
  212. theme = data.get("theme", "jelly")
  213. reel_mode = data.get("reelMode", "ways")
  214. volatility = data.get("volatility", "medium")
  215. default_features = ["cascades", "free_spins", "wilds"]
  216. if theme == "pirate_jelly":
  217. default_features = ["cascades", "free_spins", "wilds", "hold_win", "multipliers"]
  218. feature_names = set(data.get("features") or default_features)
  219. columns = 6 if reel_mode == "megaways" else 5
  220. rows = 5 if reel_mode == "cluster" else 3
  221. return {
  222. "schemaVersion": "slot_game_config.v1",
  223. "creative": data.get("creative", {}),
  224. "gameDesign": data.get("gameDesign", {}),
  225. "game": {
  226. "id": slugify(data.get("gameId") or data.get("title") or "jelly-candy-slot"),
  227. "title": data.get("title") or "Jelly Candy Slot",
  228. "mode": "demo_only",
  229. "engine": "cocos_creator_3_8",
  230. "orientation": "portrait",
  231. "targetViewport": {"width": 798, "height": 1724},
  232. },
  233. "theme": {
  234. "key": theme,
  235. "world": THEME_LABELS.get(theme, theme),
  236. "visualStyle": data.get("style") or DEFAULT_STYLE_BY_THEME.get(theme, DEFAULT_STYLE_BY_THEME["jelly"]),
  237. },
  238. "reels": {
  239. "mode": reel_mode,
  240. "columns": columns,
  241. "rows": rows,
  242. },
  243. "layout": {
  244. "viewport": {"width": 798, "height": 1724, "orientation": "portrait"},
  245. "logo": {"cy": 0.41, "maxW": 0.82, "maxH": 0.15},
  246. "reel": {
  247. "cy": 0.02,
  248. "h": 0.60,
  249. "aspect": 0.652,
  250. "holeW": 0.86,
  251. "holeH": 0.92,
  252. "cols": columns,
  253. "rows": rows,
  254. },
  255. "hud": {"cy": -0.33, "pillH": 0.40},
  256. "controls": {"cy": -0.405, "spinR": 0.13, "smallR": 0.075, "xMinus": 0.24, "xTurbo": 0.40},
  257. "symbols": {
  258. "fitMode": "contain",
  259. "targetCellFill": 0.92,
  260. "defaultScalePerCell": 0.00093,
  261. "defaultOriginYOffsetPerCell": 0.5,
  262. },
  263. },
  264. "mathProfile": {
  265. "volatility": volatility,
  266. "hitFrequencyFeel": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["hitFrequencyFeel"],
  267. "rtpTarget": clamp_float(data.get("targetRtp"), 0.96, 0.5, 0.995),
  268. "rtpTargetLabel": "math_model_candidate" if data.get("enableMathModel", True) else "configurable_demo_no_math_model",
  269. "enableMathModel": bool(data.get("enableMathModel", True)),
  270. "bonusChanceLabel": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["bonusChanceLabel"],
  271. },
  272. "economy": {
  273. "startingBalance": clamp_int(data.get("startingBalance"), 5000, 100, 1000000),
  274. "defaultBet": clamp_int(data.get("defaultBet"), 50, 1, 100000),
  275. "minBet": 10,
  276. "maxBet": 1000,
  277. "betSteps": [10, 20, 50, 100, 200, 500, 1000],
  278. },
  279. "winRules": {
  280. "evaluation": "cluster_count" if reel_mode == "cluster" else ("left_to_right_paylines" if reel_mode == "paylines" else "adjacent_ways_demo"),
  281. "minMatch": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["minMatch"],
  282. "paylines": 20 if reel_mode == "paylines" else 0,
  283. "ways": columns ** rows if reel_mode in ("ways", "megaways") else 0,
  284. },
  285. "features": {
  286. "wilds": {"enabled": "wilds" in feature_names, "variant": data.get("wildVariant", "expanding"), "substitutes": "regular"},
  287. "scatterFreeSpins": {
  288. "enabled": "free_spins" in feature_names,
  289. "triggerCount": 3,
  290. "awardSpins": clamp_int(data.get("freeSpinCount"), 8, 3, 30),
  291. "retriggers": True,
  292. },
  293. "cascades": {
  294. "enabled": "cascades" in feature_names,
  295. "maxCascades": clamp_int(data.get("maxCascades"), 6, 1, 20),
  296. "multiplierStep": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["cascadeMultiplierStep"],
  297. "maxMultiplier": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["maxCascadeMultiplier"],
  298. },
  299. "holdAndWin": {"enabled": "hold_win" in feature_names, "triggerCount": 6, "respins": 3, "jackpots": ["mini", "minor", "major"]},
  300. "multipliers": {"enabled": "multipliers" in feature_names},
  301. },
  302. "boss": {
  303. "enabled": bool(data.get("enableBoss", True)),
  304. "id": data.get("bossId") or "boss_demon_lord",
  305. "title": data.get("bossTitle") or "大魔王关主",
  306. "presence": data.get("bossPresence") or "full",
  307. "idleLoop": ["idle", "watch", "charge"],
  308. "winReaction": "boss_cracks_open_and_explodes_when_player_wins",
  309. "loseReaction": "boss_raises_greatsword_and_taunts_after_player_loses",
  310. "playerWinSequence": ["coin_throw", "hurt", "explode"],
  311. "playerLoseSequence": ["taunt", "stomp", "attack"],
  312. },
  313. "feedback": FEEDBACK_RULES.get(data.get("feedbackIntensity", "standard"), FEEDBACK_RULES["standard"]),
  314. "assetGeneration": {
  315. "characterCount": int(data.get("characterCount") or 10),
  316. "uiCompleteness": data.get("uiCompleteness", "full"),
  317. "feedbackIntensity": data.get("feedbackIntensity", "standard"),
  318. },
  319. }
  320. def build_manifest(slot_config):
  321. slot_config = copy.deepcopy(slot_config)
  322. theme = slot_config["theme"]["key"]
  323. style = slot_config["theme"]["visualStyle"]
  324. count = slot_config["assetGeneration"]["characterCount"]
  325. base_symbols = copy.deepcopy(BASE_SYMBOLS.get(theme, BASE_SYMBOLS["jelly"]))
  326. special_symbols = []
  327. if slot_config["features"]["wilds"]["enabled"]:
  328. special_symbols.append(("wild", "a glossy rainbow WILD slot symbol icon, premium mobile game asset, no small text"))
  329. if slot_config["features"]["scatterFreeSpins"]["enabled"]:
  330. special_symbols.append(("scatter", "a glowing scatter bonus candy star symbol, premium mobile slot game icon, no small text"))
  331. if slot_config["features"]["holdAndWin"]["enabled"]:
  332. special_symbols.extend([
  333. ("coin_cash", "a shiny golden cash coin slot symbol with soft glow, premium mobile game icon"),
  334. ("collect", "a glossy collect symbol with magnet-like gold energy, premium mobile slot game icon"),
  335. ])
  336. regular_count = max(1, count - len(special_symbols))
  337. symbols = base_symbols[:regular_count] + special_symbols
  338. symbol_rules = build_symbol_rules(symbols)
  339. slot_config["symbols"] = symbol_rules
  340. slot_config["paytable"] = build_paytable(symbol_rules, slot_config["mathProfile"]["volatility"])
  341. if slot_config.get("mathProfile", {}).get("enableMathModel", True):
  342. slot_config["mathModel"] = slot_math.build_math_model(slot_config)
  343. else:
  344. slot_config["mathModel"] = {
  345. "status": "disabled_by_user",
  346. "notes": ["Math model generation was disabled for this configurable demo."],
  347. }
  348. vfx = [
  349. {"id": "win_burst", "type": "particle", "template": "burst", "color": [255, 180, 80]},
  350. ]
  351. if slot_config["features"]["cascades"]["enabled"]:
  352. vfx.append({"id": "confetti_pop", "type": "particle", "template": "confetti", "color": [255, 120, 200]})
  353. if slot_config["features"]["scatterFreeSpins"]["enabled"] or slot_config["features"]["holdAndWin"]["enabled"]:
  354. vfx.append({"id": "bigwin_glow", "type": "particle", "template": "glow", "color": [255, 240, 160]})
  355. if slot_config["features"]["holdAndWin"]["enabled"] or slot_config["assetGeneration"]["feedbackIntensity"] == "loud":
  356. vfx.append({"id": "coin_rain", "type": "particle", "template": "rain", "color": [255, 215, 0]})
  357. if slot_config.get("boss", {}).get("enabled", True):
  358. vfx.append({"id": "boss_explosion", "type": "particle", "template": "burst", "color": [190, 80, 255]})
  359. ui = [
  360. {"id": "spin_btn_press", "type": "tween", "preset": "scale_bounce"},
  361. {"id": "reward_popup_in", "type": "tween", "preset": "elastic_in"},
  362. {"id": "panel_slide_in", "type": "tween", "preset": "fade_slide_in", "params": {"dy": 60}},
  363. {"id": "balance_roll", "type": "tween", "preset": "number_roll", "params": {"from": 0, "to": 8888, "dur": 0.9}},
  364. {"id": "win_icon_pulse", "type": "tween", "preset": "pulse"},
  365. ]
  366. ui_art_ids = {"basic": 6, "full": len(UI_ART)}
  367. ui_art_count = ui_art_ids.get(slot_config["assetGeneration"]["uiCompleteness"], len(UI_ART))
  368. characters = [
  369. {"id": sid, "type": "spine", "animations": ["idle", "win"], "prompt": prompt}
  370. for sid, prompt in symbols
  371. ]
  372. if slot_config.get("boss", {}).get("enabled", True):
  373. boss_id = slot_config["boss"]["id"]
  374. characters.append({
  375. "id": boss_id,
  376. "type": "spine_parts",
  377. "role": "boss",
  378. "animations": ["idle", "watch", "charge", "coin_throw", "taunt", "stomp", "attack", "hurt", "explode"],
  379. "prompt": (
  380. "a cute stylized mobile game demon lord boss character, full body, oversized dark armor, "
  381. "horned crown, glowing eyes, holding a huge greatsword, triumphant villain pose, one boot "
  382. "planted forward on a simple defeated hero silhouette, no gore, no blood, clean mobile slot "
  383. "game boss asset, transparent background"
  384. ),
  385. "parts": [
  386. {"id": "torso", "parent": "root", "x": 0, "y": 180, "prompt": "torso armor chest of a cute demon lord boss, cracked dark armor, front view"},
  387. {"id": "head", "parent": "torso", "x": 0, "y": 260, "prompt": "horned demon lord head with glowing eyes and crown, cute villain mobile game style"},
  388. {"id": "left_arm", "parent": "torso", "x": -170, "y": 160, "prompt": "left armored arm of demon lord boss with clawed gauntlet"},
  389. {"id": "right_arm", "parent": "torso", "x": 170, "y": 170, "prompt": "right armored arm of demon lord boss gripping a sword handle"},
  390. {"id": "greatsword", "parent": "right_arm", "x": 160, "y": 170, "prompt": "oversized dark fantasy greatsword, mobile game boss weapon, clean icon asset"},
  391. {"id": "left_leg", "parent": "torso", "x": -85, "y": -80, "prompt": "left armored boot leg of demon lord boss, heavy boot"},
  392. {"id": "right_leg", "parent": "torso", "x": 90, "y": -80, "prompt": "right armored boot leg of demon lord boss stepping forward"},
  393. {"id": "cape", "parent": "torso", "x": -40, "y": 120, "prompt": "torn purple villain cape piece for demon lord boss"},
  394. {"id": "horn_left", "parent": "head", "x": -70, "y": 70, "prompt": "left curved horn of demon lord boss"},
  395. {"id": "horn_right", "parent": "head", "x": 70, "y": 70, "prompt": "right curved horn of demon lord boss"},
  396. {"id": "crack_core", "parent": "torso", "x": 0, "y": 160, "prompt": "glowing purple gold magical crack core explosion light for boss armor"},
  397. {"id": "coin_bag", "parent": "left_arm", "x": -75, "y": -45, "prompt": "small purple villain coin bag overflowing with shiny gold coins, mobile game boss prop"},
  398. {"id": "coin_splash", "parent": "torso", "x": 0, "y": 220, "prompt": "arc of shiny gold coins flying outward, transparent background, clean mobile game VFX prop"},
  399. {"id": "defeated_hero_shadow", "parent": "root", "x": 95, "y": -185, "prompt": "tiny simple defeated hero silhouette shadow under a demon boss boot, comedic mobile game style, no gore, no blood"},
  400. ],
  401. })
  402. return {
  403. "game": slot_config["game"]["id"],
  404. "style": style,
  405. "slot_config": slot_config,
  406. "characters": characters,
  407. "ui_art": [
  408. {"id": aid, "transparent": transparent, "size": size, "prompt": prompt}
  409. for aid, transparent, size, prompt in UI_ART[:ui_art_count]
  410. ],
  411. "vfx": vfx,
  412. "ui": ui,
  413. }
  414. def complete_manifest(manifest):
  415. """Fill missing generated sections before running the image pipeline.
  416. Older or hand-edited manifests may only contain characters. When a
  417. slot_config exists, it is the source of truth for the complete asset plan.
  418. """
  419. manifest = copy.deepcopy(manifest or {})
  420. if manifest.get("slot_config"):
  421. full = build_manifest(manifest["slot_config"])
  422. for key in ("game", "style", "characters", "ui_art", "vfx", "ui"):
  423. manifest[key] = full.get(key)
  424. manifest["slot_config"] = full.get("slot_config", manifest["slot_config"])
  425. return manifest
  426. if not manifest.get("ui_art"):
  427. manifest["ui_art"] = [
  428. {"id": aid, "transparent": transparent, "size": size, "prompt": prompt}
  429. for aid, transparent, size, prompt in UI_ART
  430. ]
  431. manifest.setdefault("vfx", [])
  432. manifest.setdefault("ui", [])
  433. return manifest
  434. def build_workflow(data):
  435. slot_config = build_slot_config(data or {})
  436. manifest = build_manifest(slot_config)
  437. return {"slot_config": manifest["slot_config"], "manifest": manifest}