// ============================================================= // SlotGame.ts —— 《果冻消消乐 · Jelly Pop》消除式老虎机(可玩原型) // by anim_studio(按本 game 的角色自动生成) // // 小故事:糖果星球上,一群果冻好朋友最爱挤在一起蹦跳; // 只要同一种果冻聚到 5 只以上,它们就会开心地"啵"地一起消失, // 天上撒下金币,上面的果冻落下来补位,连锁越多、倍数越高。 // // 用法:把本脚本挂到 Canvas 下的一个空节点上,点播放。 // ============================================================= import { _decorator, Component, Node, sp, resources, JsonAsset, SpriteFrame, Sprite, ParticleSystem2D, UITransform, Label, Graphics, Button, Color, Vec3, view, EventTouch, tween, profiler, } from 'cc'; import { applyParticleConfig } from './ParticleConfig'; const { ccclass } = _decorator; const SYMBOLS = ["jelly_blue", "jelly_choco", "jelly_green", "jelly_lemon", "jelly_orange", "jelly_pink", "jelly_purple", "jelly_rainbow", "symbol_coin", "symbol_seven"]; const COLS = 7, ROWS = 6; const MIN_MATCH = 5; // 同种 ≥5 才消除 const BET = 50; const START_BALANCE = 5000; @ccclass('SlotGame') export class SlotGame extends Component { private dataMap: Record = {}; private cells: sp.Skeleton[][] = []; // [col][row] private ids: string[][] = []; // 当前每格符号 private cell = 90; private gridX0 = 0; private gridY0 = 0; private spinning = false; private balance = START_BALANCE; private displayBalance = START_BALANCE; private multiplier = 1; private roundWin = 0; private balanceLabel!: Label; private betLabel!: Label; private winLabel!: Label; private multLabel!: Label; private spinBtn!: Node; private particleTex: SpriteFrame | null = null; private frameCY = 0; private frameW = 0; private frameH = 0; private logoNode!: Node; onLoad() { profiler && profiler.hideStats(); this.node.setPosition(0, 0, 0); const ut = this.node.getComponent(UITransform) || this.node.addComponent(UITransform); ut.setAnchorPoint(0.5, 0.5); const s = view.getVisibleSize(); ut.setContentSize(s.width, s.height); this.computeLayout(s.width, s.height); this.buildBackground(s.width, s.height); this.buildHeader(s.width, s.height); this.buildFrame(); this.buildHud(s.width, s.height); this.buildControls(s.width, s.height); this.loadArt(s.width, s.height); // 有生成的美术就贴上来,替换代码画的 let left = SYMBOLS.length; resources.load('vfx/particle/spriteFrame', SpriteFrame, (_e, sf) => { if (sf) this.particleTex = sf; }); SYMBOLS.forEach((id) => { resources.load(`characters/${id}`, sp.SkeletonData, (err, data) => { if (!err) this.dataMap[id] = data; if (--left === 0) this.buildGrid(); }); }); } private computeLayout(W: number, H: number) { const reelW = Math.min(W * 0.94, 720); const reelTop = H * 0.28; // 网格上沿(屏幕中上) const reelMaxH = H * 0.42; this.cell = Math.min(reelW / COLS, reelMaxH / ROWS); this.gridX0 = -((COLS - 1) * this.cell) / 2; this.gridY0 = reelTop - this.cell / 2; // 第 0 行(顶行)中心 } private cellPos(c: number, r: number): [number, number] { return [this.gridX0 + c * this.cell, this.gridY0 - r * this.cell]; } // ---------------- 背景 ---------------- private buildBackground(W: number, H: number) { const n = new Node('bg'); n.parent = this.node; n.setSiblingIndex(0); const g = n.addComponent(Graphics); g.fillColor = new Color(120, 200, 255, 255); // 上:天蓝 g.rect(-W / 2, 0, W, H / 2); g.fill(); g.fillColor = new Color(255, 150, 175, 255); // 下:糖果粉 g.rect(-W / 2, -H / 2, W, H / 2); g.fill(); g.fillColor = new Color(150, 175, 230, 255); // 中间过渡带 g.rect(-W / 2, -20, W, 40); g.fill(); } // ---------------- 顶部 Logo + 故事副标题 ---------------- private buildHeader(W: number, H: number) { const t = new Node('logo'); t.parent = this.node; t.setPosition(0, H / 2 - 70, 0); this.logoNode = t; const lab = t.addComponent(Label); lab.string = '🍬 JELLY POP'; lab.fontSize = 46; lab.lineHeight = 50; lab.color = new Color(255, 90, 70, 255); const sub = new Node('sub'); sub.parent = this.node; sub.setPosition(0, H / 2 - 110, 0); const sl = sub.addComponent(Label); sl.string = '果冻消消乐 · 同款聚 5 只就啵啵消除'; sl.fontSize = 18; sl.lineHeight = 22; sl.color = new Color(255, 255, 255, 230); } // ---------------- 卷轴发光框 ---------------- private buildFrame() { const w = COLS * this.cell + 24, h = ROWS * this.cell + 24; const cy = (this.gridY0 - (ROWS - 1) * this.cell / 2); this.frameCY = cy; this.frameW = w; this.frameH = h; const n = new Node('frame'); n.parent = this.node; n.setPosition(0, cy, 0); const g = n.addComponent(Graphics); g.fillColor = new Color(245, 244, 255, 235); // 半透明白底 this.roundRect(g, -w / 2, -h / 2, w, h, 20); g.fill(); g.lineWidth = 8; g.strokeColor = new Color(120, 180, 255, 255); // 蓝色发光边 this.roundRect(g, -w / 2, -h / 2, w, h, 20); g.stroke(); g.lineWidth = 3; g.strokeColor = new Color(255, 255, 255, 230); // 内白线 this.roundRect(g, -w / 2 + 5, -h / 2 + 5, w - 10, h - 10, 16); g.stroke(); // 竖向分隔线 g.lineWidth = 2; g.strokeColor = new Color(180, 190, 230, 90); for (let c = 1; c < COLS; c++) { const x = -w / 2 + 12 + c * this.cell; g.moveTo(x, -h / 2 + 10); g.lineTo(x, h / 2 - 10); } g.stroke(); } // ---------------- 底部 HUD 三胶囊 ---------------- private buildHud(W: number, H: number) { const y = -H * 0.13; const pw = Math.min(W * 0.3, 230), ph = 56, gap = 12; this.balanceLabel = this.makePill(-(pw + gap), y, pw, ph, '💰', `${START_BALANCE}`); this.betLabel = this.makePill(0, y, pw, ph, '🎯', `${BET}`); this.winLabel = this.makePill(pw + gap, y, pw, ph, '👑', '0'); const m = new Node('mult'); m.parent = this.node; m.setPosition(0, -H * 0.13 + 70, 0); this.multLabel = m.addComponent(Label); this.multLabel.fontSize = 40; this.multLabel.color = new Color(255, 110, 70, 255); this.multLabel.string = ''; } private makePill(x: number, y: number, w: number, h: number, icon: string, val: string): Label { const n = new Node('pill'); n.parent = this.node; n.setPosition(x, y, 0); n.addComponent(UITransform).setContentSize(w, h); const g = n.addComponent(Graphics); g.fillColor = new Color(70, 50, 120, 235); this.roundRect(g, -w / 2, -h / 2, w, h, h / 2); g.fill(); const il = new Node('i'); il.parent = n; il.setPosition(-w / 2 + 26, 0, 0); const ic = il.addComponent(Label); ic.string = icon; ic.fontSize = 26; const vl = new Node('v'); vl.parent = n; vl.setPosition(12, 0, 0); const lab = vl.addComponent(Label); lab.string = val; lab.fontSize = 26; lab.color = new Color(255, 255, 255, 255); return lab; } // ---------------- 控制区:TURBO / − / SPIN / + / AUTO ---------------- private buildControls(W: number, H: number) { const y = -H * 0.36; this.spinBtn = this.makeRoundButton(0, y, 72, new Color(60, 120, 255, 255), '⟳', 44, () => this.spin()); this.makeRoundButton(-W * 0.34, y, 34, new Color(90, 70, 140, 255), '⚡', 24, () => {}); this.makeRoundButton(-W * 0.2, y, 34, new Color(90, 70, 140, 255), '−', 30, () => {}); this.makeRoundButton(W * 0.2, y, 34, new Color(90, 70, 140, 255), '+', 30, () => {}); this.makeRoundButton(W * 0.34, y, 34, new Color(90, 70, 140, 255), '▶', 24, () => {}); this.makeTextLabel(-W * 0.34, y - 48, 'TURBO', 16); this.makeTextLabel(W * 0.34, y - 48, 'AUTO', 16); } private makeRoundButton(x: number, y: number, r: number, color: Color, glyph: string, fs: number, onClick: () => void): Node { const node = new Node('btn'); node.parent = this.node; node.setPosition(x, y, 0); node.addComponent(UITransform).setContentSize(r * 2, r * 2); const g = node.addComponent(Graphics); g.fillColor = color; g.circle(0, 0, r); g.fill(); g.lineWidth = 4; g.strokeColor = new Color(255, 255, 255, 235); g.circle(0, 0, r); g.stroke(); const gl = new Node('g'); gl.parent = node; const lab = gl.addComponent(Label); lab.string = glyph; lab.fontSize = fs; lab.color = new Color(255, 255, 255, 255); const btn = node.addComponent(Button); btn.transition = Button.Transition.SCALE; btn.zoomScale = 0.9; node.on(Node.EventType.TOUCH_END, (_e: EventTouch) => onClick()); return node; } private makeTextLabel(x: number, y: number, text: string, fs: number) { const n = new Node('tl'); n.parent = this.node; n.setPosition(x, y, 0); const lab = n.addComponent(Label); lab.string = text; lab.fontSize = fs; lab.color = new Color(255, 255, 255, 230); } // ---------------- 套用生成的 UI 美术(有就贴,没有就保持代码画)---------------- private loadArt(W: number, H: number) { // 背景整图:铺满全屏,压在最底层 this.tryArt('bg_main', (sf) => { const n = this.spriteNode(sf, W, H); n.setPosition(0, 0, 0); n.setSiblingIndex(1); }); // 卷轴外框:贴到网格区域,盖掉代码框 this.tryArt('reel_frame', (sf) => { const n = this.spriteNode(sf, this.frameW + 40, this.frameH + 40); n.setPosition(0, this.frameCY, 0); const code = this.node.getChildByName('frame'); if (code) code.active = false; }); // Logo:贴到顶部,隐藏文字 Logo this.tryArt('logo', (sf) => { const n = this.spriteNode(sf, 380, 200); n.setPosition(0, H / 2 - 110, 0); if (this.logoNode) this.logoNode.active = false; }); // SPIN 按钮贴图 this.tryArt('btn_spin', (sf) => { const s = this.spinBtn.addComponent(Sprite); s.spriteFrame = sf; s.sizeMode = Sprite.SizeMode.CUSTOM; const ut = this.spinBtn.getComponent(UITransform)!; ut.setContentSize(150, 150); const g = this.spinBtn.getComponent(Graphics); if (g) g.clear(); const gl = this.spinBtn.getChildByName('g'); if (gl) gl.active = false; }); } private tryArt(id: string, cb: (sf: SpriteFrame) => void) { resources.load(`ui_art/${id}/spriteFrame`, SpriteFrame, (err, sf) => { if (!err && sf) cb(sf); }); } private spriteNode(sf: SpriteFrame, w: number, h: number): Node { const n = new Node('art'); n.parent = this.node; const ut = n.addComponent(UITransform); ut.setContentSize(w, h); const s = n.addComponent(Sprite); s.spriteFrame = sf; s.sizeMode = Sprite.SizeMode.CUSTOM; return n; } // ---------------- 网格 ---------------- private buildGrid() { const tileScale = (this.cell * 0.82) / 1032; // 角色原图约 1032px for (let c = 0; c < COLS; c++) { this.cells[c] = []; this.ids[c] = []; for (let r = 0; r < ROWS; r++) { const id = this.rand(); const [x, y] = this.cellPos(c, r); const node = new Node(`cell_${c}_${r}`); node.parent = this.node; node.setPosition(x, y, 0); node.setScale(tileScale, tileScale, 1); const sk = node.addComponent(sp.Skeleton); sk.skeletonData = this.dataMap[id]; sk.premultipliedAlpha = false; sk.setAnimation(0, 'idle', true); this.cells[c][r] = sk; this.ids[c][r] = id; } } } private setSym(c: number, r: number, id: string) { const sk = this.cells[c][r]; sk.skeletonData = this.dataMap[id]; sk.premultipliedAlpha = false; sk.setAnimation(0, 'idle', true); this.ids[c][r] = id; const base = (this.cell * 0.82) / 1032; sk.node.setScale(base * 1.3, base * 1.3, 1); tween(sk.node).to(0.14, { scale: new Vec3(base, base, 1) }, { easing: 'backOut' }).start(); } // ---------------- 玩法 ---------------- private spin() { if (this.spinning) return; if (this.balance < BET) { this.flashMult('余额不足'); return; } this.balance -= BET; this.displayBalance = this.balance; this.balanceLabel.string = `${Math.floor(this.displayBalance)}`; this.multiplier = 1; this.roundWin = 0; this.winLabel.string = '0'; this.multLabel.string = ''; this.spinning = true; this.setSpinEnabled(false); // 全盘随机铺一次(带落入动画) for (let c = 0; c < COLS; c++) for (let r = 0; r < ROWS; r++) this.setSym(c, r, this.rand()); this.scheduleOnce(() => this.resolve(), 0.45); } // 消除-补位 连锁 private resolve() { const count: Record = {}; for (let c = 0; c < COLS; c++) for (let r = 0; r < ROWS; r++) count[this.ids[c][r]] = (count[this.ids[c][r]] || 0) + 1; const winSyms = Object.keys(count).filter((k) => count[k] >= MIN_MATCH); if (winSyms.length === 0) { this.endRound(); return; } let cleared = 0; const winners: sp.Skeleton[] = []; for (let c = 0; c < COLS; c++) for (let r = 0; r < ROWS; r++) if (winSyms.indexOf(this.ids[c][r]) >= 0) { winners.push(this.cells[c][r]); cleared++; } const pay = Math.floor(cleared * (BET / MIN_MATCH) * this.multiplier); this.roundWin += pay; this.balance += pay; this.winLabel.string = `${this.roundWin}`; this.flashMult(`x${this.multiplier} +${pay}`); winners.forEach((sk) => { sk.setAnimation(0, 'win', false); sk.addAnimation(0, 'idle', true, 0); }); this.playParticle('coin_rain'); // 短暂后:把中奖格替换成新符号(消除→补位),倍数 +1,继续连锁 this.scheduleOnce(() => { for (let c = 0; c < COLS; c++) for (let r = 0; r < ROWS; r++) if (winSyms.indexOf(this.ids[c][r]) >= 0) this.setSym(c, r, this.rand()); this.multiplier += 1; this.scheduleOnce(() => this.resolve(), 0.4); }, 0.6); } private endRound() { if (this.roundWin > 0) this.flashMult(`总赢 +${this.roundWin}`); else this.multLabel.string = ''; this.spinning = false; this.setSpinEnabled(true); } private flashMult(text: string) { this.multLabel.string = text; const n = this.multLabel.node; n.setScale(0.6, 0.6, 1); tween(n).to(0.3, { scale: new Vec3(1, 1, 1) }, { easing: 'elasticOut' }).start(); } private playParticle(id: string) { resources.load(`vfx/${id}`, JsonAsset, (err, asset) => { if (err) return; const n = new Node('p'); n.parent = this.node; n.setPosition(0, view.getVisibleSize().height / 2 - 200, 0); const ps = n.addComponent(ParticleSystem2D); applyParticleConfig(ps, asset.json, this.particleTex as SpriteFrame); this.scheduleOnce(() => ps.stopSystem(), 1.2); this.scheduleOnce(() => n.destroy(), 4); }); } update(dt: number) { if (Math.abs(this.displayBalance - this.balance) > 0.5) { this.displayBalance += (this.balance - this.displayBalance) * Math.min(1, dt * 6); this.balanceLabel.string = `${Math.floor(this.displayBalance)}`; } } private rand() { return SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]; } private setSpinEnabled(on: boolean) { const btn = this.spinBtn.getComponent(Button); if (btn) btn.interactable = on; this.spinBtn.getChildByName('g')!.getComponent(Label)!.string = on ? '⟳' : '…'; } private roundRect(g: Graphics, x: number, y: number, w: number, h: number, r: number) { g.moveTo(x + r, y); g.lineTo(x + w - r, y); g.arc(x + w - r, y + r, r, -Math.PI / 2, 0, false); g.lineTo(x + w, y + h - r); g.arc(x + w - r, y + h - r, r, 0, Math.PI / 2, false); g.lineTo(x + r, y + h); g.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI, false); g.lineTo(x, y + r); g.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5, false); g.close(); } }