| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552 |
- """Slot game workflow helpers.
- This module turns a small set of form choices into the manifest format that the
- existing generation pipeline already understands. It is deterministic on
- purpose: the text-model step can be added later, but the UI should already have a
- stable "define game -> manifest -> generate" path.
- """
- import copy
- import re
- import slot_math
- DEFAULT_STYLE_BY_THEME = {
- "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",
- "fruit": "bright juicy fruit slot game, glossy 3D fruit icons, fresh arcade colors, mobile game asset, centered, clean edges",
- "egypt": "golden ancient egypt treasure slot game, polished gems, warm sandstone and turquoise palette, premium mobile game asset, centered, clean edges",
- "pirate": "playful pirate treasure slot game, glossy gold coins, tropical ocean colors, stylized 3D mobile game asset, centered, clean edges",
- "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",
- "cyber": "neon cyber arcade slot game, glowing glass panels, electric blue and magenta palette, premium mobile game asset, centered, clean edges",
- }
- THEME_LABELS = {
- "jelly": "果冻糖果",
- "fruit": "水果乐园",
- "egypt": "埃及宝藏",
- "pirate": "海盗金币",
- "pirate_jelly": "海盗糖果",
- "cyber": "霓虹赛博",
- }
- VOLATILITY_RULES = {
- "low": {
- "hitFrequencyFeel": "high",
- "minMatch": 3,
- "payMultiplier": 0.82,
- "bonusChanceLabel": "frequent_small",
- "cascadeMultiplierStep": 1,
- "maxCascadeMultiplier": 4,
- },
- "medium": {
- "hitFrequencyFeel": "medium",
- "minMatch": 4,
- "payMultiplier": 1.0,
- "bonusChanceLabel": "balanced",
- "cascadeMultiplierStep": 1,
- "maxCascadeMultiplier": 6,
- },
- "high": {
- "hitFrequencyFeel": "low",
- "minMatch": 5,
- "payMultiplier": 1.35,
- "bonusChanceLabel": "rare_large",
- "cascadeMultiplierStep": 2,
- "maxCascadeMultiplier": 10,
- },
- }
- FEEDBACK_RULES = {
- "quiet": {
- "spinDurationSec": 0.32,
- "dropTimeSec": 0.24,
- "clearPopScale": 1.18,
- "screenShake": False,
- "bigWinThresholdBet": 18,
- "vfxDensity": "light",
- },
- "standard": {
- "spinDurationSec": 0.45,
- "dropTimeSec": 0.30,
- "clearPopScale": 1.30,
- "screenShake": False,
- "bigWinThresholdBet": 25,
- "vfxDensity": "medium",
- },
- "loud": {
- "spinDurationSec": 0.58,
- "dropTimeSec": 0.36,
- "clearPopScale": 1.42,
- "screenShake": True,
- "bigWinThresholdBet": 35,
- "vfxDensity": "heavy",
- },
- }
- BASE_SYMBOLS = {
- "jelly": [
- ("jelly_blue", "a round blue blueberry jelly creature mascot with big friendly eyes and a happy open smile, tiny arms raised"),
- ("jelly_pink", "a pink strawberry jelly creature with sparkling eyes and a small leaf on top, cheerful pose"),
- ("jelly_green", "a green apple jelly creature with rosy cheeks and a cute grin"),
- ("jelly_orange", "an orange citrus jelly creature with bright eyes and little stubby hands"),
- ("jelly_purple", "a purple grape jelly creature, slightly wobbly, shy smile, sparkles"),
- ("jelly_lemon", "a yellow lemon jelly creature with a wide excited smile and star-shaped eyes"),
- ("jelly_choco", "a glossy chocolate jelly creature with caramel swirls and warm happy eyes"),
- ("jelly_rainbow", "a rainbow gradient jelly creature, extra glossy and translucent, joyful expression, premium special character"),
- ("symbol_coin", "a shiny golden coin symbol with a smiling jelly face embossed, glossy game icon"),
- ("symbol_seven", "a glossy candy-styled lucky number seven symbol, red and gold, jelly texture, game slot icon"),
- ],
- "fruit": [
- ("symbol_cherry", "a glossy cherry pair slot symbol with cute eyes, premium 3D mobile game icon"),
- ("symbol_lemon", "a bright lemon slice slot symbol, glossy and juicy, cute arcade style"),
- ("symbol_grape", "a purple grape cluster slot symbol, shiny 3D fruit icon"),
- ("symbol_watermelon", "a watermelon wedge slot symbol with juicy shine and clean edges"),
- ("symbol_orange", "an orange citrus slot symbol, glossy mobile game icon"),
- ("symbol_bell", "a golden bell slot symbol with fruit stickers and soft glow"),
- ("symbol_coin", "a shiny golden coin fruit slot symbol"),
- ("symbol_seven", "a red lucky seven fruit slot symbol with gold trim"),
- ],
- "pirate": [
- ("symbol_treasure_chest", "a glossy pirate treasure chest overflowing with bright gold coins, premium mobile slot icon"),
- ("symbol_pirate_hat", "a black pirate captain hat with gold trim and skull emblem, polished 3D slot symbol"),
- ("symbol_parrot", "a cute colorful pirate parrot, bright tropical mobile slot symbol, clean edges"),
- ("symbol_compass", "a golden pirate compass with blue glass face, polished 3D slot icon"),
- ("symbol_map_scatter", "a rolled treasure map with red X mark, parchment and gold trim, glossy slot symbol, no text"),
- ("symbol_anchor", "a shiny gold anchor wrapped with rope, pirate treasure slot icon"),
- ("symbol_ship", "a cute miniature pirate ship with black sails on blue waves, polished 3D icon"),
- ("symbol_gem", "a bright blue jewel gemstone on gold coins, premium pirate slot icon"),
- ("symbol_cannon", "a cute stylized pirate cannon with gold details, mobile slot symbol"),
- ("symbol_coin", "a shiny golden pirate coin with skull emboss, glossy slot icon"),
- ],
- "pirate_jelly": [
- ("jelly_pirate_red", "a red translucent jelly pirate mascot with a black skull pirate hat, winking, glossy gummy texture, cute mobile slot symbol"),
- ("jelly_pirate_pink", "a pink jelly candy pirate mascot holding a tiny gold coin, cheerful face, glossy 3D gummy texture"),
- ("symbol_treasure_chest", "a purple and gold pirate treasure chest overflowing with bright gold coins, glossy 3D mobile slot icon"),
- ("symbol_coin_multiplier", "a glowing golden coin multiplier badge reading 20x style, no small text, premium hold and win coin symbol"),
- ("symbol_pirate_hat", "a black pirate hat with skull emblem, candy glossy trim, cute mobile game slot icon"),
- ("symbol_parrot", "a colorful cute pirate parrot, glossy 3D mobile slot symbol, clean edges"),
- ("symbol_compass", "a golden pirate compass with blue glass, polished 3D icon"),
- ("symbol_map_scatter", "a rolled treasure map scatter symbol with red X, glossy parchment and gold trim, no tiny text"),
- ("symbol_anchor", "a candy-colored pirate anchor with gold rope, glossy 3D icon"),
- ("symbol_ship", "a cute miniature pirate ship on blue ocean waves, polished mobile slot icon"),
- ],
- }
- UI_ART = [
- ("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"),
- ("loading_screen", False, "1024x1536", "vertical mobile game loading screen background, theme world, clear center area for progress bar, no characters, no text"),
- ("bg_main", False, "1024x1536", "vertical mobile slot game background, clear top area for logo, central reel area, bright themed world, no characters, no text"),
- ("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"),
- ("reel_frame", True, "1024x1024", "rounded rectangle slot machine reel frame, glowing border, hollow transparent center, clean mobile game UI element"),
- ("btn_spin", True, "1024x1024", "large round glossy spin button with circular arrows icon, premium 3D mobile game button, transparent background"),
- ("btn_round", True, "1024x1024", "small round glossy secondary mobile game UI button, blank center, transparent background"),
- ("btn_minus", True, "1024x1024", "small glossy circular minus button with a clear minus icon, mobile game UI, transparent background"),
- ("btn_plus", True, "1024x1024", "small glossy circular plus button with a clear plus icon, mobile game UI, transparent background"),
- ("btn_turbo", True, "1024x1024", "small glossy turbo lightning button icon, mobile slot game UI, transparent background"),
- ("btn_auto", True, "1024x1024", "small glossy auto spin button icon with circular arrow, mobile slot game UI, transparent background"),
- ("hud_pill", True, "1024x1024", "horizontal rounded pill shaped mobile game HUD panel, glossy dark translucent material, blank, transparent background"),
- ("coin", True, "1024x1024", "single shiny golden coin particle sprite, round clean edges, mobile game VFX asset, transparent background"),
- ("menu_panel", True, "1024x1024", "large rounded mobile game menu panel frame, glossy themed material, blank center, transparent background"),
- ("settings_panel", True, "1024x1024", "mobile game settings popup panel frame, glossy themed material, blank center, transparent background"),
- ("paytable_panel", True, "1024x1024", "mobile slot paytable popup panel frame, glossy themed material, blank center, transparent background"),
- ("win_popup", True, "1024x1024", "big win popup frame, glossy gold and candy highlights, blank center, transparent background"),
- ("free_spin_badge", True, "1024x1024", "free spins bonus badge, glossy premium mobile slot UI, blank center, transparent background"),
- ]
- BOSS_BY_THEME = {
- "jelly": {
- "title": "果冻糖果关主",
- "prompt": (
- "a cute but mischievous giant translucent jelly candy boss, glossy gummy texture, fruit syrup crown, "
- "candy cane greatsword, jelly armor plates, warm candy-land colors, playful villain expression, "
- "full body mobile slot game boss asset, no horror, no dark fantasy"
- ),
- "material": "glossy translucent jelly candy",
- "weapon": "candy cane greatsword",
- "prop": "striped candy coin bag overflowing with gold candy coins",
- },
- "fruit": {
- "title": "水果乐园关主",
- "prompt": (
- "a mischievous giant fruit king boss made of glossy juicy fruit pieces, leafy crown, citrus armor, "
- "banana-shaped sword, bright arcade fruit slot palette, full body mobile game boss asset"
- ),
- "material": "glossy juicy fruit armor",
- "weapon": "banana-shaped golden sword",
- "prop": "fruit basket coin bag overflowing with gold coins and berries",
- },
- "egypt": {
- "title": "法老宝藏关主",
- "prompt": (
- "a cute stylized ancient egypt pharaoh treasure boss, polished gold and turquoise armor, scarab crown, "
- "ankh-shaped staff sword, sandstone gem palette, full body premium mobile slot boss asset"
- ),
- "material": "polished gold turquoise pharaoh armor",
- "weapon": "ankh-shaped staff sword",
- "prop": "golden egyptian treasure bag overflowing with coins and scarab gems",
- },
- "pirate": {
- "title": "海盗船长关主",
- "prompt": (
- "a playful pirate captain treasure boss, black captain coat with gold trim, oversized pirate hat, "
- "curved cutlass sword, tropical ocean colors, treasure chest details, full body mobile slot boss asset"
- ),
- "material": "pirate captain coat with gold trim",
- "weapon": "curved pirate cutlass",
- "prop": "pirate treasure coin bag overflowing with gold doubloons",
- },
- "pirate_jelly": {
- "title": "海盗糖果关主",
- "prompt": (
- "a cute translucent jelly pirate captain boss, glossy gummy red candy body, black skull pirate hat, "
- "wood and gold candy armor, curved candy cutlass, tropical treasure island colors, full body mobile slot boss asset"
- ),
- "material": "translucent gummy pirate candy armor",
- "weapon": "curved candy pirate cutlass",
- "prop": "purple pirate candy coin bag overflowing with gold coins",
- },
- "cyber": {
- "title": "霓虹机械关主",
- "prompt": (
- "a stylized neon cyber arcade boss robot, glowing glass armor panels, electric blue and magenta lights, "
- "plasma blade, playful mobile game villain silhouette, full body premium slot boss asset"
- ),
- "material": "neon glass cyber armor panels",
- "weapon": "glowing plasma blade",
- "prop": "holographic coin capsule with neon tokens",
- },
- }
- def build_boss_character(slot_config):
- theme = slot_config["theme"]["key"]
- profile = BOSS_BY_THEME.get(theme, BOSS_BY_THEME["jelly"])
- boss_id = slot_config["boss"]["id"]
- material = profile["material"]
- weapon = profile["weapon"]
- prop = profile["prop"]
- return {
- "id": boss_id,
- "type": "spine_parts",
- "role": "boss",
- "partGeneration": "sprite_sheet",
- "spriteSheet": {"enabled": True, "cols": 4, "rows": 4, "size": "1024x1024"},
- "animations": ["idle", "watch", "charge", "coin_throw", "taunt", "stomp", "attack", "hurt", "explode"],
- "prompt": (
- f"{profile['prompt']}, same visual style as {slot_config['theme']['world']}, "
- "triumphant villain pose, one foot planted forward on a simple defeated hero silhouette, "
- "no gore, no blood, transparent background"
- ),
- "parts": [
- {"id": "torso", "parent": "root", "x": 0, "y": 180, "prompt": f"torso chest piece of the theme boss, {material}, front view"},
- {"id": "head", "parent": "torso", "x": 0, "y": 260, "prompt": f"head of the theme boss with expressive villain eyes and matching crown, {material}"},
- {"id": "left_arm", "parent": "torso", "x": -170, "y": 160, "prompt": f"left arm of the theme boss, {material}, clean separated rigging part"},
- {"id": "right_arm", "parent": "torso", "x": 170, "y": 170, "prompt": f"right arm of the theme boss gripping weapon handle, {material}"},
- {"id": "greatsword", "parent": "right_arm", "x": 160, "y": 170, "prompt": f"{weapon}, clean mobile game boss weapon asset"},
- {"id": "left_leg", "parent": "torso", "x": -85, "y": -80, "prompt": f"left leg and boot of the theme boss, {material}"},
- {"id": "right_leg", "parent": "torso", "x": 90, "y": -80, "prompt": f"right leg and boot of the theme boss stepping forward, {material}"},
- {"id": "cape", "parent": "torso", "x": -40, "y": 120, "prompt": f"theme-matching cape or back cloth piece for the boss, {material}"},
- {"id": "horn_left", "parent": "head", "x": -70, "y": 70, "prompt": "left crown ornament or horn-like silhouette matching the theme boss"},
- {"id": "horn_right", "parent": "head", "x": 70, "y": 70, "prompt": "right crown ornament or horn-like silhouette matching the theme boss"},
- {"id": "crack_core", "parent": "torso", "x": 0, "y": 160, "prompt": "glowing magical crack core explosion light matching the boss theme"},
- {"id": "coin_bag", "parent": "left_arm", "x": -75, "y": -45, "prompt": prop},
- {"id": "coin_splash", "parent": "torso", "x": 0, "y": 220, "prompt": "arc of shiny gold coins flying outward, transparent background, clean mobile game VFX prop"},
- {"id": "defeated_hero_shadow", "parent": "root", "x": 95, "y": -185, "prompt": "tiny simple defeated hero silhouette shadow under a boss boot, comedic mobile game style, no gore, no blood"},
- ],
- }
- def slugify(value):
- value = (value or "slot-game").strip().lower()
- value = re.sub(r"[^a-z0-9]+", "-", value).strip("-")
- return value or "slot-game"
- def clamp_int(value, default, lo, hi):
- try:
- value = int(value)
- except (TypeError, ValueError):
- value = default
- return max(lo, min(hi, value))
- def build_symbol_rules(symbols):
- count = max(1, len(symbols))
- rules = []
- for index, (sid, _prompt) in enumerate(symbols):
- if sid == "wild" or sid.startswith("wild"):
- role = "wild"
- tier = "special"
- weight = 3
- elif sid == "scatter" or "scatter" in sid:
- role = "scatter"
- tier = "special"
- weight = 2
- elif sid in ("coin_cash", "collect") or "coin" in sid:
- role = "bonus"
- tier = "bonus"
- weight = 3
- else:
- role = "regular"
- pct = index / count
- if pct < 0.2:
- tier = "premium"
- elif pct < 0.55:
- tier = "mid"
- else:
- tier = "low"
- weight = max(6, 22 - index)
- rules.append({"id": sid, "role": role, "tier": tier, "weight": weight})
- return rules
- def build_paytable(symbol_rules, volatility):
- vol = VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])
- mult = vol["payMultiplier"]
- tier_base = {"premium": 10, "mid": 6, "low": 3, "bonus": 2, "special": 0}
- paytable = {}
- for item in symbol_rules:
- base = tier_base.get(item["tier"], 3)
- if item["role"] == "wild":
- paytable[item["id"]] = {"3": 0, "4": 0, "5": 0, "6": 0, "note": "substitutes_regular_symbols"}
- elif item["role"] == "scatter":
- paytable[item["id"]] = {"3": 4, "4": 12, "5": 40, "6": 100, "note": "pays_anywhere_and_triggers_free_spins"}
- else:
- paytable[item["id"]] = {
- "3": round(base * mult, 2),
- "4": round(base * 2.4 * mult, 2),
- "5": round(base * 6.0 * mult, 2),
- "6": round(base * 14.0 * mult, 2),
- }
- return paytable
- def clamp_float(value, default, lo, hi):
- try:
- value = float(value)
- except (TypeError, ValueError):
- value = default
- if value > 1:
- value = value / 100.0
- return max(lo, min(hi, value))
- def build_slot_config(data):
- theme = data.get("theme", "jelly")
- reel_mode = data.get("reelMode", "ways")
- volatility = data.get("volatility", "medium")
- default_features = ["cascades", "free_spins", "wilds"]
- if theme == "pirate_jelly":
- default_features = ["cascades", "free_spins", "wilds", "hold_win", "multipliers"]
- feature_names = set(data.get("features") or default_features)
- columns = 6 if reel_mode == "megaways" else 5
- rows = 5 if reel_mode == "cluster" else 3
- return {
- "schemaVersion": "slot_game_config.v1",
- "creative": data.get("creative", {}),
- "gameDesign": data.get("gameDesign", {}),
- "game": {
- "id": slugify(data.get("gameId") or data.get("title") or "jelly-candy-slot"),
- "title": data.get("title") or "Jelly Candy Slot",
- "mode": "demo_only",
- "engine": "cocos_creator_3_8",
- "orientation": "portrait",
- "targetViewport": {"width": 798, "height": 1724},
- },
- "theme": {
- "key": theme,
- "world": THEME_LABELS.get(theme, theme),
- "visualStyle": data.get("style") or DEFAULT_STYLE_BY_THEME.get(theme, DEFAULT_STYLE_BY_THEME["jelly"]),
- },
- "reels": {
- "mode": reel_mode,
- "columns": columns,
- "rows": rows,
- },
- "layout": {
- "viewport": {"width": 798, "height": 1724, "orientation": "portrait"},
- "logo": {"cy": 0.41, "maxW": 0.82, "maxH": 0.15},
- "reel": {
- "cy": 0.02,
- "h": 0.60,
- "aspect": 0.652,
- "holeW": 0.86,
- "holeH": 0.92,
- "cols": columns,
- "rows": rows,
- },
- "hud": {"cy": -0.33, "pillH": 0.40},
- "controls": {"cy": -0.405, "spinR": 0.13, "smallR": 0.075, "xMinus": 0.24, "xTurbo": 0.40},
- "symbols": {
- "fitMode": "contain",
- "targetCellFill": 0.92,
- "defaultScalePerCell": 0.00093,
- "defaultOriginYOffsetPerCell": 0.5,
- },
- },
- "mathProfile": {
- "volatility": volatility,
- "hitFrequencyFeel": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["hitFrequencyFeel"],
- "rtpTarget": clamp_float(data.get("targetRtp"), 0.96, 0.5, 0.995),
- "rtpTargetLabel": "math_model_candidate" if data.get("enableMathModel", True) else "configurable_demo_no_math_model",
- "enableMathModel": bool(data.get("enableMathModel", True)),
- "bonusChanceLabel": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["bonusChanceLabel"],
- },
- "economy": {
- "startingBalance": clamp_int(data.get("startingBalance"), 5000, 100, 1000000),
- "defaultBet": clamp_int(data.get("defaultBet"), 50, 1, 100000),
- "minBet": 10,
- "maxBet": 1000,
- "betSteps": [10, 20, 50, 100, 200, 500, 1000],
- },
- "winRules": {
- "evaluation": "cluster_count" if reel_mode == "cluster" else ("left_to_right_paylines" if reel_mode == "paylines" else "adjacent_ways_demo"),
- "minMatch": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["minMatch"],
- "paylines": 20 if reel_mode == "paylines" else 0,
- "ways": columns ** rows if reel_mode in ("ways", "megaways") else 0,
- },
- "features": {
- "wilds": {"enabled": "wilds" in feature_names, "variant": data.get("wildVariant", "expanding"), "substitutes": "regular"},
- "scatterFreeSpins": {
- "enabled": "free_spins" in feature_names,
- "triggerCount": 3,
- "awardSpins": clamp_int(data.get("freeSpinCount"), 8, 3, 30),
- "retriggers": True,
- },
- "cascades": {
- "enabled": "cascades" in feature_names,
- "maxCascades": clamp_int(data.get("maxCascades"), 6, 1, 20),
- "multiplierStep": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["cascadeMultiplierStep"],
- "maxMultiplier": VOLATILITY_RULES.get(volatility, VOLATILITY_RULES["medium"])["maxCascadeMultiplier"],
- },
- "holdAndWin": {"enabled": "hold_win" in feature_names, "triggerCount": 6, "respins": 3, "jackpots": ["mini", "minor", "major"]},
- "multipliers": {"enabled": "multipliers" in feature_names},
- },
- "boss": {
- "enabled": bool(data.get("enableBoss", True)),
- "id": data.get("bossId") or "boss_demon_lord",
- "title": data.get("bossTitle") or "大魔王关主",
- "presence": data.get("bossPresence") or "full",
- "idleLoop": ["idle", "watch", "charge"],
- "winReaction": "boss_cracks_open_and_explodes_when_player_wins",
- "loseReaction": "boss_raises_greatsword_and_taunts_after_player_loses",
- "playerWinSequence": ["coin_throw", "hurt", "explode"],
- "playerLoseSequence": ["taunt", "stomp", "attack"],
- },
- "feedback": FEEDBACK_RULES.get(data.get("feedbackIntensity", "standard"), FEEDBACK_RULES["standard"]),
- "assetGeneration": {
- "characterCount": int(data.get("characterCount") or 10),
- "uiCompleteness": data.get("uiCompleteness", "full"),
- "feedbackIntensity": data.get("feedbackIntensity", "standard"),
- },
- }
- def build_manifest(slot_config):
- slot_config = copy.deepcopy(slot_config)
- theme = slot_config["theme"]["key"]
- style = slot_config["theme"]["visualStyle"]
- count = slot_config["assetGeneration"]["characterCount"]
- base_symbols = copy.deepcopy(BASE_SYMBOLS.get(theme, BASE_SYMBOLS["jelly"]))
- special_symbols = []
- if slot_config["features"]["wilds"]["enabled"]:
- special_symbols.append(("wild", "a glossy rainbow WILD slot symbol icon, premium mobile game asset, no small text"))
- if slot_config["features"]["scatterFreeSpins"]["enabled"]:
- special_symbols.append(("scatter", "a glowing scatter bonus candy star symbol, premium mobile slot game icon, no small text"))
- if slot_config["features"]["holdAndWin"]["enabled"]:
- special_symbols.extend([
- ("coin_cash", "a shiny golden cash coin slot symbol with soft glow, premium mobile game icon"),
- ("collect", "a glossy collect symbol with magnet-like gold energy, premium mobile slot game icon"),
- ])
- regular_count = max(1, count - len(special_symbols))
- symbols = base_symbols[:regular_count] + special_symbols
- symbol_rules = build_symbol_rules(symbols)
- slot_config["symbols"] = symbol_rules
- slot_config["paytable"] = build_paytable(symbol_rules, slot_config["mathProfile"]["volatility"])
- if slot_config.get("mathProfile", {}).get("enableMathModel", True):
- slot_config["mathModel"] = slot_math.build_math_model(slot_config)
- else:
- slot_config["mathModel"] = {
- "status": "disabled_by_user",
- "notes": ["Math model generation was disabled for this configurable demo."],
- }
- vfx = [
- {"id": "win_burst", "type": "particle", "template": "burst", "color": [255, 180, 80]},
- ]
- if slot_config["features"]["cascades"]["enabled"]:
- vfx.append({"id": "confetti_pop", "type": "particle", "template": "confetti", "color": [255, 120, 200]})
- if slot_config["features"]["scatterFreeSpins"]["enabled"] or slot_config["features"]["holdAndWin"]["enabled"]:
- vfx.append({"id": "bigwin_glow", "type": "particle", "template": "glow", "color": [255, 240, 160]})
- if slot_config["features"]["holdAndWin"]["enabled"] or slot_config["assetGeneration"]["feedbackIntensity"] == "loud":
- vfx.append({"id": "coin_rain", "type": "particle", "template": "rain", "color": [255, 215, 0]})
- if slot_config.get("boss", {}).get("enabled", True):
- vfx.append({"id": "boss_explosion", "type": "particle", "template": "burst", "color": [190, 80, 255]})
- ui = [
- {"id": "spin_btn_press", "type": "tween", "preset": "scale_bounce"},
- {"id": "reward_popup_in", "type": "tween", "preset": "elastic_in"},
- {"id": "panel_slide_in", "type": "tween", "preset": "fade_slide_in", "params": {"dy": 60}},
- {"id": "balance_roll", "type": "tween", "preset": "number_roll", "params": {"from": 0, "to": 8888, "dur": 0.9}},
- {"id": "win_icon_pulse", "type": "tween", "preset": "pulse"},
- ]
- ui_art_ids = {"basic": 6, "full": len(UI_ART)}
- ui_art_count = ui_art_ids.get(slot_config["assetGeneration"]["uiCompleteness"], len(UI_ART))
- characters = [
- {"id": sid, "type": "spine", "animations": ["idle", "win"], "prompt": prompt}
- for sid, prompt in symbols
- ]
- if slot_config.get("boss", {}).get("enabled", True):
- profile = BOSS_BY_THEME.get(theme, BOSS_BY_THEME["jelly"])
- if slot_config["boss"].get("title") == "大魔王关主":
- slot_config["boss"]["title"] = profile["title"]
- characters.append(build_boss_character(slot_config))
- return {
- "game": slot_config["game"]["id"],
- "style": style,
- "slot_config": slot_config,
- "characters": characters,
- "ui_art": [
- {"id": aid, "transparent": transparent, "size": size, "prompt": prompt}
- for aid, transparent, size, prompt in UI_ART[:ui_art_count]
- ],
- "vfx": vfx,
- "ui": ui,
- }
- def complete_manifest(manifest):
- """Fill missing generated sections before running the image pipeline.
- Older or hand-edited manifests may only contain characters. When a
- slot_config exists, it is the source of truth for the complete asset plan.
- """
- manifest = copy.deepcopy(manifest or {})
- if manifest.get("slot_config"):
- full = build_manifest(manifest["slot_config"])
- for key in ("game", "style", "characters", "ui_art", "vfx", "ui"):
- manifest[key] = full.get(key)
- manifest["slot_config"] = full.get("slot_config", manifest["slot_config"])
- return manifest
- if not manifest.get("ui_art"):
- manifest["ui_art"] = [
- {"id": aid, "transparent": transparent, "size": size, "prompt": prompt}
- for aid, transparent, size, prompt in UI_ART
- ]
- manifest.setdefault("vfx", [])
- manifest.setdefault("ui", [])
- return manifest
- def build_workflow(data):
- slot_config = build_slot_config(data or {})
- manifest = build_manifest(slot_config)
- return {"slot_config": manifest["slot_config"], "manifest": manifest}
|