!!! Minimum viable product ACHIEVED !!!
This commit is contained in:
parent
b371b48020
commit
d95c44373a
BIN
public/assets/gui/wave.png
Normal file
BIN
public/assets/gui/wave.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 267 KiB |
@ -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]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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[] = [];
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {}
|
||||
|
84
src/classes/game/NotificationManager.ts
Normal file
84
src/classes/game/NotificationManager.ts
Normal file
@ -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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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'));
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
@ -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, () => {}));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user