diff --git a/public/assets/gui/wave.png b/public/assets/gui/wave.png new file mode 100644 index 0000000..8c8f1e3 Binary files /dev/null and b/public/assets/gui/wave.png differ diff --git a/public/assets/missions/mission_01.json b/public/assets/missions/mission_01.json index 67f5e30..9b95751 100644 --- a/public/assets/missions/mission_01.json +++ b/public/assets/missions/mission_01.json @@ -103,6 +103,26 @@ } ], "offeredGems": [0, 1, 2, 3] + }, + { + "waves": [ + { + "firstCreepSpawnTick": 500, + "spawnIntervalTicks": 1000, + "creeps": [0] + } + ], + "offeredGems": [0, 1, 2, 3] + }, + { + "waves": [ + { + "firstCreepSpawnTick": 500, + "spawnIntervalTicks": 1000, + "creeps": [0, 0] + } + ], + "offeredGems": [0, 1, 2, 3] } ] } diff --git a/src/classes/Assets.ts b/src/classes/Assets.ts index a03d33a..802e1f2 100644 --- a/src/classes/Assets.ts +++ b/src/classes/Assets.ts @@ -49,6 +49,9 @@ export default class GameAssets { GameAssets.GoldTexture = await PIXI.Assets.load({ src: '/assets/gui/money.png', }); + GameAssets.WaveTexture = await PIXI.Assets.load({ + src: '/assets/gui/wave.png', + }); GameAssets.BasicCreepTexture = await PIXI.Assets.load({ src: '/assets/creeps/basic.jpg', @@ -126,6 +129,7 @@ export default class GameAssets { public static Button02Texture: PIXI.Texture; public static HealthTexture: PIXI.Texture; public static GoldTexture: PIXI.Texture; + public static WaveTexture: PIXI.Texture; public static MissionBackgrounds: PIXI.Texture[] = []; public static TowerSprites: PIXI.Texture[] = []; diff --git a/src/classes/Bastion.ts b/src/classes/Bastion.ts index 0484c2f..ec5590d 100644 --- a/src/classes/Bastion.ts +++ b/src/classes/Bastion.ts @@ -7,6 +7,7 @@ import WaveManager from './game/WaveManager'; import TowerManager from './game/TowerManager'; import { GameScene } from '../scenes/Game'; import { AnimationManager } from './game/AnimationManager'; +import NotificationManager from './game/NotificationManager'; export class Engine { public static app: PIXI.Application; @@ -18,6 +19,7 @@ export class Engine { public static WaveManager: WaveManager; public static TowerManager: TowerManager; public static AnimationManager: AnimationManager; + public static NotificationManager: NotificationManager; public static GameScene: GameScene; public static latestCommit: string; } diff --git a/src/classes/GameObject.ts b/src/classes/GameObject.ts index e90b0bc..7b7963b 100644 --- a/src/classes/GameObject.ts +++ b/src/classes/GameObject.ts @@ -31,5 +31,21 @@ export default abstract class GameObject { return this.bb; } + public copyBBToContainer() { + this.container.x = this.bb.x; + this.container.y = this.bb.y; + this.container.width = this.bb.width; + this.container.height = this.bb.height; + return this.container; + } + + public copyPropertiesToObj(obj: PIXI.Container) { + obj.x = this.bb.x; + obj.y = this.bb.y; + obj.width = this.bb.width; + obj.height = this.bb.height; + return obj; + } + public abstract update(elapsedMS): void; } diff --git a/src/classes/game/AnimationManager.ts b/src/classes/game/AnimationManager.ts index cdb8e70..416fbbb 100644 --- a/src/classes/game/AnimationManager.ts +++ b/src/classes/game/AnimationManager.ts @@ -2,14 +2,17 @@ import * as PIXI from 'pixi.js'; class Animateable { public finished: boolean = false; - protected callbackFn: Function; + protected calledBack: boolean = false; + public callbackFn: Function; public Finish() { this.finished = true; } public update(ms) { - if (this.finished) return this.callbackFn(); + if (this.finished) { + return; + } } } @@ -34,14 +37,14 @@ export class FadeInOut extends Animateable { public update(ms) { super.update(ms); + if (this.pixiObject == null) return this.Finish(); this.ticks++; if (this.fadeType == 'in') { this.pixiObject.alpha = this.ticks / this.fadeTime; } else { this.pixiObject.alpha -= 1 / this.fadeTime; } - console.log(this.pixiObject.alpha); - if (this.ticks >= this.fadeTime) this.Finish(); + if (this.ticks >= this.fadeTime || this.pixiObject.alpha <= 0) this.Finish(); } } @@ -52,32 +55,31 @@ export class Tween extends Animateable { private goalY: number; private ticks: number = 0; - constructor(timeInFrames: number, object: PIXI.Container, fromX, fromY, goalX, goalY, callbackFn: Function) { + constructor(timeInFrames: number, object: PIXI.Container, goalX, goalY, callbackFn: Function) { super(); this.tweenTime = timeInFrames; this.pixiObject = object; this.callbackFn = callbackFn; this.goalX = goalX; this.goalY = goalY; - this.pixiObject.x = fromX; - this.pixiObject.y = fromY; } - public update(ms) { - super.update(ms); - this.ticks++; - const objX = this.pixiObject.x; - const objY = this.pixiObject.y; - // TODO: fix this by the time you get to using it, it moves the obj too fast and wrong - if (objX != this.goalX) { - let diff = this.goalX - objX; - this.pixiObject.x += ms * diff * (this.ticks / this.tweenTime); + public update(deltaMS) { + super.update(deltaMS); + this.ticks += deltaMS; + // Calculate the fraction of time elapsed + const progress = this.ticks / (this.tweenTime * 16.67); // Assuming 60 FPS, 1 frame = 16.67ms + + // Update the position based on the progress + this.pixiObject.x = (this.goalX - this.pixiObject.x) * progress + this.pixiObject.x; + this.pixiObject.y = (this.goalY - this.pixiObject.y) * progress + this.pixiObject.y; + + // Finish the animation if the time is up + if (this.ticks >= this.tweenTime * 16.67) { + this.pixiObject.x = this.goalX; + this.pixiObject.y = this.goalY; + this.Finish(); } - if (objY != this.goalY) { - let diff = this.goalY - objY; - this.pixiObject.y += ms * diff * (this.ticks / this.tweenTime); - } - if (this.ticks >= this.tweenTime) this.Finish(); } } @@ -87,9 +89,12 @@ export class AnimationManager { this.AnimationQueue.push(animatable); } public update(ms) { - this.AnimationQueue.forEach((anim) => { - if (anim.finished) this.AnimationQueue.splice(this.AnimationQueue.indexOf(anim), 1); - anim.update(ms); - }); + for (let i = this.AnimationQueue.length - 1; i >= 0; i--) { + const anim = this.AnimationQueue[i]; + if (anim.finished) { + anim.callbackFn(); + this.AnimationQueue.splice(i, 1); + } else anim.update(ms); + } } } diff --git a/src/classes/game/MissionStats.ts b/src/classes/game/MissionStats.ts index e985412..63fea9a 100644 --- a/src/classes/game/MissionStats.ts +++ b/src/classes/game/MissionStats.ts @@ -2,12 +2,14 @@ import Assets from '../Assets'; import { Engine } from '../Bastion'; import GameObject from '../GameObject'; import * as PIXI from 'pixi.js'; +import { WaveManagerEvents } from './WaveManager'; export default class MissionStats extends GameObject { private hp: number = 100; private gold: number = 0; private goldText: PIXI.Text; private healthText: PIXI.Text; + private waveText: PIXI.Text; public getHP() { return this.hp; @@ -67,8 +69,18 @@ export default class MissionStats extends GameObject { dropShadow: true, }), }); + this.waveText = new PIXI.Text({ + text: `0/${Engine.GameScene.mission.rounds.length}`, + style: new PIXI.TextStyle({ + fill: 'dodgerblue', + fontSize: 36, + fontWeight: 'bold', + dropShadow: true, + }), + }); const healthSprite = new PIXI.Sprite(Assets.HealthTexture); const goldSprite = new PIXI.Sprite(Assets.GoldTexture); + const waveSprite = new PIXI.Sprite(Assets.WaveTexture); this.healthText.x = 200; this.healthText.y = -15; @@ -84,10 +96,23 @@ export default class MissionStats extends GameObject { goldSprite.height = 56; goldSprite.y = 15; + this.waveText.x = 200; + this.waveText.y = 55; + waveSprite.x = 155; + waveSprite.width = 46; + waveSprite.height = 32; + waveSprite.y = 65; + this.container.addChild(this.healthText); this.container.addChild(this.goldText); + this.container.addChild(this.waveText); this.container.addChild(healthSprite); this.container.addChild(goldSprite); + this.container.addChild(waveSprite); + + Engine.GameScene.events.on(WaveManagerEvents.NewWave, (wave) => { + this.waveText.text = `${wave}/${Engine.GameScene.mission.rounds.length}`; + }); } public update() {} diff --git a/src/classes/game/NotificationManager.ts b/src/classes/game/NotificationManager.ts new file mode 100644 index 0000000..443208c --- /dev/null +++ b/src/classes/game/NotificationManager.ts @@ -0,0 +1,84 @@ +import { Engine } from '../Bastion'; +import GameObject from '../GameObject'; +import * as PIXI from 'pixi.js'; +import { FadeInOut } from './AnimationManager'; + +export type NotificationType = 'info' | 'warn' | 'danger' | 'reward'; + +class Notification { + public textObj: PIXI.Text; + public ticksToFadeAway: number; + public animating: boolean = false; + public destroyed = false; + constructor(text, type: NotificationType, x, y, ticksToFadeAway) { + let fill = 0xffffff; + if (type == 'info') { + fill = 0x20b3fc; + } else if (type == 'warn') { + fill = 0xfcd720; + } else if (type == 'danger') { + fill = 0xfc0a0a; + } else if (type == 'reward') { + fill = 0xd65afc; + } + this.ticksToFadeAway = ticksToFadeAway; + this.textObj = new PIXI.Text({ + text: text, + style: new PIXI.TextStyle({ + fill: fill, + fontSize: 36, + fontWeight: 'bold', + dropShadow: true, + align: 'center', + }), + x: x, + y: y, + zIndex: 100, + }); + this.textObj.anchor.set(0.5, 0.5); + Engine.NotificationManager.container.addChild(this.textObj); + } + public destroy() { + this.textObj.destroy(); + this.destroyed = true; + } +} + +export default class NotificationManager extends GameObject { + // ? TODO: (maybe) turn it into 20 slots to avoid text rendering ontop of one another. + private notifications: Notification[] = []; + private ticks: number = 0; + constructor() { + super(); + this.bb.x = Engine.app.canvas.width / 2; + this.bb.y = 40; + this.copyBBToContainer(); + this.container.zIndex = 100; + Engine.app.stage.addChild(this.container); + } + public Notify(text, type: NotificationType) { + let x = 0; + let y = this.notifications.length * 30; + this.notifications.push(new Notification(text, type, x, y, this.ticks + 180)); + console.log('CREATED NOTIFICATION '); + console.log(text, type, x, y, this.ticks + 180); + } + public update(_) { + this.ticks++; + for (let i = this.notifications.length - 1; i >= 0; i--) { + const notif = this.notifications[i]; + if (notif.destroyed) { + this.notifications.splice(i, 1); + continue; + } + if (this.ticks >= notif.ticksToFadeAway && !notif.animating) { + notif.animating = true; + Engine.AnimationManager.Animate( + new FadeInOut('out', 240, notif.textObj, () => { + notif.destroy(); + }) + ); + } + } + } +} diff --git a/src/classes/game/WaveManager.ts b/src/classes/game/WaveManager.ts index 3951e08..4d61c5b 100644 --- a/src/classes/game/WaveManager.ts +++ b/src/classes/game/WaveManager.ts @@ -2,10 +2,12 @@ import { CreepType, MissionRoundDefinition, PathDefinition } from '../Definition import * as PIXI from 'pixi.js'; import Creep, { CreepEvents } from './Creep'; import { Engine } from '../Bastion'; +import GameObject from '../GameObject'; export enum WaveManagerEvents { CreepSpawned = 'creepSpawned', Finished = 'finished', + NewWave = 'newwave', } type CreepInstance = { @@ -14,7 +16,7 @@ type CreepInstance = { spawned: boolean; }; -export default class WaveManager { +export default class WaveManager extends GameObject { // Doesn't need to extend GameObject since it does not render private creeps: CreepInstance[] = []; private rounds: MissionRoundDefinition[]; @@ -22,9 +24,9 @@ export default class WaveManager { private ticks: number = 0; private started: boolean = false; public finished: boolean = false; - public events = new PIXI.EventEmitter(); private internalCreepId: number = 0; constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) { + super(); Engine.WaveManager = this; this.rounds = rounds; this.paths = paths; diff --git a/src/classes/gui/TowerTab.ts b/src/classes/gui/TowerTab.ts index 58c1939..9db2696 100644 --- a/src/classes/gui/TowerTab.ts +++ b/src/classes/gui/TowerTab.ts @@ -8,6 +8,7 @@ class TowerButton extends GuiObject { private frameSprite: PIXI.NineSliceSprite; private background: PIXI.Sprite; private towerName: string; + private i: number = 0; constructor(index: number, row, width, height, parent: PIXI.Container, backgroundTexture, towerName) { if (index > 3 || row > 2 || index < 0 || row < 0) throw 'Index/row out of bounds for TowerButton.'; super(true); diff --git a/src/main.ts b/src/main.ts index 9b33675..5dbcd1a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { MainScene } from './scenes/Main'; import { GameScene } from './scenes/Game'; import { log } from './utils'; import { AnimationManager } from './classes/game/AnimationManager'; +import NotificationManager from './classes/game/NotificationManager'; (async () => { const app = new PIXI.Application(); @@ -49,8 +50,12 @@ import { AnimationManager } from './classes/game/AnimationManager'; await Assets.LoadAssets(); new GameMaster(); Engine.AnimationManager = new AnimationManager(); + Engine.NotificationManager = new NotificationManager(); globalThis.Engine = Engine; - PIXI.Ticker.shared.add((ticker) => Engine.AnimationManager.update(ticker.elapsedMS)); + PIXI.Ticker.shared.add((ticker) => { + Engine.NotificationManager.update(ticker.elapsedMS); + Engine.AnimationManager.update(ticker.elapsedMS); + }); Engine.GameMaster.changeScene(new MainScene()); let params = new URLSearchParams(location.href); if (params.entries().next().value[1] == 'game') Engine.GameMaster.changeScene(new GameScene('Mission 1')); diff --git a/src/scenes/Game.ts b/src/scenes/Game.ts index 9d95172..d697e09 100644 --- a/src/scenes/Game.ts +++ b/src/scenes/Game.ts @@ -10,6 +10,8 @@ import Scene from './Scene'; import * as PIXI from 'pixi.js'; import MissionStats from '../classes/game/MissionStats'; import TowerManager from '../classes/game/TowerManager'; +import NotificationManager from '../classes/game/NotificationManager'; +import { MissionPickerScene } from './MissionPicker'; enum RoundMode { Purchase = 0, @@ -72,9 +74,12 @@ export class GameScene extends Scene { this.changeRoundButton.container.removeFromParent(); this.sidebar.container.addChild(this.changeRoundButton.container); this.changeRoundButton.onClick = () => { + if (this.playerWon) return this.ReturnToMain(); + if (this.isGameOver) return Engine.NotificationManager.Notify('No more waves.', 'warn'); this.changeRoundButton.setEnabled(false); - this.changeRoundButton.setCaption('[X]'); + this.changeRoundButton.setCaption('WAVE IN PROGRESS'); this.setRoundMode(RoundMode.Combat); + this.events.emit(WaveManagerEvents.NewWave, `${this.currentRound + 1}`); }; this.MissionStats = new MissionStats(100, 200); @@ -84,15 +89,25 @@ export class GameScene extends Scene { Engine.Grid.update(elapsedMS); Engine.TowerManager.update(elapsedMS); if (this.isWaveManagerFinished && Engine.Grid.creeps.length == 0) { + this.isWaveManagerFinished = false; this.changeRoundButton.setEnabled(true); this.changeRoundButton.setCaption('Start'); this.setRoundMode(RoundMode.Purchase); - this.NotifyPlayer(`Round ${this.currentRound + 1}/${this.mission.rounds.length} completed.`, 'info'); + Engine.NotificationManager.Notify( + `Round ${this.currentRound + 1}/${this.mission.rounds.length} completed.`, + 'info' + ); + if (this.currentRound == this.mission.rounds.length) { + Engine.NotificationManager.Notify(`Final round.`, 'danger'); + } if (this.currentRound + 1 == this.mission.rounds.length) { this.changeRoundButton.setCaption('WINNER!'); - this.NotifyPlayer(`Mission win!`, 'info'); + Engine.NotificationManager.Notify(`Mission victory!!`, 'reward'); + this.changeRoundButton.setCaption('Return to menu'); this.playerWon = true; + return; } + this.currentRound++; } if (this.MissionStats.getHP() <= 0) { @@ -128,10 +143,11 @@ export class GameScene extends Scene { Engine.WaveManager.end(); } } - public NotifyPlayer(notification, notifytype) { - // TODO: show to player for real - console.log('NOTIFY PLAYER! type: ' + notifytype); - console.log(notification); + + private ReturnToMain() { + this.destroy(); + Engine.app.stage.removeChildren(); + Engine.GameMaster.changeScene(new MissionPickerScene()); } public onTowerPlaced() {} } diff --git a/src/scenes/Main.ts b/src/scenes/Main.ts index 5bb73d7..b9d142c 100644 --- a/src/scenes/Main.ts +++ b/src/scenes/Main.ts @@ -62,6 +62,5 @@ export class MainScene extends Scene { b2.onClick = (e) => { alert('Does nothing for now, just placeholder.'); }; - Engine.AnimationManager.Animate(new Tween(300, b2.container, 100, 600, 620, 600, () => {})); } }