From aa57deaa582ec2468462c11ca5c66116f0333077 Mon Sep 17 00:00:00 2001 From: tiendat3699 <96950844+tiendat3699@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:13:32 +0700 Subject: [PATCH] feat: add gacha --- assets/_Game/Scripts/Base/GachaBase.ts | 8 + .../Scripts/Base/SpineAnimationHandler.ts | 282 ++++++++++++++++++ .../Scripts/Base/SpineIgnoreTimeScale.ts | 22 ++ assets/_Game/Scripts/Events/GameEvent.ts | 3 + assets/_Game/Scripts/Gacha/FlipCard.ts | 63 ++++ assets/_Game/Scripts/Gacha/FreeReward.ts | 70 +++++ assets/_Game/Scripts/Gacha/LuckyChain.ts | 105 +++++++ assets/_Game/Scripts/Gacha/LuckyWheel.ts | 81 +++++ assets/_Game/Scripts/Manager/GachaManager.ts | 138 +++++++++ settings/v2/packages/engine.json | 3 +- 10 files changed, 774 insertions(+), 1 deletion(-) create mode 100644 assets/_Game/Scripts/Base/GachaBase.ts create mode 100644 assets/_Game/Scripts/Base/SpineAnimationHandler.ts create mode 100644 assets/_Game/Scripts/Base/SpineIgnoreTimeScale.ts create mode 100644 assets/_Game/Scripts/Gacha/FlipCard.ts create mode 100644 assets/_Game/Scripts/Gacha/FreeReward.ts create mode 100644 assets/_Game/Scripts/Gacha/LuckyChain.ts create mode 100644 assets/_Game/Scripts/Gacha/LuckyWheel.ts create mode 100644 assets/_Game/Scripts/Manager/GachaManager.ts diff --git a/assets/_Game/Scripts/Base/GachaBase.ts b/assets/_Game/Scripts/Base/GachaBase.ts new file mode 100644 index 0000000..0844c19 --- /dev/null +++ b/assets/_Game/Scripts/Base/GachaBase.ts @@ -0,0 +1,8 @@ +import { _decorator, Component, Node } from 'cc'; + +const { ccclass, property } = _decorator; + +@ccclass('GachaBase') +export default abstract class GachaBase extends Component { + public abstract show(): void; +} diff --git a/assets/_Game/Scripts/Base/SpineAnimationHandler.ts b/assets/_Game/Scripts/Base/SpineAnimationHandler.ts new file mode 100644 index 0000000..d81870a --- /dev/null +++ b/assets/_Game/Scripts/Base/SpineAnimationHandler.ts @@ -0,0 +1,282 @@ +import { _decorator, Component, Enum, Node, setPropertyEnumType, sp } from 'cc'; + +const { ccclass, property, type } = _decorator; + +export enum SocketPath {} +Enum(SocketPath); + +export enum SpineAnimation {} +Enum(SpineAnimation); + +export enum SpineSkin {} +Enum(SpineSkin); + +@ccclass('SpineAnimationHandler') +export default class SpineAnimationHandler extends Component { + @property(sp.Skeleton) + private skeleton: sp.Skeleton; + @property({ visible: true }) + private _flipX: boolean = false; + @property({ visible: true }) + private _flipY: boolean = false; + + public get flipX() { + return this._flipX; + } + public set flipX(value: boolean) { + if (value) { + this.skeleton._skeleton.scaleX = -Math.abs(this.skeleton._skeleton.scaleX); + } else { + this.skeleton._skeleton.scaleX = Math.abs(this.skeleton._skeleton.scaleX); + } + + this._flipX = value; + } + public get flipY() { + return this._flipY; + } + public set flipY(value: boolean) { + if (value) { + this.skeleton._skeleton.scaleY = -Math.abs(this.skeleton._skeleton.scaleY); + } else { + this.skeleton._skeleton.scaleY = Math.abs(this.skeleton._skeleton.scaleY); + } + + this._flipX = value; + } + + private _enumSocketPath = Enum({}); + private _enumAnimation = Enum({}); + private _enumSkin = Enum({}); + + onFocusInEditor(): void { + this.setPropertySocketPath(this, ''); + } + + protected onLoad(): void { + this.skeleton._skeleton.scaleX = this._flipX ? -this.skeleton._skeleton.scaleX : this.skeleton._skeleton.scaleX; + this.skeleton._skeleton.scaleY = this._flipY ? -this.skeleton._skeleton.scaleY : this.skeleton._skeleton.scaleY; + } + + public setPropertySocketPath(object: object, propertyName: string) { + //update enum SocketPath + let pathEnum = {}; + this.skeleton.querySockets().forEach((path, i) => { + pathEnum[path] = i; + }); + this._enumSocketPath = Enum({}); + Object.assign(this._enumSocketPath, pathEnum); + Enum.update(this._enumSocketPath); + setPropertyEnumType(object, propertyName, this._enumSocketPath); + } + + public setPropertySpineAnimation(object: object, propertyName: string) { + //update enum Animations + let animEnum = this.skeleton.skeletonData.getAnimsEnum(); + this._enumAnimation = Enum({}); + Object.assign(this._enumAnimation, animEnum); + Enum.update(this._enumAnimation); + setPropertyEnumType(object, propertyName, this._enumAnimation); + } + + public setPropertySpineSkin(object: object, propertyName: string) { + //update enum Skin + let skinEnum = this.skeleton.skeletonData.getSkinsEnum(); + this._enumSkin = Enum({}); + Object.assign(this._enumSkin, skinEnum); + Enum.update(this._enumSkin); + setPropertyEnumType(object, propertyName, this._enumSkin); + } + + public setAnimation( + animName: string, + options?: { + loop?: boolean; + trackIndex?: number; + ignoreAnimationRunning?: boolean; + onComplete?: (entry: sp.spine.TrackEntry) => void; + }, + ): sp.spine.TrackEntry { + if (!this.skeleton) return null; + const opts = { + trackIndex: 0, + loop: false, + ...options, + }; + + if (opts.ignoreAnimationRunning) { + const trackEntry = this.skeleton.getCurrent(opts.trackIndex); + const animRunning = trackEntry?.animation?.name; + if (animRunning && animRunning == animName) return trackEntry; + } + + this.skeleton.setCompleteListener(null); + const trackEntry = this.skeleton.setAnimation(opts?.trackIndex || 0, animName, opts?.loop); + if (opts.onComplete) { + this.skeleton.setCompleteListener(opts.onComplete); + } + + return trackEntry; + } + + public setListener(listener: (entry: sp.spine.TrackEntry, e: sp.spine.Event) => void) { + this.skeleton.setEventListener(listener); + } + + public setAnimationAsync( + animName: string, + options?: { + loop?: boolean; + trackIndex?: number; + ignoreAnimationRunning?: boolean; + }, + ): Promise { + return new Promise((resolve) => { + this.setAnimation(animName, { + ...options, + onComplete(entry) { + resolve(entry); + }, + }); + }); + } + + public addAnimation( + animName: string, + options?: { + loop?: boolean; + trackIndex?: number; + mixDuration?: number; + delay?: number; + onComplete?: (entry: sp.spine.TrackEntry) => void; + }, + ): sp.spine.TrackEntry { + if (!this.skeleton) return null; + const opts = { + trackIndex: 0, + loop: false, + ...options, + }; + + this.skeleton.setCompleteListener(null); + const trackEntry = this.skeleton.addAnimation(opts?.trackIndex || 0, animName, opts?.loop, opts?.delay); + + if (opts?.mixDuration) { + trackEntry.mixDuration = opts.mixDuration; + } + + if (opts.onComplete) { + this.skeleton.setCompleteListener(opts.onComplete); + } + + return trackEntry; + } + + public addAnimationAsync( + animName: string, + options?: { + loop?: boolean; + trackIndex?: number; + mixDuration?: number; + delay?: number; + }, + ): Promise { + return new Promise((resolve) => { + this.addAnimation(animName, { + ...options, + onComplete(entry) { + resolve(entry); + }, + }); + }); + } + + public addEmptyAnimation(options?: { + trackIndex?: number; + mixDuration?: number; + delay?: number; + onComplete?: (entry: sp.spine.TrackEntry) => void; + }): sp.spine.TrackEntry { + if (!this.skeleton) return; + const opts = { + trackIndex: 0, + mixDuration: 0.2, + delay: 0.2, + ...options, + }; + + const trackEntry = this.skeleton.getState().addEmptyAnimation(opts.trackIndex, opts.mixDuration, opts.delay); + + if (opts.onComplete) { + this.skeleton.setTrackCompleteListener(trackEntry, opts.onComplete); + } + return trackEntry; + } + + public addEmptyAnimationAsync(options?: { + trackIndex?: number; + mixDuration?: number; + delay?: number; + }): Promise { + return new Promise((resolve) => { + this.addEmptyAnimation({ + ...options, + onComplete(entry) { + resolve(entry); + }, + }); + }); + } + + public clearTrack(trackIndex: number = 0): void { + if (!this.skeleton) return; + this.skeleton.clearTrack(trackIndex); + } + + public findBone(boneName: string): sp.spine.Bone { + return this.skeleton.findBone(boneName); + } + + public socketPathToString(socketPath: SocketPath): string { + return this.skeleton.querySockets()[socketPath]; + } + + public SpineAnimationToString(animationName: SpineAnimation): string { + return this.skeleton._skeleton.data.animations[animationName].name; + } + + public SpineAnimationToAnimation(animationName: SpineAnimation): sp.spine.Animation { + return this.skeleton._skeleton.data.animations[animationName]; + } + + public addSocket(socketPath: SocketPath | string, target: Node): sp.SpineSocket { + let socket: sp.SpineSocket; + if (typeof socketPath === 'string') { + socket = new sp.SpineSocket(socketPath, target); + } else { + socket = new sp.SpineSocket(this.socketPathToString(socketPath), target); + } + this.skeleton.sockets.push(socket); + this.skeleton!.sockets = this.skeleton!.sockets; + return socket; + } + + public updateSocketPath(socket: sp.SpineSocket, socketPath: SocketPath | string): sp.SpineSocket { + let newSocket: sp.SpineSocket; + if (typeof socketPath === 'string') { + newSocket = new sp.SpineSocket(socketPath, socket.target); + } else { + newSocket = new sp.SpineSocket(this.socketPathToString(socketPath), socket.target); + } + const index = this.skeleton.sockets.indexOf(socket); + this.skeleton.sockets[index] = newSocket; + this.skeleton!.sockets = this.skeleton!.sockets; + return newSocket; + } + + public removeSocket(socket: sp.SpineSocket): void { + const index = this.skeleton.sockets.indexOf(socket); + this.skeleton.sockets.splice(index, 1); + this.skeleton!.sockets = this.skeleton!.sockets; + } +} diff --git a/assets/_Game/Scripts/Base/SpineIgnoreTimeScale.ts b/assets/_Game/Scripts/Base/SpineIgnoreTimeScale.ts new file mode 100644 index 0000000..643a65b --- /dev/null +++ b/assets/_Game/Scripts/Base/SpineIgnoreTimeScale.ts @@ -0,0 +1,22 @@ +import { _decorator, CCBoolean, Component, game, Node, sp } from 'cc'; + +const { ccclass, property, requireComponent } = _decorator; + +@ccclass('SpineIgnoreTimeScale') +@requireComponent(sp.Skeleton) +export default class SpineIgnoreTimeScale extends Component { + @property(CCBoolean) + private ignoreTimeScale: boolean = false; + + private _skeleton: sp.Skeleton; + + protected onLoad(): void { + this._skeleton = this.getComponent(sp.Skeleton); + } + + protected update(dt: number): void { + if (game.timeScale != 1) { + this._skeleton.updateAnimation(this.ignoreTimeScale ? game.deltaTime : dt); + } + } +} diff --git a/assets/_Game/Scripts/Events/GameEvent.ts b/assets/_Game/Scripts/Events/GameEvent.ts index fe05bdc..748a66c 100644 --- a/assets/_Game/Scripts/Events/GameEvent.ts +++ b/assets/_Game/Scripts/Events/GameEvent.ts @@ -3,6 +3,7 @@ import BoosterType from '../Enum/BoosterType'; import ControllerSide from '../Enum/ControllerSide'; import GameState from '../Enum/GameState'; import ScoreType from '../Enum/ScoreType'; +import { RewardType } from '../Manager/GachaManager'; enum GameEvent { Score, @@ -17,6 +18,7 @@ enum GameEvent { ControlTouchEnd, WarningTime, TicketUpdate, + GachaReward, } export interface GameEventCallbackMap { @@ -47,6 +49,7 @@ export interface GameEventArgMap { [GameEvent.ControlTouchEnd]: ControllerSide; [GameEvent.WarningTime]: boolean; [GameEvent.TicketUpdate]: number; + [GameEvent.GachaReward]: [RewardType, number]; } export default GameEvent; diff --git a/assets/_Game/Scripts/Gacha/FlipCard.ts b/assets/_Game/Scripts/Gacha/FlipCard.ts new file mode 100644 index 0000000..5739de0 --- /dev/null +++ b/assets/_Game/Scripts/Gacha/FlipCard.ts @@ -0,0 +1,63 @@ +import { _decorator, Component, Node, Sprite } from 'cc'; +import GachaBase from '../Base/GachaBase'; +import SpineAnimationHandler from '../Base/SpineAnimationHandler'; +import GachaManager from '../Manager/GachaManager'; + +const { ccclass, property } = _decorator; + +@ccclass('FlipCard') +export default class FlipCard extends GachaBase { + @property(SpineAnimationHandler) + private animationHandler: SpineAnimationHandler; + @property(Node) + private spineRoot: Node; + @property(Sprite) + private cards: Sprite[] = []; + + private _opened: boolean = false; + + protected onLoad(): void { + this.animationHandler.setListener((_, e) => { + switch (e.data.name) { + case 'card1-active': + this.cards[0].setNodeActive(true); + break; + case 'card2-active': + this.cards[1].setNodeActive(true); + break; + case 'card3-active': + this.cards[2].setNodeActive(true); + break; + case 'card4-active': + this.cards[3].setNodeActive(true); + break; + } + }); + } + + protected onEnable(): void { + this._opened = false; + this.cards.forEach((card) => card.setNodeActive(false)); + this.spineRoot.setActive(false); + } + + public async show(): Promise { + this.spineRoot.setActive(true); + await this.animationHandler.setAnimationAsync('appear'); + this.animationHandler.addAnimation('idle', { loop: true }); + } + + public async open(event: Event, value: string) { + if (this._opened) return; + this._opened = true; + if (this.cards[+value - 1].node.active) return; + const reward = await GachaManager.instance.getReward(); + if (reward) { + this.cards[+value - 1].spriteFrame = reward.icon; + await this.animationHandler.setAnimationAsync(`card${value}-active`, { trackIndex: +value }); + GachaManager.instance.gachaDone(); + } + + GachaManager.instance.gachaDone(); + } +} diff --git a/assets/_Game/Scripts/Gacha/FreeReward.ts b/assets/_Game/Scripts/Gacha/FreeReward.ts new file mode 100644 index 0000000..d05a592 --- /dev/null +++ b/assets/_Game/Scripts/Gacha/FreeReward.ts @@ -0,0 +1,70 @@ +import { _decorator, clamp01, Component, easing, game, Label, Node, Sprite, Vec3 } from 'cc'; +import GachaBase from '../Base/GachaBase'; +import SpineAnimationHandler from '../Base/SpineAnimationHandler'; +import GachaManager from '../Manager/GachaManager'; +import Utils from '../Utilities'; + +const { ccclass, property } = _decorator; + +@ccclass('FreeReward') +export default class FreeReward extends GachaBase { + @property(SpineAnimationHandler) + private animationHandler: SpineAnimationHandler; + @property(Node) + private spineRoot: Node; + @property(Sprite) + private rewardSprite: Sprite; + @property(Label) + private rewardLabel: Label; + + private _opened: boolean = false; + private _startShowReward: boolean = false; + private _timer: number = 0; + + protected onEnable(): void { + this.spineRoot.setActive(false); + this.rewardSprite.setNodeActive(false); + this._opened = false; + } + + protected update(dt: number): void { + if (this._startShowReward) { + let k = clamp01(this._timer / 0.3); + k = easing.quintInOut(k); + const targetScale = Vec3.lerp(this.rewardSprite.node.scale, Vec3.ZERO, Vec3.ONE, k); + const targetPosition = Vec3.lerp(this.rewardSprite.node.position, new Vec3(0, 150), new Vec3(0, 700), k); + this.rewardSprite.node.setScale(targetScale); + this.rewardSprite.node.setPosition(targetPosition); + this._timer += game.deltaTime; + if (k === 1) { + this._startShowReward = false; + GachaManager.instance.gachaDone(); + } + } + } + + public async show() { + this.spineRoot.setActive(true); + await this.animationHandler.setAnimationAsync('appear'); + this.animationHandler.addAnimation('idle', { loop: true }); + } + + public async open() { + if (this._opened) return; + this._timer = 0; + this._opened = true; + const reward = await GachaManager.instance.getReward(); + if (reward) { + this.animationHandler.setAnimation('open'); + this.rewardSprite.spriteFrame = reward.icon; + this.rewardLabel.string = reward.quantity.toString(); + await Utils.delay(1); + this.rewardSprite.setNodeActive(true); + this._startShowReward = true; + return; + } + + this._startShowReward = false; + GachaManager.instance.gachaDone(); + } +} diff --git a/assets/_Game/Scripts/Gacha/LuckyChain.ts b/assets/_Game/Scripts/Gacha/LuckyChain.ts new file mode 100644 index 0000000..478a11b --- /dev/null +++ b/assets/_Game/Scripts/Gacha/LuckyChain.ts @@ -0,0 +1,105 @@ +import { _decorator, Component, Node, sp, Sprite } from 'cc'; +import GachaBase from '../Base/GachaBase'; +import SpineAnimationHandler, { SocketPath, SpineAnimation, SpineSkin } from '../Base/SpineAnimationHandler'; +import GachaManager from '../Manager/GachaManager'; +import Utils from '../Utilities'; + +const { ccclass, property } = _decorator; + +class ActiveCard { + public socket: sp.SpineSocket; + public node: Node; + public idReward: string; + + constructor(node: Node, idReward: string) { + this.node = node; + this.idReward = idReward; + } + + public setActive(value: boolean) { + this.node.setActive(value); + } +} + +@ccclass('LuckyChain') +export default class LuckyChain extends GachaBase { + @property(SpineAnimationHandler) + private animationHandler: SpineAnimationHandler; + @property(Node) + private spriteRoot: Node; + @property(Node) + private cards: Node[] = []; + @property({ type: SocketPath }) + private cardSlotPath: SocketPath[] = []; + + private _activeCards: ActiveCard[] = []; + private _allCards: ActiveCard[] = []; + private _rewardId: string; + + onFocusInEditor(): void { + this.animationHandler.setPropertySocketPath(this, 'cardSlotPath'); + } + + protected onLoad(): void { + this.setReward(); + + for (let i = 0; i < 4; i++) { + const card = this._allCards.shift(); + const path = this.cardSlotPath[i]; + const socket = this.animationHandler.addSocket(path, card.node); + card.setActive(true); + card.socket = socket; + this._activeCards.push(card); + } + + this.animationHandler.setListener((_, e) => { + switch (e.data.name) { + case 'card-claimed': + const cardRemove = this._activeCards.shift(); + this._rewardId = cardRemove.idReward; + this.animationHandler.removeSocket(cardRemove.socket); + cardRemove.setActive(false); + cardRemove.socket = null; + this._allCards.push(cardRemove); + break; + case 'new-card-spawned': + const card = this._allCards.shift(); + const path = this.cardSlotPath[this.cardSlotPath.length - 1]; + const socket = this.animationHandler.addSocket(path, card.node); + card.setActive(true); + card.socket = socket; + this._activeCards.push(card); + break; + } + }); + } + + protected onEnable(): void { + this.spriteRoot.setActive(false); + } + + private setReward() { + this._allCards = this.cards.map((card) => { + const rw = GachaManager.instance.getRandomReward(); + card.getComponent(Sprite).spriteFrame = rw.icon; + card.setActive(false); + return new ActiveCard(card, rw.id); + }); + } + + public async show(): Promise { + this.spriteRoot.setActive(true); + await this.animationHandler.setAnimationAsync('appear'); + await this.animationHandler.addAnimationAsync('active'); + + GachaManager.instance.setReward(this._rewardId); + + await Utils.delay(1.5); + //update socket path + for (let i = 0; i < 4; i++) { + const card = this._activeCards[i]; + const path = this.cardSlotPath[i]; + this._activeCards[i].socket = this.animationHandler.updateSocketPath(card.socket, path); + } + } +} diff --git a/assets/_Game/Scripts/Gacha/LuckyWheel.ts b/assets/_Game/Scripts/Gacha/LuckyWheel.ts new file mode 100644 index 0000000..74f72f6 --- /dev/null +++ b/assets/_Game/Scripts/Gacha/LuckyWheel.ts @@ -0,0 +1,81 @@ +import { _decorator, CCInteger, CCString, Component, game, Node, randomRangeInt, RealCurve, sp, Sprite } from 'cc'; +import GachaBase from '../Base/GachaBase'; +import SpineAnimationHandler from '../Base/SpineAnimationHandler'; +import GachaManager from '../Manager/GachaManager'; + +const { ccclass, property } = _decorator; + +@ccclass('LuckyWheel') +export default class LuckyWheel extends GachaBase { + @property(SpineAnimationHandler) + private animationHandler: SpineAnimationHandler; + @property(Node) + private spineRoot: Node; + @property(CCString) + private wheelBoneName: string = ''; + @property(CCInteger) + private speed: number = 1; + @property(RealCurve) + private spinCurve: RealCurve = new RealCurve(); + @property(Sprite) + private sprites: Sprite[] = []; + + private _wheel: sp.spine.Bone; + private _targetAngle: number = 0; + private _spinning: boolean = false; + private _timer: number = 0; + private _timeSpin: number = 0; + private _maxAngle: number = 0; + + protected onLoad(): void { + this._wheel = this.animationHandler.findBone(this.wheelBoneName); + } + + protected onEnable(): void { + this.spineRoot.setActive(false); + this._wheel.rotation = 0; + this._wheel.update(); + } + + protected update(): void { + if (this._spinning) { + this._timer += game.deltaTime * this.speed; + const angle = this._maxAngle * this.spinCurve.evaluate(this._timer / this._timeSpin); + this._wheel.rotation = this._targetAngle + angle; + this._wheel.update(); + if (this._timer >= this._timeSpin) { + this._spinning = false; + GachaManager.instance.gachaDone(); + } + } + } + + public async show(): Promise { + for (let i = 0; i < GachaManager.instance.rewards.length; i++) { + this.sprites[i].spriteFrame = GachaManager.instance.rewards[i].icon; + } + this.spineRoot.setActive(true); + await this.animationHandler.setAnimationAsync('appear'); + + this.animationHandler.addAnimation('idle', { loop: true }); + } + + public async spin() { + if (this._spinning) return; + this._spinning = true; + const reward = await GachaManager.instance.getReward(); + const item = GachaManager.instance.getRewardIndex(reward); + if (reward) { + this.animationHandler.clearTrack(0); + this._targetAngle = -36 * item; + this._timer = 0; + this._timeSpin = randomRangeInt(10, 15); + this._maxAngle = 360 * this._timeSpin; + + return; + } + + this._spinning = true; + GachaManager.instance.gachaDone(); + } +} diff --git a/assets/_Game/Scripts/Manager/GachaManager.ts b/assets/_Game/Scripts/Manager/GachaManager.ts new file mode 100644 index 0000000..433dd89 --- /dev/null +++ b/assets/_Game/Scripts/Manager/GachaManager.ts @@ -0,0 +1,138 @@ +import { + _decorator, + CCInteger, + CCString, + clamp01, + Component, + easing, + Enum, + game, + lerp, + Node, + SpriteFrame, + tween, + UIOpacity, +} from 'cc'; +import GachaBase from '../Base/GachaBase'; +import GameEvent from '../Events/GameEvent'; +import Singleton from '../Singleton'; +import Utils from '../Utilities'; +import { EventManger } from './EventManger'; + +const { ccclass, property } = _decorator; + +export enum GachaType { + FreeReward, + LuckyWheel, + LuckyChain, + FlipCard, +} +Enum(GachaType); + +export enum RewardType { + Oxi, + Gas, + Star, + Shield, + ScoreX2, + Magnet, +} +Enum(RewardType); + +@ccclass('Gacha') +class Gacha { + @property(CCString) + public id: string = ''; + @property({ type: GachaType }) + public type: GachaType = GachaType.FreeReward; + @property(GachaBase) + public gacha: GachaBase; +} + +@ccclass('Reward') +class RewardConfig { + @property(CCString) + public id: string = ''; + @property({ type: RewardType }) + public type: RewardType = RewardType.Oxi; + @property(SpriteFrame) + public icon: SpriteFrame; + @property(CCInteger) + public quantity: number; +} + +@ccclass('GachaManager') +export default class GachaManager extends Singleton() { + @property(CCString) + public gachaId: string; + @property(UIOpacity) + private container: UIOpacity; + @property(Gacha) + private gachas: Gacha[] = []; + @property(RewardConfig) + public rewards: RewardConfig[] = []; + + private _reward: RewardConfig; + private _showing: boolean = false; + private _showTimer: number = 0; + private _idType: string; + + protected onLoad(): void { + super.onLoad(); + this.container.setNodeActive(false); + } + + public show(idType: string) { + game.timeScale = 0; + this.container.setNodeActive(true); + this._showTimer = 0; + this._showing = true; + this._idType = idType; + } + + protected update(dt: number): void { + if (this._showing) { + this._showTimer += game.deltaTime; + const k = easing.smooth(clamp01(this._showTimer / 0.2)); + this.container.opacity = lerp(0, 255, k); + if (k === 1) { + this._showing = false; + this.showGacha(); + } + } + } + + private showGacha() { + const gacha = this.gachas.find((gacha) => gacha.id == this._idType); + gacha.gacha.show(); + } + + public async getReward(): Promise { + this._reward = this.getRandomReward(); + return this._reward; + } + + public getRewardIndex(reward: RewardConfig): number { + return this.rewards.indexOf(reward); + } + + public async gachaDone() { + console.log(`Gacha reward: ${RewardType[this._reward.type]} quantity: ${this._reward.quantity}`); + EventManger.instance.emit(GameEvent.GachaReward, [this._reward.type, this._reward.quantity]); + await Utils.delay(1); + game.timeScale = 1; + tween(this.container) + .to(0.1, { opacity: 0 }) + .call(() => this.container.setNodeActive(false)) + .start(); + } + + public getRandomReward() { + return this.rewards.getRandom(); + } + + public setReward(id: string) { + this._reward = this.rewards.find((r) => r.id == id); + this.gachaDone(); + } +} diff --git a/settings/v2/packages/engine.json b/settings/v2/packages/engine.json index b2701eb..5ed2127 100644 --- a/settings/v2/packages/engine.json +++ b/settings/v2/packages/engine.json @@ -110,7 +110,7 @@ "_value": false }, "spine": { - "_value": false + "_value": true }, "dragon-bones": { "_value": false @@ -138,6 +138,7 @@ "particle-2d", "physics-2d-box2d", "profiler", + "spine", "tween", "ui", "websocket"