| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- // =============================================================
- // 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<string, sp.SkeletonData> = {};
- 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<string, number> = {};
- 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();
- }
- }
|