Browse Source

Add boss character gameplay feedback

bang 2 weeks ago
parent
commit
e785fb49d6
3 changed files with 81 additions and 11 deletions
  1. 4 2
      exporter.py
  2. 29 4
      slot_workflow.py
  3. 48 5
      templates/SlotGame.ts

+ 4 - 2
exporter.py

@@ -655,13 +655,15 @@ def export(game, out_root, log=print):
     slot_tmpl = os.path.join(HERE, "templates", "SlotGame.ts")
     slot_src = open(slot_tmpl, encoding="utf-8").read() if os.path.isfile(slot_tmpl) else SLOT_GAME_TS
     slot_config = lib.get("slot_config") or {}
+    configured_symbols = [s.get("id") for s in (slot_config.get("symbols") or []) if s.get("id")]
+    slot_symbol_ids = [sid for sid in configured_symbols if sid in char_ids] or char_ids
     symbol_fit, symbol_fit_default = _symbol_fit_from_library(lib)
     game_config_ts = (
         "export const SLOT_CONFIG = "
         + json.dumps(slot_config, ensure_ascii=False, indent=2)
         + " as const;\n"
         + "export const SYMBOLS = "
-        + json.dumps(char_ids, ensure_ascii=False, indent=2)
+        + json.dumps(slot_symbol_ids, ensure_ascii=False, indent=2)
         + " as const;\n"
         + "export const SYMBOL_FIT = "
         + json.dumps(symbol_fit, ensure_ascii=False, indent=2)
@@ -670,7 +672,7 @@ def export(game, out_root, log=print):
     open(os.path.join(scripts, "GameConfig.ts"), "w", encoding="utf-8").write(game_config_ts)
     open(os.path.join(pack, "SlotMathReport.md"), "w", encoding="utf-8").write(_math_report_md(slot_config))
     slot = (slot_src
-            .replace("__SYMBOLS__", json.dumps(char_ids, ensure_ascii=False))
+            .replace("__SYMBOLS__", json.dumps(slot_symbol_ids, ensure_ascii=False))
             .replace("__GAME_CONFIG__", json.dumps(slot_config, ensure_ascii=False))
             .replace("__SYMBOL_FIT__", json.dumps(symbol_fit, ensure_ascii=False))
             .replace("__SYMBOL_FIT_DEFAULT__", json.dumps(symbol_fit_default, ensure_ascii=False)))

+ 29 - 4
slot_workflow.py

@@ -285,6 +285,13 @@ def build_slot_config(data):
             "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 "大魔王关主",
+            "winReaction": "boss_cracks_open_and_explodes_when_player_wins",
+            "loseReaction": "boss_raises_greatsword_and_taunts_after_player_loses",
+        },
         "feedback": FEEDBACK_RULES.get(data.get("feedbackIntensity", "standard"), FEEDBACK_RULES["standard"]),
         "assetGeneration": {
             "characterCount": int(data.get("characterCount") or 10),
@@ -332,6 +339,8 @@ def build_manifest(slot_config):
         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"},
@@ -344,14 +353,30 @@ def build_manifest(slot_config):
     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):
+        boss_id = slot_config["boss"]["id"]
+        characters.append({
+            "id": boss_id,
+            "type": "spine",
+            "role": "boss",
+            "animations": ["idle", "hurt", "attack", "win"],
+            "prompt": (
+                "a cute stylized mobile game demon lord boss character, full body, oversized dark armor, "
+                "horned crown, glowing eyes, holding a huge greatsword, triumphant villain pose, one boot "
+                "planted forward on a simple defeated hero silhouette, no gore, no blood, clean mobile slot "
+                "game boss asset, transparent background"
+            ),
+        })
+
     return {
         "game": slot_config["game"]["id"],
         "style": style,
         "slot_config": slot_config,
-        "characters": [
-            {"id": sid, "type": "spine", "animations": ["idle", "win"], "prompt": prompt}
-            for sid, prompt in symbols
-        ],
+        "characters": characters,
         "ui_art": [
             {"id": aid, "transparent": transparent, "size": size, "prompt": prompt}
             for aid, transparent, size, prompt in UI_ART[:ui_art_count]

+ 48 - 5
templates/SlotGame.ts

@@ -37,6 +37,8 @@ const SYMBOL_RULES = GENERATED_CONFIG.symbols || [];
 const MATH_MODEL = GENERATED_CONFIG.mathModel || {};
 const PAYOUT_SCALE = MATH_MODEL.payoutScale ?? 1;
 const REEL_STRIPS = MATH_MODEL.reelStrips || [];
+const BOSS_CONFIG = GENERATED_CONFIG.boss || { enabled: false };
+const BOSS_ID = BOSS_CONFIG.id || 'boss_demon_lord';
 
 // ============ 文字样式表(统一定义,按角色取用)============
 // size<=1 表示相对所在容器高度的比例;否则按像素。ow=描边宽。
@@ -120,6 +122,8 @@ export class SlotGame extends Component {
   private t = 0;
   private turbo = false; private auto = false;
   private turboBtn!: Node; private autoBtn!: Node;
+  private bossNode: Node | null = null;
+  private bossSk: sp.Skeleton | null = null;
 
   onLoad() {
     try { (cc as any).profiler?.hideStats?.(); } catch (e) {}
@@ -149,11 +153,12 @@ export class SlotGame extends Component {
     this.buildArt(W, H);
     this.buildHud(W, H);
     this.buildControls(W, H);
-    let left = SYMBOLS.length;
-    SYMBOLS.forEach((id) => {
+    const loadIds = BOSS_CONFIG.enabled ? SYMBOLS.concat([BOSS_ID]) : SYMBOLS;
+    let left = loadIds.length;
+    loadIds.forEach((id) => {
       resources.load(`characters/${id}`, sp.SkeletonData, (err, data) => {
         if (!err) this.dataMap[id] = data;
-        if (--left === 0) this.buildGrid();
+        if (--left === 0) { this.buildGrid(); this.buildBoss(); }
       });
     });
   }
@@ -205,6 +210,19 @@ export class SlotGame extends Component {
     }
   }
 
+  private buildBoss() {
+    if (!BOSS_CONFIG.enabled || !this.dataMap[BOSS_ID]) return;
+    const node = new Node('boss'); node.parent = this.node; node.setSiblingIndex(8);
+    const sk = node.addComponent(sp.Skeleton);
+    sk.skeletonData = this.dataMap[BOSS_ID];
+    sk.premultipliedAlpha = false;
+    sk.setAnimation(0, 'idle', true);
+    const scale = Math.min(0.22, this.W / 3900);
+    node.setScale(scale, scale, 1);
+    node.setPosition(this.frameW * 0.40, this.frameCY + this.frameH * 0.32, 0);
+    this.bossNode = node; this.bossSk = sk;
+  }
+
   // ---------------- HUD:用 hud_pill 美术 ----------------
   private buildHud(W: number, H: number) {
     const y = H * LAYOUT.hud.cy;
@@ -345,6 +363,31 @@ export class SlotGame extends Component {
     tween(op).delay(0.95).to(0.35, { opacity: 0 }).call(() => n.destroy()).start();
     this.coinShower(Math.max(12, Math.min(40, Math.floor(amount / 15))));
   }
+
+  private bossDefeated() {
+    if (!this.bossNode || !this.bossSk) return;
+    this.bossSk.setAnimation(0, 'hurt', false);
+    this.bossSk.addAnimation(0, 'idle', true, 0.2);
+    this.playParticle('boss_explosion');
+    const n = this.bossNode;
+    const base = n.scale.x;
+    tween(n).to(0.10, { scale: new Vec3(base * 1.18, base * 0.86, 1), angle: -7 })
+            .to(0.12, { scale: new Vec3(base * 0.82, base * 1.18, 1), angle: 8 })
+            .to(0.18, { scale: new Vec3(base, base, 1), angle: 0 }, { easing: 'backOut' }).start();
+    this.flashMult('大魔王裂开爆炸!');
+  }
+
+  private bossTaunt() {
+    if (!this.bossNode || !this.bossSk) return;
+    this.bossSk.setAnimation(0, 'attack', false);
+    this.bossSk.addAnimation(0, 'idle', true, 0);
+    const n = this.bossNode;
+    const x = n.position.x, y = n.position.y;
+    tween(n).to(0.12, { position: new Vec3(x - this.cell * 0.12, y + this.cell * 0.10, 0), angle: -5 })
+            .to(0.16, { position: new Vec3(x + this.cell * 0.10, y - this.cell * 0.18, 0), angle: 7 })
+            .to(0.18, { position: new Vec3(x, y, 0), angle: 0 }, { easing: 'backOut' }).start();
+    this.flashMult('大魔王举剑耀武扬威');
+  }
   private coinShower(count: number) {
     const sf = this.art['coin']; if (!sf) return;
     const sz = this.cell * 0.6;
@@ -636,8 +679,8 @@ export class SlotGame extends Component {
   }
 
   private endRound() {
-    if (this.roundWin > 0) { this.multLabel.string = ''; this.celebrate(this.roundWin); }
-    else this.multLabel.string = '';
+    if (this.roundWin > 0) { this.multLabel.string = ''; this.celebrate(this.roundWin); this.bossDefeated(); }
+    else { this.multLabel.string = ''; this.bossTaunt(); }
     this.spinning = false; this.setSpinEnabled(true);
     if (this.holdActive) return;
     if (this.freeSpins > 0) this.scheduleOnce(() => this.spin(), this.td(0.9));