pinball/assets/_Game/Scripts/Manager/GameManager.ts

383 lines
13 KiB
TypeScript

import {
_decorator,
AudioClip,
CCInteger,
Color,
EPhysics2DDrawFlags,
Node,
PhysicsSystem2D,
Quat,
randomRangeInt,
SpriteFrame,
tween,
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<GameManager>() {
@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<BoosterType, BoosterBase> = 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<void> {
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<void> {
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');
});
}
}
}