!!! 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]
},
{
"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({
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[] = [];

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}
}

View File

@ -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() {}

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 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;

View File

@ -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);

View File

@ -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'));

View File

@ -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() {}
}

View File

@ -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, () => {}));
}
}