!!! Minimum viable product ACHIEVED !!!

This commit is contained in:
koneko 2025-01-11 21:43:00 +01:00
parent b371b48020
commit d95c44373a
13 changed files with 215 additions and 36 deletions

BIN
public/assets/gui/wave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

View File

@ -103,6 +103,26 @@
} }
], ],
"offeredGems": [0, 1, 2, 3] "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]
} }
] ]
} }

View File

@ -49,6 +49,9 @@ export default class GameAssets {
GameAssets.GoldTexture = await PIXI.Assets.load({ GameAssets.GoldTexture = await PIXI.Assets.load({
src: '/assets/gui/money.png', src: '/assets/gui/money.png',
}); });
GameAssets.WaveTexture = await PIXI.Assets.load({
src: '/assets/gui/wave.png',
});
GameAssets.BasicCreepTexture = await PIXI.Assets.load({ GameAssets.BasicCreepTexture = await PIXI.Assets.load({
src: '/assets/creeps/basic.jpg', src: '/assets/creeps/basic.jpg',
@ -126,6 +129,7 @@ export default class GameAssets {
public static Button02Texture: PIXI.Texture; public static Button02Texture: PIXI.Texture;
public static HealthTexture: PIXI.Texture; public static HealthTexture: PIXI.Texture;
public static GoldTexture: PIXI.Texture; public static GoldTexture: PIXI.Texture;
public static WaveTexture: PIXI.Texture;
public static MissionBackgrounds: PIXI.Texture[] = []; public static MissionBackgrounds: PIXI.Texture[] = [];
public static TowerSprites: PIXI.Texture[] = []; public static TowerSprites: PIXI.Texture[] = [];

View File

@ -7,6 +7,7 @@ import WaveManager from './game/WaveManager';
import TowerManager from './game/TowerManager'; import TowerManager from './game/TowerManager';
import { GameScene } from '../scenes/Game'; import { GameScene } from '../scenes/Game';
import { AnimationManager } from './game/AnimationManager'; import { AnimationManager } from './game/AnimationManager';
import NotificationManager from './game/NotificationManager';
export class Engine { export class Engine {
public static app: PIXI.Application; public static app: PIXI.Application;
@ -18,6 +19,7 @@ export class Engine {
public static WaveManager: WaveManager; public static WaveManager: WaveManager;
public static TowerManager: TowerManager; public static TowerManager: TowerManager;
public static AnimationManager: AnimationManager; public static AnimationManager: AnimationManager;
public static NotificationManager: NotificationManager;
public static GameScene: GameScene; public static GameScene: GameScene;
public static latestCommit: string; public static latestCommit: string;
} }

View File

@ -31,5 +31,21 @@ export default abstract class GameObject {
return this.bb; 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; public abstract update(elapsedMS): void;
} }

View File

@ -2,14 +2,17 @@ import * as PIXI from 'pixi.js';
class Animateable { class Animateable {
public finished: boolean = false; public finished: boolean = false;
protected callbackFn: Function; protected calledBack: boolean = false;
public callbackFn: Function;
public Finish() { public Finish() {
this.finished = true; this.finished = true;
} }
public update(ms) { 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) { public update(ms) {
super.update(ms); super.update(ms);
if (this.pixiObject == null) return this.Finish();
this.ticks++; this.ticks++;
if (this.fadeType == 'in') { if (this.fadeType == 'in') {
this.pixiObject.alpha = this.ticks / this.fadeTime; this.pixiObject.alpha = this.ticks / this.fadeTime;
} else { } else {
this.pixiObject.alpha -= 1 / this.fadeTime; this.pixiObject.alpha -= 1 / this.fadeTime;
} }
console.log(this.pixiObject.alpha); if (this.ticks >= this.fadeTime || this.pixiObject.alpha <= 0) this.Finish();
if (this.ticks >= this.fadeTime) this.Finish();
} }
} }
@ -52,32 +55,31 @@ export class Tween extends Animateable {
private goalY: number; private goalY: number;
private ticks: number = 0; 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(); super();
this.tweenTime = timeInFrames; this.tweenTime = timeInFrames;
this.pixiObject = object; this.pixiObject = object;
this.callbackFn = callbackFn; this.callbackFn = callbackFn;
this.goalX = goalX; this.goalX = goalX;
this.goalY = goalY; this.goalY = goalY;
this.pixiObject.x = fromX;
this.pixiObject.y = fromY;
} }
public update(ms) { public update(deltaMS) {
super.update(ms); super.update(deltaMS);
this.ticks++; this.ticks += deltaMS;
const objX = this.pixiObject.x; // Calculate the fraction of time elapsed
const objY = this.pixiObject.y; const progress = this.ticks / (this.tweenTime * 16.67); // Assuming 60 FPS, 1 frame = 16.67ms
// TODO: fix this by the time you get to using it, it moves the obj too fast and wrong
if (objX != this.goalX) { // Update the position based on the progress
let diff = this.goalX - objX; this.pixiObject.x = (this.goalX - this.pixiObject.x) * progress + this.pixiObject.x;
this.pixiObject.x += ms * diff * (this.ticks / this.tweenTime); 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); this.AnimationQueue.push(animatable);
} }
public update(ms) { public update(ms) {
this.AnimationQueue.forEach((anim) => { for (let i = this.AnimationQueue.length - 1; i >= 0; i--) {
if (anim.finished) this.AnimationQueue.splice(this.AnimationQueue.indexOf(anim), 1); const anim = this.AnimationQueue[i];
anim.update(ms); if (anim.finished) {
}); anim.callbackFn();
this.AnimationQueue.splice(i, 1);
} else anim.update(ms);
}
} }
} }

View File

@ -2,12 +2,14 @@ import Assets from '../Assets';
import { Engine } from '../Bastion'; import { Engine } from '../Bastion';
import GameObject from '../GameObject'; import GameObject from '../GameObject';
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import { WaveManagerEvents } from './WaveManager';
export default class MissionStats extends GameObject { export default class MissionStats extends GameObject {
private hp: number = 100; private hp: number = 100;
private gold: number = 0; private gold: number = 0;
private goldText: PIXI.Text; private goldText: PIXI.Text;
private healthText: PIXI.Text; private healthText: PIXI.Text;
private waveText: PIXI.Text;
public getHP() { public getHP() {
return this.hp; return this.hp;
@ -67,8 +69,18 @@ export default class MissionStats extends GameObject {
dropShadow: true, 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 healthSprite = new PIXI.Sprite(Assets.HealthTexture);
const goldSprite = new PIXI.Sprite(Assets.GoldTexture); const goldSprite = new PIXI.Sprite(Assets.GoldTexture);
const waveSprite = new PIXI.Sprite(Assets.WaveTexture);
this.healthText.x = 200; this.healthText.x = 200;
this.healthText.y = -15; this.healthText.y = -15;
@ -84,10 +96,23 @@ export default class MissionStats extends GameObject {
goldSprite.height = 56; goldSprite.height = 56;
goldSprite.y = 15; 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.healthText);
this.container.addChild(this.goldText); this.container.addChild(this.goldText);
this.container.addChild(this.waveText);
this.container.addChild(healthSprite); this.container.addChild(healthSprite);
this.container.addChild(goldSprite); 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() {} public update() {}

View 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();
})
);
}
}
}
}

View File

@ -2,10 +2,12 @@ import { CreepType, MissionRoundDefinition, PathDefinition } from '../Definition
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import Creep, { CreepEvents } from './Creep'; import Creep, { CreepEvents } from './Creep';
import { Engine } from '../Bastion'; import { Engine } from '../Bastion';
import GameObject from '../GameObject';
export enum WaveManagerEvents { export enum WaveManagerEvents {
CreepSpawned = 'creepSpawned', CreepSpawned = 'creepSpawned',
Finished = 'finished', Finished = 'finished',
NewWave = 'newwave',
} }
type CreepInstance = { type CreepInstance = {
@ -14,7 +16,7 @@ type CreepInstance = {
spawned: boolean; spawned: boolean;
}; };
export default class WaveManager { export default class WaveManager extends GameObject {
// Doesn't need to extend GameObject since it does not render // Doesn't need to extend GameObject since it does not render
private creeps: CreepInstance[] = []; private creeps: CreepInstance[] = [];
private rounds: MissionRoundDefinition[]; private rounds: MissionRoundDefinition[];
@ -22,9 +24,9 @@ export default class WaveManager {
private ticks: number = 0; private ticks: number = 0;
private started: boolean = false; private started: boolean = false;
public finished: boolean = false; public finished: boolean = false;
public events = new PIXI.EventEmitter();
private internalCreepId: number = 0; private internalCreepId: number = 0;
constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) { constructor(rounds: MissionRoundDefinition[], paths: PathDefinition[]) {
super();
Engine.WaveManager = this; Engine.WaveManager = this;
this.rounds = rounds; this.rounds = rounds;
this.paths = paths; this.paths = paths;

View File

@ -8,6 +8,7 @@ class TowerButton extends GuiObject {
private frameSprite: PIXI.NineSliceSprite; private frameSprite: PIXI.NineSliceSprite;
private background: PIXI.Sprite; private background: PIXI.Sprite;
private towerName: string; private towerName: string;
private i: number = 0;
constructor(index: number, row, width, height, parent: PIXI.Container, backgroundTexture, towerName) { 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.'; if (index > 3 || row > 2 || index < 0 || row < 0) throw 'Index/row out of bounds for TowerButton.';
super(true); super(true);

View File

@ -5,6 +5,7 @@ import { MainScene } from './scenes/Main';
import { GameScene } from './scenes/Game'; import { GameScene } from './scenes/Game';
import { log } from './utils'; import { log } from './utils';
import { AnimationManager } from './classes/game/AnimationManager'; import { AnimationManager } from './classes/game/AnimationManager';
import NotificationManager from './classes/game/NotificationManager';
(async () => { (async () => {
const app = new PIXI.Application(); const app = new PIXI.Application();
@ -49,8 +50,12 @@ import { AnimationManager } from './classes/game/AnimationManager';
await Assets.LoadAssets(); await Assets.LoadAssets();
new GameMaster(); new GameMaster();
Engine.AnimationManager = new AnimationManager(); Engine.AnimationManager = new AnimationManager();
Engine.NotificationManager = new NotificationManager();
globalThis.Engine = Engine; 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()); Engine.GameMaster.changeScene(new MainScene());
let params = new URLSearchParams(location.href); let params = new URLSearchParams(location.href);
if (params.entries().next().value[1] == 'game') Engine.GameMaster.changeScene(new GameScene('Mission 1')); if (params.entries().next().value[1] == 'game') Engine.GameMaster.changeScene(new GameScene('Mission 1'));

View File

@ -10,6 +10,8 @@ import Scene from './Scene';
import * as PIXI from 'pixi.js'; import * as PIXI from 'pixi.js';
import MissionStats from '../classes/game/MissionStats'; import MissionStats from '../classes/game/MissionStats';
import TowerManager from '../classes/game/TowerManager'; import TowerManager from '../classes/game/TowerManager';
import NotificationManager from '../classes/game/NotificationManager';
import { MissionPickerScene } from './MissionPicker';
enum RoundMode { enum RoundMode {
Purchase = 0, Purchase = 0,
@ -72,9 +74,12 @@ export class GameScene extends Scene {
this.changeRoundButton.container.removeFromParent(); this.changeRoundButton.container.removeFromParent();
this.sidebar.container.addChild(this.changeRoundButton.container); this.sidebar.container.addChild(this.changeRoundButton.container);
this.changeRoundButton.onClick = () => { 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.setEnabled(false);
this.changeRoundButton.setCaption('[X]'); this.changeRoundButton.setCaption('WAVE IN PROGRESS');
this.setRoundMode(RoundMode.Combat); this.setRoundMode(RoundMode.Combat);
this.events.emit(WaveManagerEvents.NewWave, `${this.currentRound + 1}`);
}; };
this.MissionStats = new MissionStats(100, 200); this.MissionStats = new MissionStats(100, 200);
@ -84,15 +89,25 @@ export class GameScene extends Scene {
Engine.Grid.update(elapsedMS); Engine.Grid.update(elapsedMS);
Engine.TowerManager.update(elapsedMS); Engine.TowerManager.update(elapsedMS);
if (this.isWaveManagerFinished && Engine.Grid.creeps.length == 0) { if (this.isWaveManagerFinished && Engine.Grid.creeps.length == 0) {
this.isWaveManagerFinished = false;
this.changeRoundButton.setEnabled(true); this.changeRoundButton.setEnabled(true);
this.changeRoundButton.setCaption('Start'); this.changeRoundButton.setCaption('Start');
this.setRoundMode(RoundMode.Purchase); 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) { if (this.currentRound + 1 == this.mission.rounds.length) {
this.changeRoundButton.setCaption('WINNER!'); this.changeRoundButton.setCaption('WINNER!');
this.NotifyPlayer(`Mission win!`, 'info'); Engine.NotificationManager.Notify(`Mission victory!!`, 'reward');
this.changeRoundButton.setCaption('Return to menu');
this.playerWon = true; this.playerWon = true;
return;
} }
this.currentRound++;
} }
if (this.MissionStats.getHP() <= 0) { if (this.MissionStats.getHP() <= 0) {
@ -128,10 +143,11 @@ export class GameScene extends Scene {
Engine.WaveManager.end(); Engine.WaveManager.end();
} }
} }
public NotifyPlayer(notification, notifytype) {
// TODO: show to player for real private ReturnToMain() {
console.log('NOTIFY PLAYER! type: ' + notifytype); this.destroy();
console.log(notification); Engine.app.stage.removeChildren();
Engine.GameMaster.changeScene(new MissionPickerScene());
} }
public onTowerPlaced() {} public onTowerPlaced() {}
} }

View File

@ -62,6 +62,5 @@ export class MainScene extends Scene {
b2.onClick = (e) => { b2.onClick = (e) => {
alert('Does nothing for now, just placeholder.'); alert('Does nothing for now, just placeholder.');
}; };
Engine.AnimationManager.Animate(new Tween(300, b2.container, 100, 600, 620, 600, () => {}));
} }
} }