SlotGame.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // =============================================================
  2. // SlotGame.ts —— 《果冻消消乐 · Jelly Pop》消除式老虎机(可玩原型)
  3. // by anim_studio(按本 game 的角色自动生成)
  4. //
  5. // 小故事:糖果星球上,一群果冻好朋友最爱挤在一起蹦跳;
  6. // 只要同一种果冻聚到 5 只以上,它们就会开心地"啵"地一起消失,
  7. // 天上撒下金币,上面的果冻落下来补位,连锁越多、倍数越高。
  8. //
  9. // 用法:把本脚本挂到 Canvas 下的一个空节点上,点播放。
  10. // =============================================================
  11. import {
  12. _decorator, Component, Node, sp, resources, JsonAsset, SpriteFrame, Sprite,
  13. ParticleSystem2D, UITransform, Label, Graphics, Button, Color, Vec3,
  14. view, EventTouch, tween, profiler,
  15. } from 'cc';
  16. import { applyParticleConfig } from './ParticleConfig';
  17. const { ccclass } = _decorator;
  18. const SYMBOLS = ["jelly_blue", "jelly_choco", "jelly_green", "jelly_lemon", "jelly_orange", "jelly_pink", "jelly_purple", "jelly_rainbow", "symbol_coin", "symbol_seven"];
  19. const COLS = 7, ROWS = 6;
  20. const MIN_MATCH = 5; // 同种 ≥5 才消除
  21. const BET = 50;
  22. const START_BALANCE = 5000;
  23. @ccclass('SlotGame')
  24. export class SlotGame extends Component {
  25. private dataMap: Record<string, sp.SkeletonData> = {};
  26. private cells: sp.Skeleton[][] = []; // [col][row]
  27. private ids: string[][] = []; // 当前每格符号
  28. private cell = 90;
  29. private gridX0 = 0;
  30. private gridY0 = 0;
  31. private spinning = false;
  32. private balance = START_BALANCE;
  33. private displayBalance = START_BALANCE;
  34. private multiplier = 1;
  35. private roundWin = 0;
  36. private balanceLabel!: Label;
  37. private betLabel!: Label;
  38. private winLabel!: Label;
  39. private multLabel!: Label;
  40. private spinBtn!: Node;
  41. private particleTex: SpriteFrame | null = null;
  42. private frameCY = 0; private frameW = 0; private frameH = 0;
  43. private logoNode!: Node;
  44. onLoad() {
  45. profiler && profiler.hideStats();
  46. this.node.setPosition(0, 0, 0);
  47. const ut = this.node.getComponent(UITransform) || this.node.addComponent(UITransform);
  48. ut.setAnchorPoint(0.5, 0.5);
  49. const s = view.getVisibleSize();
  50. ut.setContentSize(s.width, s.height);
  51. this.computeLayout(s.width, s.height);
  52. this.buildBackground(s.width, s.height);
  53. this.buildHeader(s.width, s.height);
  54. this.buildFrame();
  55. this.buildHud(s.width, s.height);
  56. this.buildControls(s.width, s.height);
  57. this.loadArt(s.width, s.height); // 有生成的美术就贴上来,替换代码画的
  58. let left = SYMBOLS.length;
  59. resources.load('vfx/particle/spriteFrame', SpriteFrame, (_e, sf) => { if (sf) this.particleTex = sf; });
  60. SYMBOLS.forEach((id) => {
  61. resources.load(`characters/${id}`, sp.SkeletonData, (err, data) => {
  62. if (!err) this.dataMap[id] = data;
  63. if (--left === 0) this.buildGrid();
  64. });
  65. });
  66. }
  67. private computeLayout(W: number, H: number) {
  68. const reelW = Math.min(W * 0.94, 720);
  69. const reelTop = H * 0.28; // 网格上沿(屏幕中上)
  70. const reelMaxH = H * 0.42;
  71. this.cell = Math.min(reelW / COLS, reelMaxH / ROWS);
  72. this.gridX0 = -((COLS - 1) * this.cell) / 2;
  73. this.gridY0 = reelTop - this.cell / 2; // 第 0 行(顶行)中心
  74. }
  75. private cellPos(c: number, r: number): [number, number] {
  76. return [this.gridX0 + c * this.cell, this.gridY0 - r * this.cell];
  77. }
  78. // ---------------- 背景 ----------------
  79. private buildBackground(W: number, H: number) {
  80. const n = new Node('bg'); n.parent = this.node; n.setSiblingIndex(0);
  81. const g = n.addComponent(Graphics);
  82. g.fillColor = new Color(120, 200, 255, 255); // 上:天蓝
  83. g.rect(-W / 2, 0, W, H / 2); g.fill();
  84. g.fillColor = new Color(255, 150, 175, 255); // 下:糖果粉
  85. g.rect(-W / 2, -H / 2, W, H / 2); g.fill();
  86. g.fillColor = new Color(150, 175, 230, 255); // 中间过渡带
  87. g.rect(-W / 2, -20, W, 40); g.fill();
  88. }
  89. // ---------------- 顶部 Logo + 故事副标题 ----------------
  90. private buildHeader(W: number, H: number) {
  91. const t = new Node('logo'); t.parent = this.node; t.setPosition(0, H / 2 - 70, 0);
  92. this.logoNode = t;
  93. const lab = t.addComponent(Label);
  94. lab.string = '🍬 JELLY POP'; lab.fontSize = 46; lab.lineHeight = 50;
  95. lab.color = new Color(255, 90, 70, 255);
  96. const sub = new Node('sub'); sub.parent = this.node; sub.setPosition(0, H / 2 - 110, 0);
  97. const sl = sub.addComponent(Label);
  98. sl.string = '果冻消消乐 · 同款聚 5 只就啵啵消除'; sl.fontSize = 18; sl.lineHeight = 22;
  99. sl.color = new Color(255, 255, 255, 230);
  100. }
  101. // ---------------- 卷轴发光框 ----------------
  102. private buildFrame() {
  103. const w = COLS * this.cell + 24, h = ROWS * this.cell + 24;
  104. const cy = (this.gridY0 - (ROWS - 1) * this.cell / 2);
  105. this.frameCY = cy; this.frameW = w; this.frameH = h;
  106. const n = new Node('frame'); n.parent = this.node; n.setPosition(0, cy, 0);
  107. const g = n.addComponent(Graphics);
  108. g.fillColor = new Color(245, 244, 255, 235); // 半透明白底
  109. this.roundRect(g, -w / 2, -h / 2, w, h, 20); g.fill();
  110. g.lineWidth = 8; g.strokeColor = new Color(120, 180, 255, 255); // 蓝色发光边
  111. this.roundRect(g, -w / 2, -h / 2, w, h, 20); g.stroke();
  112. g.lineWidth = 3; g.strokeColor = new Color(255, 255, 255, 230); // 内白线
  113. this.roundRect(g, -w / 2 + 5, -h / 2 + 5, w - 10, h - 10, 16); g.stroke();
  114. // 竖向分隔线
  115. g.lineWidth = 2; g.strokeColor = new Color(180, 190, 230, 90);
  116. for (let c = 1; c < COLS; c++) {
  117. const x = -w / 2 + 12 + c * this.cell;
  118. g.moveTo(x, -h / 2 + 10); g.lineTo(x, h / 2 - 10);
  119. }
  120. g.stroke();
  121. }
  122. // ---------------- 底部 HUD 三胶囊 ----------------
  123. private buildHud(W: number, H: number) {
  124. const y = -H * 0.13;
  125. const pw = Math.min(W * 0.3, 230), ph = 56, gap = 12;
  126. this.balanceLabel = this.makePill(-(pw + gap), y, pw, ph, '💰', `${START_BALANCE}`);
  127. this.betLabel = this.makePill(0, y, pw, ph, '🎯', `${BET}`);
  128. this.winLabel = this.makePill(pw + gap, y, pw, ph, '👑', '0');
  129. const m = new Node('mult'); m.parent = this.node; m.setPosition(0, -H * 0.13 + 70, 0);
  130. this.multLabel = m.addComponent(Label);
  131. this.multLabel.fontSize = 40; this.multLabel.color = new Color(255, 110, 70, 255);
  132. this.multLabel.string = '';
  133. }
  134. private makePill(x: number, y: number, w: number, h: number, icon: string, val: string): Label {
  135. const n = new Node('pill'); n.parent = this.node; n.setPosition(x, y, 0);
  136. n.addComponent(UITransform).setContentSize(w, h);
  137. const g = n.addComponent(Graphics);
  138. g.fillColor = new Color(70, 50, 120, 235);
  139. this.roundRect(g, -w / 2, -h / 2, w, h, h / 2); g.fill();
  140. const il = new Node('i'); il.parent = n; il.setPosition(-w / 2 + 26, 0, 0);
  141. const ic = il.addComponent(Label); ic.string = icon; ic.fontSize = 26;
  142. const vl = new Node('v'); vl.parent = n; vl.setPosition(12, 0, 0);
  143. const lab = vl.addComponent(Label);
  144. lab.string = val; lab.fontSize = 26; lab.color = new Color(255, 255, 255, 255);
  145. return lab;
  146. }
  147. // ---------------- 控制区:TURBO / − / SPIN / + / AUTO ----------------
  148. private buildControls(W: number, H: number) {
  149. const y = -H * 0.36;
  150. this.spinBtn = this.makeRoundButton(0, y, 72, new Color(60, 120, 255, 255), '⟳', 44, () => this.spin());
  151. this.makeRoundButton(-W * 0.34, y, 34, new Color(90, 70, 140, 255), '⚡', 24, () => {});
  152. this.makeRoundButton(-W * 0.2, y, 34, new Color(90, 70, 140, 255), '−', 30, () => {});
  153. this.makeRoundButton(W * 0.2, y, 34, new Color(90, 70, 140, 255), '+', 30, () => {});
  154. this.makeRoundButton(W * 0.34, y, 34, new Color(90, 70, 140, 255), '▶', 24, () => {});
  155. this.makeTextLabel(-W * 0.34, y - 48, 'TURBO', 16);
  156. this.makeTextLabel(W * 0.34, y - 48, 'AUTO', 16);
  157. }
  158. private makeRoundButton(x: number, y: number, r: number, color: Color, glyph: string, fs: number, onClick: () => void): Node {
  159. const node = new Node('btn'); node.parent = this.node; node.setPosition(x, y, 0);
  160. node.addComponent(UITransform).setContentSize(r * 2, r * 2);
  161. const g = node.addComponent(Graphics);
  162. g.fillColor = color; g.circle(0, 0, r); g.fill();
  163. g.lineWidth = 4; g.strokeColor = new Color(255, 255, 255, 235); g.circle(0, 0, r); g.stroke();
  164. const gl = new Node('g'); gl.parent = node;
  165. const lab = gl.addComponent(Label); lab.string = glyph; lab.fontSize = fs; lab.color = new Color(255, 255, 255, 255);
  166. const btn = node.addComponent(Button);
  167. btn.transition = Button.Transition.SCALE; btn.zoomScale = 0.9;
  168. node.on(Node.EventType.TOUCH_END, (_e: EventTouch) => onClick());
  169. return node;
  170. }
  171. private makeTextLabel(x: number, y: number, text: string, fs: number) {
  172. const n = new Node('tl'); n.parent = this.node; n.setPosition(x, y, 0);
  173. const lab = n.addComponent(Label); lab.string = text; lab.fontSize = fs; lab.color = new Color(255, 255, 255, 230);
  174. }
  175. // ---------------- 套用生成的 UI 美术(有就贴,没有就保持代码画)----------------
  176. private loadArt(W: number, H: number) {
  177. // 背景整图:铺满全屏,压在最底层
  178. this.tryArt('bg_main', (sf) => {
  179. const n = this.spriteNode(sf, W, H); n.setPosition(0, 0, 0); n.setSiblingIndex(1);
  180. });
  181. // 卷轴外框:贴到网格区域,盖掉代码框
  182. this.tryArt('reel_frame', (sf) => {
  183. const n = this.spriteNode(sf, this.frameW + 40, this.frameH + 40);
  184. n.setPosition(0, this.frameCY, 0);
  185. const code = this.node.getChildByName('frame'); if (code) code.active = false;
  186. });
  187. // Logo:贴到顶部,隐藏文字 Logo
  188. this.tryArt('logo', (sf) => {
  189. const n = this.spriteNode(sf, 380, 200); n.setPosition(0, H / 2 - 110, 0);
  190. if (this.logoNode) this.logoNode.active = false;
  191. });
  192. // SPIN 按钮贴图
  193. this.tryArt('btn_spin', (sf) => {
  194. const s = this.spinBtn.addComponent(Sprite); s.spriteFrame = sf; s.sizeMode = Sprite.SizeMode.CUSTOM;
  195. const ut = this.spinBtn.getComponent(UITransform)!; ut.setContentSize(150, 150);
  196. const g = this.spinBtn.getComponent(Graphics); if (g) g.clear();
  197. const gl = this.spinBtn.getChildByName('g'); if (gl) gl.active = false;
  198. });
  199. }
  200. private tryArt(id: string, cb: (sf: SpriteFrame) => void) {
  201. resources.load(`ui_art/${id}/spriteFrame`, SpriteFrame, (err, sf) => { if (!err && sf) cb(sf); });
  202. }
  203. private spriteNode(sf: SpriteFrame, w: number, h: number): Node {
  204. const n = new Node('art'); n.parent = this.node;
  205. const ut = n.addComponent(UITransform); ut.setContentSize(w, h);
  206. const s = n.addComponent(Sprite); s.spriteFrame = sf; s.sizeMode = Sprite.SizeMode.CUSTOM;
  207. return n;
  208. }
  209. // ---------------- 网格 ----------------
  210. private buildGrid() {
  211. const tileScale = (this.cell * 0.82) / 1032; // 角色原图约 1032px
  212. for (let c = 0; c < COLS; c++) {
  213. this.cells[c] = []; this.ids[c] = [];
  214. for (let r = 0; r < ROWS; r++) {
  215. const id = this.rand();
  216. const [x, y] = this.cellPos(c, r);
  217. const node = new Node(`cell_${c}_${r}`); node.parent = this.node;
  218. node.setPosition(x, y, 0);
  219. node.setScale(tileScale, tileScale, 1);
  220. const sk = node.addComponent(sp.Skeleton);
  221. sk.skeletonData = this.dataMap[id];
  222. sk.premultipliedAlpha = false;
  223. sk.setAnimation(0, 'idle', true);
  224. this.cells[c][r] = sk; this.ids[c][r] = id;
  225. }
  226. }
  227. }
  228. private setSym(c: number, r: number, id: string) {
  229. const sk = this.cells[c][r];
  230. sk.skeletonData = this.dataMap[id]; sk.premultipliedAlpha = false;
  231. sk.setAnimation(0, 'idle', true);
  232. this.ids[c][r] = id;
  233. const base = (this.cell * 0.82) / 1032;
  234. sk.node.setScale(base * 1.3, base * 1.3, 1);
  235. tween(sk.node).to(0.14, { scale: new Vec3(base, base, 1) }, { easing: 'backOut' }).start();
  236. }
  237. // ---------------- 玩法 ----------------
  238. private spin() {
  239. if (this.spinning) return;
  240. if (this.balance < BET) { this.flashMult('余额不足'); return; }
  241. this.balance -= BET; this.displayBalance = this.balance;
  242. this.balanceLabel.string = `${Math.floor(this.displayBalance)}`;
  243. this.multiplier = 1; this.roundWin = 0;
  244. this.winLabel.string = '0'; this.multLabel.string = '';
  245. this.spinning = true; this.setSpinEnabled(false);
  246. // 全盘随机铺一次(带落入动画)
  247. for (let c = 0; c < COLS; c++)
  248. for (let r = 0; r < ROWS; r++) this.setSym(c, r, this.rand());
  249. this.scheduleOnce(() => this.resolve(), 0.45);
  250. }
  251. // 消除-补位 连锁
  252. private resolve() {
  253. const count: Record<string, number> = {};
  254. for (let c = 0; c < COLS; c++)
  255. for (let r = 0; r < ROWS; r++) count[this.ids[c][r]] = (count[this.ids[c][r]] || 0) + 1;
  256. const winSyms = Object.keys(count).filter((k) => count[k] >= MIN_MATCH);
  257. if (winSyms.length === 0) {
  258. this.endRound();
  259. return;
  260. }
  261. let cleared = 0;
  262. const winners: sp.Skeleton[] = [];
  263. for (let c = 0; c < COLS; c++)
  264. for (let r = 0; r < ROWS; r++)
  265. if (winSyms.indexOf(this.ids[c][r]) >= 0) { winners.push(this.cells[c][r]); cleared++; }
  266. const pay = Math.floor(cleared * (BET / MIN_MATCH) * this.multiplier);
  267. this.roundWin += pay; this.balance += pay;
  268. this.winLabel.string = `${this.roundWin}`;
  269. this.flashMult(`x${this.multiplier} +${pay}`);
  270. winners.forEach((sk) => { sk.setAnimation(0, 'win', false); sk.addAnimation(0, 'idle', true, 0); });
  271. this.playParticle('coin_rain');
  272. // 短暂后:把中奖格替换成新符号(消除→补位),倍数 +1,继续连锁
  273. this.scheduleOnce(() => {
  274. for (let c = 0; c < COLS; c++)
  275. for (let r = 0; r < ROWS; r++)
  276. if (winSyms.indexOf(this.ids[c][r]) >= 0) this.setSym(c, r, this.rand());
  277. this.multiplier += 1;
  278. this.scheduleOnce(() => this.resolve(), 0.4);
  279. }, 0.6);
  280. }
  281. private endRound() {
  282. if (this.roundWin > 0) this.flashMult(`总赢 +${this.roundWin}`);
  283. else this.multLabel.string = '';
  284. this.spinning = false; this.setSpinEnabled(true);
  285. }
  286. private flashMult(text: string) {
  287. this.multLabel.string = text;
  288. const n = this.multLabel.node; n.setScale(0.6, 0.6, 1);
  289. tween(n).to(0.3, { scale: new Vec3(1, 1, 1) }, { easing: 'elasticOut' }).start();
  290. }
  291. private playParticle(id: string) {
  292. resources.load(`vfx/${id}`, JsonAsset, (err, asset) => {
  293. if (err) return;
  294. const n = new Node('p'); n.parent = this.node; n.setPosition(0, view.getVisibleSize().height / 2 - 200, 0);
  295. const ps = n.addComponent(ParticleSystem2D);
  296. applyParticleConfig(ps, asset.json, this.particleTex as SpriteFrame);
  297. this.scheduleOnce(() => ps.stopSystem(), 1.2);
  298. this.scheduleOnce(() => n.destroy(), 4);
  299. });
  300. }
  301. update(dt: number) {
  302. if (Math.abs(this.displayBalance - this.balance) > 0.5) {
  303. this.displayBalance += (this.balance - this.displayBalance) * Math.min(1, dt * 6);
  304. this.balanceLabel.string = `${Math.floor(this.displayBalance)}`;
  305. }
  306. }
  307. private rand() { return SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)]; }
  308. private setSpinEnabled(on: boolean) {
  309. const btn = this.spinBtn.getComponent(Button); if (btn) btn.interactable = on;
  310. this.spinBtn.getChildByName('g')!.getComponent(Label)!.string = on ? '⟳' : '…';
  311. }
  312. private roundRect(g: Graphics, x: number, y: number, w: number, h: number, r: number) {
  313. g.moveTo(x + r, y); g.lineTo(x + w - r, y);
  314. g.arc(x + w - r, y + r, r, -Math.PI / 2, 0, false);
  315. g.lineTo(x + w, y + h - r); g.arc(x + w - r, y + h - r, r, 0, Math.PI / 2, false);
  316. g.lineTo(x + r, y + h); g.arc(x + r, y + h - r, r, Math.PI / 2, Math.PI, false);
  317. g.lineTo(x, y + r); g.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5, false); g.close();
  318. }
  319. }