import { _decorator, AudioClip, CCInteger, Color, EPhysics2DDrawFlags, Node, PhysicsSystem2D, Quat, randomRangeInt, SpriteFrame, Vec2, Vec3, } from 'cc'; import { EDITOR, PREVIEW } from 'cc/env'; import Timer, { TimerType } from '../Base/Timer'; import { BoosterBase } from '../Booster/BoosterBase'; import BoosterType from '../Enum/BoosterType'; import GameState from '../Enum/GameState'; import ScoreType from '../Enum/ScoreType'; import TimeConfig from '../Enum/TimeConfig'; import GameEvent from '../Events/GameEvent'; import BallFactory from '../Factory/BallFactory'; import FloatingTextFactory from '../Factory/FloatingTextFactory'; import { Ball } from '../GamePlay/Ball'; import P4PSDK from '../P4PSDK'; import Singleton from '../Singleton'; import Utils from '../Utilities'; import AudioManager from './AudioManager'; import { EventManger } from './EventManger'; import { StickerManager } from './StickerManager'; const { ccclass, property } = _decorator; @ccclass('GameManager') export class GameManager extends Singleton() { @property({ visible: true }) private _colliderDebug: boolean = false; @property({ type: FloatingTextFactory, visible: true }) private _floatingScoreFactory: FloatingTextFactory; @property({ type: Node, visible: true }) private _topContainer: Node; @property({ type: Node, visible: true }) private _ballHolder: Node; @property({ visible: true }) private _ballSpawnPosition: Vec3 = new Vec3(); @property({ type: CCInteger, visible: true }) private readonly _timePlay = 120; @property({ type: SpriteFrame, visible: true }) private _clockIcon: SpriteFrame; @property({ type: AudioClip, visible: true }) private _startSound: AudioClip; @property({ type: AudioClip, visible: true }) private _ballOutSound: AudioClip; @property({ type: AudioClip, visible: true }) private _backgroundMusic: AudioClip; @property({ type: AudioClip, visible: true }) private _gameOverMusic: AudioClip; private _gameState: GameState; private _timer: Timer = new Timer(TimerType.countDown); private _activeBoosters: Map = new Map(); private _score = 0; private isReplayed = false; private _isMultiBall = false; private _warningTime = false; private _currentBallInGame = 0; private _isWaitingUpdateScore = false; public get isWaitingUpdateScore() { return this._isWaitingUpdateScore; } public get topContainer() { return this._topContainer; } public get score() { return this._score; } public get gameTime() { return this._timePlay; } public get gameState() { return this._gameState; } protected onLoad(): void { super.onLoad(); if (this._colliderDebug) PhysicsSystem2D.instance.debugDrawFlags = EPhysics2DDrawFlags.Shape; } protected async start(): Promise { await P4PSDK.init(this.onBoughtTicket, this); await P4PSDK.authenticate(); this.changeGameState(GameState.Init); } protected update(dt: number): void { this._timer.update(dt); if (this._gameState != GameState.Playing) return; this.runBooster(dt); } private onBoughtTicket() { this.gameRelive(); EventManger.instance.emit(GameEvent.TicketUpdate, P4PSDK.getUserTicket()); } private async changeGameState(state: GameState) { this._gameState = state; EventManger.instance.emit(GameEvent.GameStateChange, this._gameState); switch (state) { case GameState.Init: break; case GameState.Ready: break; case GameState.Playing: this.countTime(); await P4PSDK.minusTicket('auth'); EventManger.instance.emit(GameEvent.TicketUpdate, P4PSDK.getUserTicket()); break; case GameState.GameOver: break; case GameState.End: break; case GameState.Relive: await P4PSDK.minusTicket('revive'); EventManger.instance.emit(GameEvent.TicketUpdate, P4PSDK.getUserTicket()); break; default: throw new Error(`Argument Out Of Range Exception: ${GameState[state]}`); } } public addScore( score: number, type: ScoreType, position?: Vec3, opts?: { scaleMin?: number; scaleMax?: number; duration?: number }, ) { this._score += score; P4PSDK.updateScore(this.score); const floatingScore = this._floatingScoreFactory.create(this._topContainer); if (position) { floatingScore.show( `+${score}`, position, score >= 100 ? opts?.scaleMax || 1 : opts?.scaleMin || 1, opts?.duration || 1, ); } EventManger.instance.emit(GameEvent.Score, [this._score, score, type, position]); } public async addScoreWithWaiting( score: number, type: ScoreType, position: Vec3, predicate: () => boolean, opts: { scaleMin: number; scaleMax: number; duration: number }, ) { this._isWaitingUpdateScore = true; await Utils.waitUntil(predicate); this._score += score; P4PSDK.updateScore(this.score); const floatingScore = this._floatingScoreFactory.create(this._topContainer); floatingScore.show(`+${score}`, position, score >= 100 ? opts.scaleMax : opts.scaleMin, opts.duration); EventManger.instance.emit(GameEvent.Score, [this._score, score, type, position]); this._isWaitingUpdateScore = false; } private async countTime() { while (this._gameState == GameState.Playing) { if (this._timer.time <= 0) { this._timer.time = 0; this._timer.stopCount(); this.gameOver(); } if (!this._warningTime && this._timer.time <= 10) { this._warningTime = true; EventManger.instance.emit(GameEvent.WarningTime, true); } EventManger.instance.emit(GameEvent.TimeUpdate, this._timer.timeRound); await Utils.delay(1); } } private setCurrentBallInGame(value: number) { this._currentBallInGame += value; if (value > 0 && this._currentBallInGame >= 2) { this._isMultiBall = true; EventManger.instance.emit(GameEvent.MultiBall, true); BallFactory.instance.listActive.forEach((ball) => ball.getComponent(Ball).playMultiBallEffect()); } if (this._currentBallInGame <= 0) { if (this._isMultiBall) { this._isMultiBall = false; EventManger.instance.emit(GameEvent.MultiBall, false); } } } public spawnBall(throwBall: boolean, playStartSound: boolean = true): Ball { if (this._gameState != GameState.Playing) return; if (playStartSound) AudioManager.playSfx(this._startSound); this.setCurrentBallInGame(1); const ball = BallFactory.instance.create(this._ballHolder); this._activeBoosters.forEach((_, type) => { ball.addBoosterEffect(type); }); ball.node.setRotation(Quat.IDENTITY); ball.node.setPosition(this._ballSpawnPosition); if (!throwBall) return ball; let dir = randomRangeInt(-1, 2); while (dir == 0) { dir = randomRangeInt(-1, 2); } const force = new Vec2(dir, 1); ball.throwBall(force.multiply2f(1, 40)); return ball; } public async ballOut() { this.setCurrentBallInGame(-1); if (this._currentBallInGame <= 0) { EventManger.instance.emit(GameEvent.BallOut, null); AudioManager.playSfx(this._ballOutSound); this.cleanBooster(); await Utils.delay(TimeConfig.DelayPLay); this.spawnBall(true); } } public async goal(bonusScore: number, position: Vec3) { this.addScore(this._isMultiBall ? bonusScore * 2 : bonusScore, ScoreType.Goal, position, { scaleMin: 2, scaleMax: 3, duration: 1, }); this.setCurrentBallInGame(-1); if (this._currentBallInGame <= 0) { await Utils.delay(TimeConfig.DelayGoal); this.spawnBall(true); } } public async destroyEnvironmentObject(bonusScore: number, position: Vec3, bonusTime?: number) { if (bonusScore) { this.addScore(bonusScore, ScoreType.DestroyObject, position, { scaleMin: 1.5, scaleMax: 2, duration: 0.7, }); await Utils.delay(0.3); } if (bonusTime) { this.addTime(bonusTime, position); } } public addTime(time: number, position?: Vec3) { this._timer.time += time; if (this._warningTime && this._timer.time > 10) { this._warningTime = false; EventManger.instance.emit(GameEvent.WarningTime, false); } if (position) { const floatingScore = this._floatingScoreFactory.create(this._topContainer); floatingScore.show(`+${time}`, position, 1.5, 1, this._clockIcon); } EventManger.instance.emit(GameEvent.TimeUpdate, this._timer.timeRound); } public async gameOver() { BallFactory.instance.releaseAll(); this.cleanBooster(); AudioManager.playBGM(this._gameOverMusic); StickerManager.instance.showLabel('TIME UP!!!', { color: new Color('#ed3a18'), outLineColor: Color.WHITE }); if (this.isReplayed) { this.changeGameState(GameState.End); return; } this.isReplayed = true; this.changeGameState(GameState.GameOver); } public Ready() { AudioManager.playBGM(this._backgroundMusic); this.changeGameState(GameState.Ready); } public async replay(): Promise { if (!PREVIEW && !EDITOR) { if (P4PSDK.canRelive()) { const success = await P4PSDK.minusTicket('revive'); if (success) { this.gameRelive(); } else { this.gameOver(); return; } } else { P4PSDK.callPayPalModal(); } } else { this.gameRelive(); } } public async play() { this._timer.time = this._timePlay; this._score = 0; this._currentBallInGame = 0; this._isMultiBall = false; this.changeGameState(GameState.Playing); await Utils.delay(TimeConfig.DelayPLay); this._timer.startCount(); this.spawnBall(true); } public async gameRelive() { this.changeGameState(GameState.Relive); this._timer.time = this._timePlay; this._currentBallInGame = 0; this._isMultiBall = false; AudioManager.playBGM(this._backgroundMusic); this.changeGameState(GameState.Playing); await Utils.delay(TimeConfig.DelayPLay); this._timer.startCount(); this.spawnBall(true); } public addBooster(booster: BoosterBase) { let activeBooster = this._activeBoosters.get(booster.type); if (activeBooster) { booster.dispose(); activeBooster.resetTime(); return; } else { activeBooster = booster; } activeBooster.collect(this.node); console.log(booster.displayName + ' active'); this._activeBoosters.set(booster.type, booster); EventManger.instance.emit(GameEvent.BoosterActive, [booster.type, booster.displayName]); } private cleanBooster() { this._activeBoosters.forEach((booster) => { booster.end(); EventManger.instance.emit(GameEvent.BoosterDisable, booster.type); }); this._activeBoosters.clear(); } private runBooster(dt: number) { if (this._activeBoosters.size > 0) { const boosterToRemove: BoosterBase[] = []; this._activeBoosters.forEach((booster) => { booster.tick(dt); if (!booster.active) { boosterToRemove.push(booster); } }); boosterToRemove.forEach((booster) => { booster.end(); EventManger.instance.emit(GameEvent.BoosterDisable, booster.type); this._activeBoosters.delete(booster.type); console.log(booster.displayName + ' inactive'); }); } } }