diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 00000000..e69de29b diff --git a/README.md b/README.md index dc9b7cce..fe046d03 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,24 @@ # Game Name +Terminal Tactics + +# Team Color + +Olive + +# Developers + +Lawrence Collins (larryc@udel.edu) +Victor Vasquez (vicvas@udel.edu) + +# Blurb + +In "Terminal Tactics," players face a zombie onslaught using strategic Unix commands as their arsenal. Mimicking the tower defense genre, players must strategically arrange their troops in the correct sequence to repel the zombie invasion. Just like in "Plants vs. Zombies," each command possesses unique attributes, from defensive capabilities to offensive strikes, requiring players to think tactically to outsmart the undead hordes. This educational twist not only challenges players' gaming skills but also enhances their understanding of Unix commands, making "Terminal Tactics" an engaging fusion of entertainment and education in the battle against the undead. + +# Basic Instructions + +Type into the command line to find your troops (cd, ls, etc.) +======= TEXT GOES HERE # Team Color @@ -29,6 +48,16 @@ TEXT GOES HERE TEXT GOES HERE # Educational Game Design Document +(https://github.com/UD-S24-CISC374/final-project-olive/blob/main/docs/egdd.md) +# Credits + +GitHub: Ideas and some code +Phaser Forums: Ideas and some code +Pixabay: Music +ChatGPT +Microsoft Designer AI Image Creator + +======= Link to our [egdd](Educational Game Design Document) diff --git a/assets/audio/backgroundMusic.mp3 b/assets/audio/backgroundMusic.mp3 new file mode 100644 index 00000000..be9f2d2b Binary files /dev/null and b/assets/audio/backgroundMusic.mp3 differ diff --git a/assets/audio/defeatMusic.mp3 b/assets/audio/defeatMusic.mp3 new file mode 100644 index 00000000..0ffa64b4 Binary files /dev/null and b/assets/audio/defeatMusic.mp3 differ diff --git a/assets/audio/endGameMusic.mp3 b/assets/audio/endGameMusic.mp3 new file mode 100644 index 00000000..0768cc01 Binary files /dev/null and b/assets/audio/endGameMusic.mp3 differ diff --git a/assets/audio/titleScreenMusic.mp3 b/assets/audio/titleScreenMusic.mp3 new file mode 100644 index 00000000..c4247591 Binary files /dev/null and b/assets/audio/titleScreenMusic.mp3 differ diff --git a/assets/audio/tutorialMusic.mp3 b/assets/audio/tutorialMusic.mp3 new file mode 100644 index 00000000..3ea62728 Binary files /dev/null and b/assets/audio/tutorialMusic.mp3 differ diff --git a/assets/audio/victoryMusic.mp3 b/assets/audio/victoryMusic.mp3 new file mode 100644 index 00000000..e94171a3 Binary files /dev/null and b/assets/audio/victoryMusic.mp3 differ diff --git a/assets/img/arrow-small.png b/assets/img/arrow-small.png new file mode 100644 index 00000000..e5741335 Binary files /dev/null and b/assets/img/arrow-small.png differ diff --git a/assets/img/arrow.png b/assets/img/arrow.png new file mode 100644 index 00000000..12224cbd Binary files /dev/null and b/assets/img/arrow.png differ diff --git a/assets/img/background.jpg b/assets/img/background.jpg new file mode 100644 index 00000000..b09b2be2 Binary files /dev/null and b/assets/img/background.jpg differ diff --git a/assets/img/book.png b/assets/img/book.png new file mode 100644 index 00000000..6d437a1d Binary files /dev/null and b/assets/img/book.png differ diff --git a/assets/img/button.jpg b/assets/img/button.jpg new file mode 100644 index 00000000..5e71778c Binary files /dev/null and b/assets/img/button.jpg differ diff --git a/assets/img/dnd_titlescreen1.png b/assets/img/dnd_titlescreen1.png new file mode 100644 index 00000000..19855e53 Binary files /dev/null and b/assets/img/dnd_titlescreen1.png differ diff --git a/assets/img/dude.png b/assets/img/dude.png new file mode 100644 index 00000000..6b35f4b4 Binary files /dev/null and b/assets/img/dude.png differ diff --git a/assets/img/energy-ball-sm.png b/assets/img/energy-ball-sm.png new file mode 100644 index 00000000..1924c42b Binary files /dev/null and b/assets/img/energy-ball-sm.png differ diff --git a/assets/img/file.jpg b/assets/img/file.jpg new file mode 100644 index 00000000..bd257fb4 Binary files /dev/null and b/assets/img/file.jpg differ diff --git a/assets/img/finishLine.png b/assets/img/finishLine.png new file mode 100644 index 00000000..2f82c498 Binary files /dev/null and b/assets/img/finishLine.png differ diff --git a/assets/img/fire-ball-sm.png b/assets/img/fire-ball-sm.png new file mode 100644 index 00000000..717148be Binary files /dev/null and b/assets/img/fire-ball-sm.png differ diff --git a/assets/img/grass-sm.png b/assets/img/grass-sm.png new file mode 100644 index 00000000..55a3a92f Binary files /dev/null and b/assets/img/grass-sm.png differ diff --git a/assets/img/grass.png b/assets/img/grass.png new file mode 100644 index 00000000..b4fea4bc Binary files /dev/null and b/assets/img/grass.png differ diff --git a/assets/img/left-trees.jpg b/assets/img/left-trees.jpg new file mode 100644 index 00000000..e9fc3691 Binary files /dev/null and b/assets/img/left-trees.jpg differ diff --git a/assets/img/map1.png b/assets/img/map1.png new file mode 100644 index 00000000..c0366203 Binary files /dev/null and b/assets/img/map1.png differ diff --git a/assets/img/platform.png b/assets/img/platform.png new file mode 100644 index 00000000..1e4a3f86 Binary files /dev/null and b/assets/img/platform.png differ diff --git a/assets/img/play_button.png b/assets/img/play_button.png new file mode 100644 index 00000000..36aa309b Binary files /dev/null and b/assets/img/play_button.png differ diff --git a/assets/img/questionMark.png b/assets/img/questionMark.png new file mode 100644 index 00000000..132b436d Binary files /dev/null and b/assets/img/questionMark.png differ diff --git a/assets/img/ranger-small.png b/assets/img/ranger-small.png new file mode 100644 index 00000000..c794cbbb Binary files /dev/null and b/assets/img/ranger-small.png differ diff --git a/assets/img/ranger.png b/assets/img/ranger.png new file mode 100644 index 00000000..aa450f86 Binary files /dev/null and b/assets/img/ranger.png differ diff --git a/assets/img/right-trees.jpg b/assets/img/right-trees.jpg new file mode 100644 index 00000000..7eb18afa Binary files /dev/null and b/assets/img/right-trees.jpg differ diff --git a/assets/img/soldier-small.png b/assets/img/soldier-small.png new file mode 100644 index 00000000..ec8eeadd Binary files /dev/null and b/assets/img/soldier-small.png differ diff --git a/assets/img/soldier.png b/assets/img/soldier.png new file mode 100644 index 00000000..b0805a26 Binary files /dev/null and b/assets/img/soldier.png differ diff --git a/assets/img/sword.png b/assets/img/sword.png new file mode 100644 index 00000000..1f8c4e83 Binary files /dev/null and b/assets/img/sword.png differ diff --git a/assets/img/titlePage.jpeg b/assets/img/titlePage.jpeg new file mode 100644 index 00000000..8532cea3 Binary files /dev/null and b/assets/img/titlePage.jpeg differ diff --git a/assets/img/trainGrounds.png b/assets/img/trainGrounds.png new file mode 100644 index 00000000..2163c1f0 Binary files /dev/null and b/assets/img/trainGrounds.png differ diff --git a/assets/img/tree.png b/assets/img/tree.png new file mode 100644 index 00000000..b1b19fb8 Binary files /dev/null and b/assets/img/tree.png differ diff --git a/assets/img/tutorial_icon.png b/assets/img/tutorial_icon.png new file mode 100644 index 00000000..79f4bf9f Binary files /dev/null and b/assets/img/tutorial_icon.png differ diff --git a/assets/img/wizard-small.png b/assets/img/wizard-small.png new file mode 100644 index 00000000..652fd440 Binary files /dev/null and b/assets/img/wizard-small.png differ diff --git a/assets/img/wizard.png b/assets/img/wizard.png new file mode 100644 index 00000000..d3cb180a Binary files /dev/null and b/assets/img/wizard.png differ diff --git a/assets/img/wizardNPC.jpg b/assets/img/wizardNPC.jpg new file mode 100644 index 00000000..35bcbe1f Binary files /dev/null and b/assets/img/wizardNPC.jpg differ diff --git a/assets/img/youLose.jpeg b/assets/img/youLose.jpeg new file mode 100644 index 00000000..03c373fd Binary files /dev/null and b/assets/img/youLose.jpeg differ diff --git a/assets/img/youWin.jpeg b/assets/img/youWin.jpeg new file mode 100644 index 00000000..1cac44ab Binary files /dev/null and b/assets/img/youWin.jpeg differ diff --git a/assets/img/zombie-small.png b/assets/img/zombie-small.png new file mode 100644 index 00000000..ae4c6880 Binary files /dev/null and b/assets/img/zombie-small.png differ diff --git a/assets/img/zombie.png b/assets/img/zombie.png new file mode 100644 index 00000000..e6d4cf2f Binary files /dev/null and b/assets/img/zombie.png differ diff --git a/docs/egdd.md b/docs/egdd.md index 51ed6536..16cfeb0a 100644 --- a/docs/egdd.md +++ b/docs/egdd.md @@ -1 +1,181 @@ -REPLACE THIS TEXT WITH YOUR EGDD MARKDOWN. +# Alliance vs The Council + + +## Elevator Pitch + +A tower defense game where the user must survive an onslaught of creatures by placing towers using Unix commands + + +## Influences (Brief) + +- *Plants Vs Zombies*: + - Medium: *Video Game* + - Explanation: *This game is a major influence on what we are creating. As we are taking this idea and expanding it. We are utilizing the idea of a board-based tower defense game and modifying it by forcing the player to use Unix commands to move around.* +- *CISC210*: + - Medium: *UD Course* + - Explanation: *CISC201 is a UD course where we learned basic terminal commands to be able to gain a better understanding of UNIX. By modeling our game based on some of the material we learned we can gain a better understanding of how to make the game educational.* +- *DnD / Baulders Gate*: + - Medium: *Game* + - Explanation: *We are using the fantasy aspect of DnD to change around the allies and enemies. This influence has a large variety of small and large-scale enemies for us to use to make the game more interesting to look at. This also works for the placeable allies as we can use the various classes as stackable/movable/combinable characters.* + +## Core Gameplay Mechanics (Brief) + +*Give a very high-level description of any core gameplay mechanics* + +- *Health* +- *Battle Phases (Buying phase and monster wave phase)* +- *Level progression* +- *Currency* + +# Learning Aspects + + +## Learning Domains + +*Unix Commands* + +## Target Audiences + +Middle School to high school-aged kids new to Unix commands + +## Target Contexts + +*This would be used within a lesson plan of a * + +## Learning Objectives + +*Remember, Learning Objectives are NOT simply topics. They are statements of observable behavior that a learner can do after the learning experience. You cannot observe someone "understanding" or "knowing" something.* + +- *Unix Commands*: *By the end of instruction, students will be able to identify common Unix commands and describe their effects.* +- *Unix Complexity*: *Students should be able to combine unix commands to do more complex tasks.* + +## Prerequisite Knowledge + +*What do they need to know prior to trying this game?* + +- *An idea of what Unix commands are* +- *Why they would use Unix commands* + +## Assessment Measures + +*Given a preexisting directory and be able to traverse and modify it with little to no help* + + +# What sets this project apart? + +*Give some reasons why this game is not like every other game out there. Whether the learning objective is unique, the gameplay mechanics are new, or what. You should persuade the reader that your game is novel and worthy of development. Consider arguments that would be persuasive to a Venture Capitalist, Teacher, or Researcher. These might be focused on learning needs, too.* + +- *It is unique* +- *Educational while still being fun* +- *Most coding or systems-based assignments are the same or throw you in headfirst. This will visualize the learning curve and shorten said curve* + +# Player Interaction Patterns and Modes + +## Player Interaction Pattern + +*People will play our game using keyboard and mouse on a browser. There will only be one player involved at once.* + +## Player Modes + +*Your game has one or more player modes. Describe each discrete mode, considering things like menus too. Generally describe the transitions between modes too.* + +- *Single Player Mode*: *A player will be able to access a folder menu that allows them to understand all of the different characters available, different abilities offered in the game, what level you are on, etc.* +- *Battle Board Menu*: *This menu is where * +- *etc.* + +# Gameplay Objectives + +- *Protect the castle*: + - Description: *The player must place allies to protect against the waves of enemies * + - Alignment: *This aligns with learning how to adapt and work under pressure* +- *Navigate the “Book”*: + - Description: *The player must use Unix commands to navigate the “Book” to pick out their allies* + - Alignment: *This follows the learning objective of learning Unix commands* + +# Procedures/Actions + +*The player will type Unix commands (turning the pages) to place allies to protect the castle * + +# Rules + +If the player types in an incorrect or currently unknown Unix command the book will flash red +*What resources are available to the player that they make use of? How does this affect gameplay? How are these resources finite?* + +# Objects/Entities + +*We will have to design the board and some sort of terminal implementation* + +## Core Gameplay Mechanics (Detailed) + +- *Zombie Wave Defense*: *The game challenges players with progressively tougher waves of “zombies.” The idea is a “zombie” wave of defense. The different types of zombies require the player to adapt their strategy by choosing different plants and placements of those plants to handle the wave of zombies. The idea is that players should anticipate and plan out their defense to beat the wave. * +- *Plant Placement and Resource Management:*: *Strategically placing different types of “plants” on the board to defend against the wave of zombies trying to get past your defense. Each plant has unique abilities that are good for defending against certain zombies or waves of zombies. Players must manage their resources to strategically have good defense throughout their progression. Deciding which plants to place before each wave is a core mechanic of the game. * +- *Progression and Unlocking New Plants*: *As you progress in the game you will unlock new plants/characters that will aid you in defeating the waves of zombies. The progression mechanic will encourage users to create new strategies and find different ways to defeat the waves of zombies. * + + +## Feedback + +*Explicitly describe what visual/audio/animation indicators there are that give players feedback on their progress towards their gameplay objectives (and ideally the learning objectives): A player will know how they are doing in terms of their learning objective because they will have to use UNIX commands to progress in the game. The further they make it into the game the more “reps” they will have with UNIX command built into the game. The more they play the more the commands are being reinforced into their brain.* + +*Describe what longer-term feedback you detect and give that guides the player in their learning and lets them know how they are doing in regard to the learning objectives: The player will know how they are doing in regard to the learning objectives when they are seamlessly navigating our commands. This will help the player translate their learned understanding into a real Linux environment.* + +# Story and Gameplay + +## Presentation of Rules + +*The player will learn the game mechanics by being presented with a tutorial (level 0) which allows them to experiment with the game and its mechanics. They will also be introduced to a cheat sheet which will help them understand the built-in UNIX-like commands that they will need to use in the game.* + +## Presentation of Content + +*Briefly describe how the player will be taught the core material they are meant to learn: The player will be taught the core material by being forced to use UNIX-like commands that will help them familiarize themselves with real UNIX commands and other concepts related to the topic. * + +## Story (Brief) + +*The Summary or TL;DR version of below* + +## Storyboarding + +*Go into as much detail as needs be to visually convey the Dynamics of your game. Be detailed. Create storyboards and freeze frame images that concisely capture important key elements of your game. You are strongly recommended to sketch pictures on paper and embed them here. Be sure make it clear how previously-described mechanics come through in the dynamics.* + +# Assets Needed + +## Aethestics + +*Give a sense of the aesthetics of your game, the spirit and atmosphere: In this game, gardens are transformed into battlefields where magical plants, inspired by D&D characters, defend against a zombie invasion. The setting mixes a touch of fantasy with light-hearted horror, creating a unique backdrop for strategy and action. It's a straightforward yet engaging world where players navigate through challenges with strategy and a bit of humor.* + +## Graphical + +- Characters List + - *Mage character* + - *Wizard Character* + - *Zombies Characters* +- Textures: + - *Dungeon textures* + - *Zombie Textures* + - *...* +- Environment Art/Textures: + - *DnD like battlefields* + + +## Audio + + +*Game region/phase/time are ways of designating a particularly important place in the game.* + +- Music List (Ambient sound) + - *Zombie Phase*: *Dark advancing music* + - *Buying Phase*: *Relax ambient music* +*Game Interactions are things that trigger SFX, like character movement, hitting a spiky enemy, collecting a coin.* + +- Sound List (SFX) + - *Hitting a zombie*: *OOF sound*, *hurt sounds* + - *Buying a character*: *satisfaction sound* + - *Placing a character*: *satisfaction sound* + - *Correct UNIX-like command*: *satisfaction sound* + + +# Metadata + +* Template created by Austin Cory Bart , Mark Sheriff, Alec Markarian, and Benjamin Stanley. +* Version 0.0.3 + + diff --git a/package-lock.json b/package-lock.json index 279e7740..4b4d31a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "coding-3-phaser-scenes", + "name": "final-project", "version": "4.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "coding-3-phaser-scenes", + "name": "final-project", "version": "4.0.0", "license": "MIT", "dependencies": { diff --git a/src/config.ts b/src/config.ts index 9776bc5c..9ce37f28 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,8 @@ import Phaser from "phaser"; import MainScene from "./scenes/mainScene"; import PreloadScene from "./scenes/preloadScene"; +import TitleScene from "./scenes/titleScene"; +import TutorialScene from "./scenes/tutorialScene"; const DEFAULT_WIDTH = 1280; const DEFAULT_HEIGHT = 720; @@ -17,12 +19,12 @@ export const CONFIG = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }, - scene: [PreloadScene, MainScene], + scene: [PreloadScene, TitleScene, MainScene, TutorialScene], physics: { default: "arcade", arcade: { debug: false, - gravity: { y: 300 }, + gravity: { y: 0 }, }, }, input: { diff --git a/src/interfaces/directories.ts b/src/interfaces/directories.ts new file mode 100644 index 00000000..7f634b23 --- /dev/null +++ b/src/interfaces/directories.ts @@ -0,0 +1,6 @@ +import Phaser from "phaser"; + +export interface Directories { + curDir: string | undefined; + dialogue: Phaser.GameObjects.Text | undefined; +} diff --git a/src/interfaces/terminalCommandInterface.ts b/src/interfaces/terminalCommandInterface.ts new file mode 100644 index 00000000..60cf94bc --- /dev/null +++ b/src/interfaces/terminalCommandInterface.ts @@ -0,0 +1,7 @@ +import Phaser from "phaser"; + +export interface terminalCommandInterface { + text: string; + curDir: string; + consoleDialogue?: Phaser.GameObjects.Text; +} diff --git a/src/objects/CharacterManager.ts b/src/objects/CharacterManager.ts new file mode 100644 index 00000000..6f5819cb --- /dev/null +++ b/src/objects/CharacterManager.ts @@ -0,0 +1,58 @@ +import Phaser from "phaser"; +import { GameCharacter } from "./GameCharacter"; + +export class CharacterManager { + characters = new Set(); + + addCharacter(character: Phaser.GameObjects.GameObject) { + this.characters.add(character); + } + + sameCoord(x: number, y: number): boolean { + const foundCharacter = Array.from(this.characters).find((character) => { + const gameChar = character as GameCharacter; + return gameChar.x === x && gameChar.y === y; + }); + if (foundCharacter) { + return true; + } + + return false; + } + + removeCharacter(character: Phaser.GameObjects.GameObject) { + character.destroy(); + this.characters.delete(character); + } + removeCharacterByNameAndPosition( + name: string, + x: number, + y: number + ): boolean { + const characterToRemove = Array.from(this.characters).find( + (character) => { + const gameChar = character as GameCharacter; + return ( + gameChar.name === name && + gameChar.x === x && + gameChar.y === y + ); + } + ); + + if (characterToRemove) { + this.removeCharacter(characterToRemove); + return true; + } + + return false; + } + + update() { + this.characters.forEach((character) => { + if ("update" in character) { + (character as GameCharacter).update(); + } + }); + } +} diff --git a/src/objects/GameCharacter.ts b/src/objects/GameCharacter.ts new file mode 100644 index 00000000..6ef120da --- /dev/null +++ b/src/objects/GameCharacter.ts @@ -0,0 +1,50 @@ +import Phaser from "phaser"; +import MainScene from "../scenes/mainScene"; + +export abstract class GameCharacter extends Phaser.Physics.Arcade.Sprite { + name: string; + health: number; + position: Phaser.Math.Vector2; + alive: boolean; + cost: number; + dmg: number; + + constructor( + scene: Phaser.Scene, + name: string, + health: number, + x: number, + y: number, + texture: string, + cost: number, + dmg: number + ) { + super(scene, x, y, texture); // Call the super constructor with necessary parameters + this.scene.add.existing(this); // Add this sprite to the scene + this.name = name; + this.health = health; + this.position = new Phaser.Math.Vector2(x, y); + this.alive = true; + this.cost = cost; + scene.physics.add.existing(this); + this.setOrigin(0.5, 0.5); // Set the origin of the sprite + this.dmg = dmg; + } + + abstract attack(): void; + + // Method for the character to take damage. Reduces health and checks for death. + takeDamage(damage: number): void { + this.health -= damage; + if (this.health <= 0) { + this.alive = false; + (this.scene as MainScene).characterManager.removeCharacter(this); + } + } + + // Method to remove the character from the game, e.g., when health is 0. + remove(): void { + this.destroy(); // Just destroy this sprite + console.log(`${this.name} has been removed from the game.`); + } +} diff --git a/src/objects/Projectile.ts b/src/objects/Projectile.ts new file mode 100644 index 00000000..e2cd782c --- /dev/null +++ b/src/objects/Projectile.ts @@ -0,0 +1,23 @@ +import Phaser from "phaser"; + +export class Projectile extends Phaser.Physics.Arcade.Sprite { + damage: number; + constructor( + scene: Phaser.Scene, + x: number, + y: number, + texture: string, + damage: number + ) { + super(scene, x, y, texture); + scene.add.existing(this); + scene.physics.add.existing(this); + this.damage = damage; + } + + update(): void { + if (this.x > this.scene.sys.canvas.width) { + this.destroy(); // Remove the projectile if it goes off screen + } + } +} diff --git a/src/objects/RangerChar.ts b/src/objects/RangerChar.ts new file mode 100644 index 00000000..447ef96a --- /dev/null +++ b/src/objects/RangerChar.ts @@ -0,0 +1,23 @@ +import Phaser from "phaser"; +import { GameCharacter } from "./GameCharacter"; +import { Projectile } from "./Projectile"; +import MainScene from "../scenes/mainScene"; + +export class Ranger extends GameCharacter { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, "ranger", 100, x, y, "rangerTexture", 30, 50); + } + + attack(): void { + console.log(`${this.name} is attacking.`); + let projectile = new Projectile( + this.scene, + this.x, + this.y, + "arrowTexture", + this.dmg + ); + projectile.body?.setSize(40, 10); + (this.scene as MainScene).projectiles?.add(projectile); // Cast to MainScene to access projectiles + } +} diff --git a/src/objects/SoldierChar.ts b/src/objects/SoldierChar.ts new file mode 100644 index 00000000..5a093175 --- /dev/null +++ b/src/objects/SoldierChar.ts @@ -0,0 +1,23 @@ +import Phaser from "phaser"; +import { GameCharacter } from "./GameCharacter"; +import { Projectile } from "./Projectile"; +import MainScene from "../scenes/mainScene"; + +export class Soldier extends GameCharacter { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, "soldier", 120, x, y, "soldierTexture", 10, 10); + } + + attack(): void { + console.log(`${this.name} is attacking.`); + let projectile = new Projectile( + this.scene, + this.x, + this.y, + "fireBallTexture", + this.dmg + ); + projectile.body?.setSize(40, 10); + (this.scene as MainScene).projectiles?.add(projectile); // Cast to MainScene to access projectiles + } +} diff --git a/src/objects/WizardChar.ts b/src/objects/WizardChar.ts new file mode 100644 index 00000000..416b58d3 --- /dev/null +++ b/src/objects/WizardChar.ts @@ -0,0 +1,23 @@ +import Phaser from "phaser"; +import { GameCharacter } from "./GameCharacter"; +import { Projectile } from "./Projectile"; +import MainScene from "../scenes/mainScene"; + +export class Wizard extends GameCharacter { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, "wizard", 100, x, y, "wizardTexture", 20, 20); + } + + attack(): void { + console.log(`${this.name} is attacking.`); + let projectile = new Projectile( + this.scene, + this.x, + this.y, + "energyBallTexture", + this.dmg + ); + projectile.body?.setSize(40, 10); + (this.scene as MainScene).projectiles?.add(projectile); // Cast to MainScene to access projectiles + } +} diff --git a/src/objects/Zombie1Char.ts b/src/objects/Zombie1Char.ts new file mode 100644 index 00000000..4334430c --- /dev/null +++ b/src/objects/Zombie1Char.ts @@ -0,0 +1,31 @@ +import Phaser from "phaser"; +import MainScene from "../scenes/mainScene"; +import { BaddyCharacter } from "./baddyCharacter"; + +export class Zombie1 extends BaddyCharacter { + constructor(scene: Phaser.Scene, x: number, y: number) { + super(scene, "Zombie1", 50, x, y, "zombieTexture", 10, 1); + } + + attack(): void { + console.log(`${this.name} attacked base.`); + } + takeDamage(damage: number): void { + this.health -= damage; + if (this.health <= 0) { + this.alive = false; + this.dropCurrency(); // Drop currency when the zombie dies + (this.scene as MainScene).baddiesManager.removeCharacter( + "Zombie1", + this + ); + } + console.log(`${this.name} is taking damage.`); + } + + dropCurrency() { + const scene = this.scene as MainScene; + const currencyDrop = 5; // Fixed amount of currency to drop + scene.increaseCurrency(currencyDrop); + } +} diff --git a/src/objects/baddiesManager.ts b/src/objects/baddiesManager.ts new file mode 100644 index 00000000..e746bd7c --- /dev/null +++ b/src/objects/baddiesManager.ts @@ -0,0 +1,62 @@ +import Phaser from "phaser"; +import { BaddyCharacter } from "./baddyCharacter"; +import { Zombie1 } from "./Zombie1Char"; // Assuming Zombie1 is a specific class extending BaddyCharacter +import MainScene from "../scenes/mainScene"; +import TutorialScene from "../scenes/tutorialScene"; + +export class BaddiesManager { + public baddies: Phaser.Physics.Arcade.Group; + mainScene: MainScene | TutorialScene; + size: number; + + constructor(scene: MainScene | TutorialScene) { + this.mainScene = scene; + this.size = 0; + + this.baddies = this.mainScene.physics.add.group({ + classType: Zombie1, + key: "Zombie1", + }); + } + + addCharacter(type: string, baddy: BaddyCharacter) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + //const typeKey = type.charAt(0).toUpperCase() + type.slice(1); // Ensure first letter is uppercase + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.baddies) { + //a bunch of if statements to handle configs for each bad guy + if (type === "Zombie1") { + baddy.setY( + this.mainScene.board_map.getRandomCellPosition().y + 60 + ); + baddy.setVelocityX(Phaser.Math.FloatBetween(-50, -10)); + baddy.setPushable(false); + baddy.setScale(1.1); // Now you can safely apply setScale + baddy.setOrigin(0.5, 0.95); // Adjusting origin for better alignment + baddy.body?.setSize(20, 55); //set hitbox size + + this.baddies.add(baddy); + this.size++; + } + } else { + console.warn(`No group found for type ${type}`); + } + } + + removeCharacter(type: string, baddy: Phaser.GameObjects.GameObject) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.baddies) { + baddy.destroy(); + this.baddies.remove(baddy, true, true); + this.size--; + } + } + + update() { + this.baddies.children.iterate((child) => { + const zombie = child as Phaser.Physics.Arcade.Sprite; + zombie.setVelocityX(Phaser.Math.FloatBetween(-50, -10)); + return true; + }); + } +} diff --git a/src/objects/baddyCharacter.ts b/src/objects/baddyCharacter.ts new file mode 100644 index 00000000..bc8b8e36 --- /dev/null +++ b/src/objects/baddyCharacter.ts @@ -0,0 +1,21 @@ +import Phaser from "phaser"; +import { GameCharacter } from "./GameCharacter"; + +export abstract class BaddyCharacter extends GameCharacter { + difficulty: number; + constructor( + scene: Phaser.Scene, + name: string, + health: number, + x: number, + y: number, + texture: string, + dmg: number, + difficulty: number + ) { + super(scene, name, health, x, y, texture, 0, dmg); + this.difficulty = difficulty; + } + + abstract dropCurrency(): void; +} diff --git a/src/objects/board.ts b/src/objects/board.ts new file mode 100644 index 00000000..a5391026 --- /dev/null +++ b/src/objects/board.ts @@ -0,0 +1,116 @@ +// Board.ts +import Phaser from "phaser"; + +interface BoardConfig { + rows: number; + cols: number; + cellWidth: number; + cellHeight: number; + posX: number; + posY: number; +} + +interface Cell { + col: number; + row: number; + x: number; + y: number; + text: Phaser.GameObjects.Text; // Add this line +} + +export class Board { + scene: Phaser.Scene; + config: BoardConfig; + cells: Cell[][]; + x: number; + y: number; + width: number; + height: number; + + constructor(scene: Phaser.Scene, config: BoardConfig) { + this.scene = scene; + this.config = config; + this.x = config.posX; + this.y = config.posY; + this.width = config.cols * config.cellWidth; + this.height = config.rows * config.cellHeight; + this.cells = this.createBoard(); + } + + private createBoard(): Cell[][] { + let cells: Cell[][] = []; + + for (let row = 0; row < this.config.rows; row++) { + let rowCells: Cell[] = []; + + for (let col = 0; col < this.config.cols; col++) { + const { cellWidth, cellHeight, posX, posY } = this.config; + const x = posX + col * cellWidth + cellWidth / 2; + const y = posY + row * cellHeight + cellHeight / 2; + + const text = this.scene.add + .text(x, y, `(${col}, ${row})`, { + color: "#000000", + fontSize: "16px", + align: "center", + }) + .setOrigin(0.5); // Center text in the cell + + const cell: Cell = { + col, + row, + x, + y, + text: text, // Store the text object in the cell + }; + + rowCells.push(cell); + } + + cells.push(rowCells); + } + + return cells; + } + + getCellPosition(col: number, row: number) { + if ( + row >= 0 && + row < this.config.rows && + col >= 0 && + col < this.config.cols + ) { + const cell = this.cells[row][col]; + return { x: cell.x, y: cell.y }; + } + return { x: 0, y: 0 }; + } + + getRandomCellPosition() { + const row = Phaser.Math.Between(0, this.config.rows - 1); + const col = Phaser.Math.Between(0, this.config.cols - 1); + const cell = this.cells[row][col]; + return { x: cell.x, y: cell.y }; + } + + isWithinBounds(x: number, y: number) { + return ( + x >= this.x && + x <= this.x + this.width && + y >= this.y && + y <= this.y + this.height + 60 + ); + } + + public getCellFromCoordinates(x: number, y: number): Cell | null { + for (let row = 0; row < this.config.rows; row++) { + for (let col = 0; col < this.config.cols; col++) { + const cell = this.cells[row][col]; + if (x === cell.x && y === cell.y) { + return cell; + } + } + } + return null; + } +} diff --git a/src/objects/commandLine.ts b/src/objects/commandLine.ts new file mode 100644 index 00000000..f463c06f --- /dev/null +++ b/src/objects/commandLine.ts @@ -0,0 +1,166 @@ +// CommandLine.ts +import Phaser from "phaser"; +import { FolderSystem } from "../objects/folderSystem"; +import { CharacterManager } from "../objects/CharacterManager"; +import MainScene from "../scenes/mainScene"; +import { WaveManager } from "./waveManager"; +import TutorialScene from "../scenes/tutorialScene"; + +export class CommandLine { + folderSystem: FolderSystem; + private outputText: Phaser.GameObjects.Text; + private prompt: string = "$ "; + characterManager: CharacterManager; + mainScene: MainScene | TutorialScene; + private inputBox: HTMLInputElement; + waveManager: WaveManager; + + constructor( + scene: MainScene | TutorialScene, + characterManager: CharacterManager, + waveManager: WaveManager + ) { + this.folderSystem = new FolderSystem(); + this.characterManager = characterManager; + this.mainScene = scene; + this.waveManager = waveManager; + } + + public processCommand(command: string) { + const tokens = command.split(" "); + const mainCommand = tokens[0]; + const arg1 = tokens[1] ?? ""; + const arg2 = parseFloat(tokens[2] ?? ""); + const arg3 = parseFloat(tokens[3] ?? ""); + + let output = ""; + + switch (mainCommand) { + case "cd": + output = this.folderSystem.changeDirectory(arg1); + break; + case "ls": + output = this.folderSystem.listContents(); + break; + case "mv": + output = this.purchaseCharacter(arg1, arg2, arg3); + break; + case "rm": + output = this.removeCharacter(arg1, arg2, arg3); + break; + case "startwave": + // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression + output = this.waveManager.startNextWave(); + break; + case "cat": + output = this.folderSystem.readFileContent(arg1); + break; + case "display-health": + output = `Current health: ${this.mainScene.health}`; + break; + case "display-currency": + output = `Current amount of currency: ${this.mainScene.currency}`; + break; + case "display-wave": + output = `Current wave: ${this.waveManager.currentWaveIndex}`; + break; + default: + output = `Invalid command: ${command}`; + break; + } + + this.updateOutputText(output); + } + + private purchaseCharacter( + characterName: string, + x: number, + y: number + ): string { + if (isNaN(x) || isNaN(y)) { + return `Invalid coordinates. Usage: purchase `; + } + + const characterPrice = this.folderSystem + .getRootFolder() + .children?.find((folder) => folder.name === "characters") + ?.children?.find( + (character) => character.name === characterName + )?.price; + + if (!characterPrice) { + return `Character not found: ${characterName}`; + } + + if (characterPrice > this.mainScene.currency) { + return `Insufficient currency to purchase ${characterName}`; + } + + const coordsToBoard = this.mainScene.board_map.getCellPosition(x, y); + + if ( + !this.mainScene.board_map.isWithinBounds( + coordsToBoard.x, + coordsToBoard.y + ) + ) { + return `Invalid coordinates (${x}, ${y})`; + } + + //create statement that wont allow purchase if another char in same position + if (this.characterManager.sameCoord(coordsToBoard.x, coordsToBoard.y)) { + return `Another character is already in the position (${x}, ${y})`; + } + + const gameCharacter = this.folderSystem.createCharacter( + characterName, + this.mainScene, + coordsToBoard.x, + coordsToBoard.y + ); + + if (!gameCharacter) { + return `Failed to instantiate ${characterName}`; + } + this.mainScene.increaseCurrency(-characterPrice); + this.characterManager.addCharacter(gameCharacter); + + return `Purchased ${characterName} for ${characterPrice} and placed it at (${x}, ${y})`; + } + + private removeCharacter( + characterName: string, + x: number, + y: number + ): string { + if (isNaN(x) || isNaN(y)) { + return `Invalid coordinates. Usage: remove `; + } + const coordsToBoard = this.mainScene.board_map.getCellPosition(x, y); + + if ( + !this.mainScene.board_map.isWithinBounds( + coordsToBoard.x, + coordsToBoard.y + ) + ) { + return `Invalid coordinates (${x}, ${y})`; + } + + if ( + this.characterManager.removeCharacterByNameAndPosition( + characterName, + coordsToBoard.x, + coordsToBoard.y + ) + ) { + return `Removed ${characterName} at position (${x}, ${y})`; + } + + return `Character not found: ${characterName} at (${x}, ${y})`; + } + + private updateOutputText(output: string) { + this.mainScene.updateOutputText(output); + } +} diff --git a/src/objects/folderSystem.ts b/src/objects/folderSystem.ts new file mode 100644 index 00000000..20a92bad --- /dev/null +++ b/src/objects/folderSystem.ts @@ -0,0 +1,209 @@ +// FolderSystem.ts +import { GameCharacter } from "./GameCharacter"; +import Phaser from "phaser"; +import { Soldier } from "./SoldierChar"; +import { Ranger } from "./RangerChar"; +import { Wizard } from "./WizardChar"; + +interface Folder { + name: string; + type: "folder" | "file"; + children?: Folder[]; + price?: number; + health?: number; + damage?: number; + createCharacter?: ( + scene: Phaser.Scene, + x: number, + y: number + ) => GameCharacter; + content?: string; // Text content for .txt files +} + +export class FolderSystem { + private rootFolder: Folder = { + name: "/", + type: "folder", + children: [ + { + name: "characters", + type: "folder", + children: [ + { + name: "soldier", + type: "folder", + children: [ + { + name: "soldier.txt", + type: "file", + content: + "Name: Soldier\nCost: 10\nHealth: 100\nDamage: 20", + }, + ], + price: 10, + health: 100, + damage: 20, + createCharacter: (scene, x, y) => + new Soldier(scene, x, y), + }, + { + name: "ranger", + type: "folder", + children: [ + { + name: "ranger.txt", + type: "file", + content: + "Name: Ranger\nCost: 20\nHealth: 100\nDamage: 50", + }, + ], + price: 30, + health: 100, + damage: 50, + createCharacter: (scene, x, y) => + new Ranger(scene, x, y), + }, + { + name: "wizard", + type: "folder", + children: [ + { + name: "wizard.txt", + type: "file", + content: + "Name: Wizard\nCost: 30\nHealth: 100\nDamage: 20", + }, + ], + price: 20, + health: 100, + damage: 20, + createCharacter: (scene, x, y) => + new Wizard(scene, x, y), + }, + ], + }, + { + name: "baddies", + type: "folder", + children: [ + { + name: "zombie1", + type: "folder", + children: [ + { + name: "zombie1.txt", + type: "file", + content: + "Name: Zombie1\nCost: 0\nHealth: 50\nDamage: 10", + }, + ], + price: 0, + health: 50, + damage: 10, + }, + ], + }, + ], + }; + + private currentPath: string[] = []; + + public findFolderByPath(path: string[]): Folder | null { + let currentFolder: Folder = this.rootFolder; + for (const part of path) { + if (part === "") continue; // Ignore empty parts from leading slashes or errors + let foundFolder = currentFolder.children?.find( + (f) => f.name === part && f.type === "folder" + ); + if (!foundFolder) return null; // Early exit if any part of the path is not found + currentFolder = foundFolder; + } + return currentFolder; + } + + public changeDirectory(folderName: string): string { + if (folderName === "..") { + if (this.currentPath.length > 0) { + this.currentPath.pop(); + } + } else { + const currentFolder = this.findFolderByPath(this.currentPath); + const targetFolder = currentFolder?.children?.find( + (child) => child.name === folderName && child.type === "folder" + ); + + if (targetFolder) { + this.currentPath.push(folderName); + } else { + return `Folder not found: ${folderName}`; + } + } + return this.listContents(); + } + + public listContents(): string { + const currentFolder = this.findFolderByPath(this.currentPath); + if (!currentFolder) { + return `Error: Path not found`; + } + + const folderContents = currentFolder.children?.map((child) => { + return `${child.type === "folder" ? "[D]" : "[F]"} ${child.name}`; + }); + + return ( + `Current Path: /${this.currentPath.join("/") || ""}\n` + + folderContents?.join("\n") || "" + ); + } + + public getCurrentPath(): string[] { + return this.currentPath; + } + + public getRootFolder(): Folder { + return this.rootFolder; + } + + public readFileContent(fileName: string): string { + const pathParts = fileName.split("/"); + // Check if root directory should be skipped in path navigation + const effectivePath = + pathParts[0] === "" ? pathParts.slice(1) : pathParts; + + const file = this.findFolderByPath( + effectivePath.slice(0, -1) + )?.children?.find( + (f) => + f.name === effectivePath[effectivePath.length - 1] && + f.type === "file" + ); + + return file?.content || "File not found"; + } + + public createCharacter( + characterName: string, + scene: Phaser.Scene, + x: number, + y: number + ): GameCharacter | null { + const charactersFolder = this.rootFolder.children?.find( + (folder) => folder.name === "characters" && folder.type === "folder" + ); + + const character = charactersFolder?.children?.find( + (child) => child.name === characterName && child.type === "folder" + ); + + if ( + !character || + character.type !== "folder" || + !character.createCharacter + ) { + return null; + } + + return character.createCharacter(scene, x, y); + } +} diff --git a/src/objects/instruction.ts b/src/objects/instruction.ts new file mode 100644 index 00000000..7185da98 --- /dev/null +++ b/src/objects/instruction.ts @@ -0,0 +1,46 @@ +import { Directories } from "../interfaces/directories"; +import Phaser from "phaser"; + +export class instruction { + constructor() {} + + public handleInstruction( + lsTut: boolean, + cdTut: boolean, + cdLsTut: boolean, + cdBackTut: boolean, + curDir: string | undefined, + dialogue?: Phaser.GameObjects.Text | undefined + ): Directories { + // Display textbox as commands are entered + dialogue?.setText( + "Welcome General, we must defend the castle!\nUse the spell ls to find your allies" + ); + if (lsTut) { + dialogue?.setText( + "ls is used to list out all the contents in a given directory.\nSpeaking of directories type cd HiddenText" + ); + curDir = "HiddenText"; + } + if (cdTut) { + dialogue?.setText( + "Using cd allows you to change the directory you are looking at\nNow using what you just learned see whats in this folder" + ); + } + if (cdLsTut) { + dialogue?.setText( + "Well done General, time to get back to the begining.\nType cd.." + ); + } + if (cdBackTut) { + dialogue?.setText( + "Great now there is one last spell which will be used later to place your troops.\nYou will use mv to place your troops" + ); + curDir = ""; + } + return { + curDir, + dialogue, + }; + } +} diff --git a/src/objects/text.ts b/src/objects/text.ts new file mode 100644 index 00000000..b1a2a3d8 --- /dev/null +++ b/src/objects/text.ts @@ -0,0 +1,12 @@ +import Phaser from "phaser"; +export default class FpsText extends Phaser.GameObjects.Text { + constructor(scene: Phaser.Scene) { + super(scene, 10, 10, "", { color: "black", fontSize: "28px" }); + scene.add.existing(this); + this.setOrigin(0); + } + + public update() { + this.setText(`fps: ${Math.floor(this.scene.game.loop.actualFps)}`); + } +} diff --git a/src/objects/wave.ts b/src/objects/wave.ts new file mode 100644 index 00000000..419f8e98 --- /dev/null +++ b/src/objects/wave.ts @@ -0,0 +1,59 @@ +import Phaser from "phaser"; +import { BaddiesManager } from "./baddiesManager"; +import { Zombie1 } from "./Zombie1Char"; +import MainScene from "../scenes/mainScene"; +import TutorialScene from "../scenes/tutorialScene"; + +export class Wave { + private manager: BaddiesManager; + mainScene: MainScene | TutorialScene; + private totalEnemies: number; + private enemiesSpawned: number = 0; + private spawnInterval: number; // Time between spawns + + constructor( + scene: MainScene | TutorialScene, + manager: BaddiesManager, + totalEnemies: number, + spawnInterval: number + ) { + this.mainScene = scene; + this.manager = manager; + this.totalEnemies = totalEnemies; + this.spawnInterval = spawnInterval; + } + + //might want to remove delay + start() { + this.mainScene.time.addEvent({ + delay: this.spawnInterval, + repeat: this.totalEnemies - 1, + callback: () => { + this.spawnEnemy(); + }, + callbackScope: this, + }); + } + + //once we have more baddies we will have to implement a randomizer that randomizes types of baddies + //that spawn depending on the difficulty. For now logic is for only one zommbie type + spawnEnemy() { + const x = 950; // Starting X position for enemies {might need to randomize a bit; not really?} + const y = this.mainScene.spawn_board.getRandomCellPosition().y + 60; + const zombie1 = new Zombie1(this.mainScene, x, y); // Parameters might need to be adjusted + zombie1.setVelocityX(Phaser.Math.FloatBetween(-50, -10)); // zombie speed + zombie1.setPushable(false); + zombie1.setScale(1.1); // Now you can safely apply setScale + zombie1.setOrigin(0.5, 0.95); // Adjusting origin for better alignment + zombie1.body?.setSize(20, 55); //sets the hitbox size for the zombies + this.manager.addCharacter("Zombie1", zombie1); + this.enemiesSpawned++; + } + + //might have a bug here soon + isComplete() { + return ( + this.enemiesSpawned >= this.totalEnemies && this.manager.size === 0 + ); + } +} diff --git a/src/objects/waveManager.ts b/src/objects/waveManager.ts new file mode 100644 index 00000000..4cc72ecc --- /dev/null +++ b/src/objects/waveManager.ts @@ -0,0 +1,49 @@ +import MainScene from "../scenes/mainScene"; +import TutorialScene from "../scenes/tutorialScene"; +import { BaddiesManager } from "./baddiesManager"; +import { Wave } from "./wave"; + +export class WaveManager { + private waves: Wave[]; + public currentWaveIndex: number = 0; + mainScene: MainScene | TutorialScene; + baddiesManager: BaddiesManager; + isWaveActive: boolean = false; // Track if a wave is active + + constructor( + scene: MainScene | TutorialScene, + baddiesManager: BaddiesManager + ) { + this.mainScene = scene; + this.baddiesManager = baddiesManager; + this.waves = [ + new Wave(scene, baddiesManager, 10, 2000), + new Wave(scene, baddiesManager, 15, 1800), + new Wave(scene, baddiesManager, 20, 1500), + ]; + } + + startNextWave(): string { + if (!this.isWaveActive && this.currentWaveIndex < this.waves.length) { + this.isWaveActive = true; // Set the wave as active + this.waves[this.currentWaveIndex++].start(); + return "Wave started"; + } else if (this.currentWaveIndex >= this.waves.length) { + return "Cannot start next wave, this is the last wave"; + } else { + return "A wave is currently active. Wait until it finishes."; + } + } + + update() { + // Here, you might check conditions to set isWaveActive to false when a wave ends + if ( + this.isWaveActive && + this.baddiesManager.size === 0 && + this.waves[this.currentWaveIndex - 1].isComplete() + ) { + this.isWaveActive = false; + console.log("Wave completed. You can now start a new wave."); + } + } +} diff --git a/src/scenes/mainScene.ts b/src/scenes/mainScene.ts index 1c6b6089..227bbad5 100644 --- a/src/scenes/mainScene.ts +++ b/src/scenes/mainScene.ts @@ -1,28 +1,403 @@ import Phaser from "phaser"; -import PhaserLogo from "../objects/phaserLogo"; +//import PhaserLogo from "../objects/phaserLogo"; import FpsText from "../objects/fpsText"; +import { Projectile } from "../objects/Projectile"; +import { CharacterManager } from "../objects/CharacterManager"; +import { GameCharacter } from "../objects/GameCharacter"; +import { Board } from "../objects/board"; +import { CommandLine } from "../objects/commandLine"; +import { WaveManager } from "../objects/waveManager"; +import { BaddiesManager } from "../objects/baddiesManager"; +import { Zombie1 } from "../objects/Zombie1Char"; +import { BaddyCharacter } from "../objects/baddyCharacter"; export default class MainScene extends Phaser.Scene { + private inputBox: HTMLInputElement; + private outputBox?: Phaser.GameObjects.Text; + private edge: Phaser.Physics.Arcade.StaticGroup; + //private grunts?: Phaser.Physics.Arcade.Group; + waveManager: WaveManager; + baddiesManager: BaddiesManager; + public projectiles?: Phaser.Physics.Arcade.Group; fpsText: FpsText; + public board_map: Board; + private map_boardConfig = { + rows: 5, + cols: 7, + cellWidth: 85, + cellHeight: 100, + posX: 280, // Centered X position for the board + posY: 150, // Centered Y position for the board + }; + public spawn_board: Board; + private spawn_boardConfig = { + rows: 5, + cols: 5, + cellWidth: 85, + cellHeight: 100, + posX: 870, // Centered X position for the board + posY: 150, // Centered Y position for the board + }; + private gameOver = false; + private gruntAmount = 50; + private currentWave = 0; + private maxWave = 5; + //private score = 0; + private scoreText: Phaser.GameObjects.Text; + public currency: number; // Player currency + public health: number; // Health of the base + private healthBar: Phaser.GameObjects.Graphics; + private end: Phaser.Physics.Arcade.StaticGroup; + private finish: Phaser.Physics.Arcade.StaticGroup; + public characterManager: CharacterManager; //is a list of all the characters + private userInput: string = ""; + private consoleDialogue?: Phaser.GameObjects.Text; + private eventEmitter = new Phaser.Events.EventEmitter(); + private instructionDialogue?: Phaser.GameObjects.Text; + private won: boolean = false; + private readonly prompt: string = ""; + currencyText: Phaser.GameObjects.Text; + private commandLine?: CommandLine; + gameMusic: Phaser.Sound.BaseSound; constructor() { super({ key: "MainScene" }); + this.characterManager = new CharacterManager(); + this.health = 100; // Initialize health + this.currency = 50; // Starting currency } create() { - new PhaserLogo(this, this.cameras.main.width / 2, 0); + //map board + this.add.image(400, 350, "map").setScale(1); + this.board_map = new Board(this, this.map_boardConfig); + //background audio + this.gameMusic = this.sound.add("backgroundMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + //Win and Lose images + //enemy spawn board + this.spawn_board = new Board(this, this.spawn_boardConfig); + this.baddiesManager = new BaddiesManager(this); + + this.waveManager = new WaveManager(this, this.baddiesManager); + //currency + // Add currency text + this.currencyText = this.add.text( + 500, + 5, + `Currency: ${this.currency}`, + { + fontSize: "30px", + color: "white", + } + ); + //help button + var graphics = this.add.graphics(); + // Fill the background with grey color + graphics.fillStyle(0x808080, 1); // Grey color + graphics.fillRoundedRect(125, 50, 600, 115, 20); + + // Create a Text object for the text + var helpText = this.add.text( + 150, + 50, + "cd:Change Directory ; ls:View content of file \n purchase(unit name,xCoord,yCoord): buy units \n remove(unit name,xCoord,yCoord): remove units \nstartwave:Starts the wave;cat(unit):view info about unit", + { + fontFamily: "Arial", + fontSize: 24, + color: "#ffffff", // White color for text + } + ); + + // Make the text appear above the background + helpText.setDepth(1); + graphics.setVisible(false); + helpText.setVisible(false); + let help = this.add.image(800, 100, "questionMark").setInteractive(); + help.setScale(1 / 2); + help.on("pointerover", () => { + graphics.setVisible(true); + helpText.setVisible(true); + console.log("hovered over"); + }); + help.on("pointerout", () => { + graphics.setVisible(false); + helpText.setVisible(false); + console.log("not hovered over"); + }); + + //health bar stuff + this.health = 100; // Starting health + this.healthBar = this.add.graphics(); + this.updateHealthBar(); + + this.commandLine = new CommandLine( + this, + this.characterManager, + this.waveManager + ); + + this.time.addEvent({ + delay: 2000, // Attack every 2000 ms (2 seconds) + callback: () => { + this.characterManager.characters.forEach((character) => { + const gameCharacter = character as GameCharacter; + gameCharacter.attack(); + }); + }, + loop: true, + }); + + // When creating projectiles, ensure they are active and can interact + this.projectiles = this.physics.add.group({ + classType: Projectile, + runChildUpdate: true, // Ensures projectile update logic is executed + }); + + this.edge = this.physics.add.staticGroup(); + let platform = this.edge.create( + 270, + 400, + "platform" + ) as Phaser.Physics.Arcade.Sprite; + // After rotating the platform + platform.angle = 90; + // Manually set the size of the physics body + platform.body?.setSize(30, 500); + platform.setVisible(false); + + this.physics.add.collider( + this.baddiesManager.baddies, + this.edge, + (zombie, platform) => { + if ( + zombie instanceof Zombie1 && + platform instanceof Phaser.Physics.Arcade.Sprite + ) { + this.handleHitWall(zombie); + } + }, + undefined, + this + ); + + // Command Line Console + this.consoleDialogue = this.add.text(100, 160, "", { + fontSize: "24px", + color: "green", + backgroundColor: "#000000", + }); + this.fpsText = new FpsText(this); - const message = `Phaser v${Phaser.VERSION}`; - this.add - .text(this.cameras.main.width - 15, 15, message, { - color: "#000000", - fontSize: "24px", - }) - .setOrigin(1, 0); + this.outputBox = this.createOutputBox(870, 0, 410, 650); + + this.inputBox = this.createInputBox(670, 575, 310); + + //this.score = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + + // let scoreText = this.add.text( + // this.currencyText.x, + // this.currencyText.y + 50, + // "Score: 0", + // { + // fontSize: "20px", + // color: "white", + // } + // ); + + + this.physics.add.collider( + this.baddiesManager.baddies, + this.edge, + (zombie, platform) => { + if ( + zombie instanceof Zombie1 && + platform instanceof Phaser.Physics.Arcade.Sprite + ) { + this.handleHitWall(zombie); + //this.shakeScreen(zombie, platform); + } + }, + undefined, + this + ); + + this.physics.add.collider( + this.baddiesManager.baddies, + this.projectiles, + (zombie, projectile) => { + // Cast zombie and projectile to their expected types + let z = zombie as Phaser.Physics.Arcade.Sprite; + let p = projectile as Phaser.Physics.Arcade.Sprite; + + if (this.board_map.isWithinBounds(z.x, z.y)) { + // Ensure the collision affects only those zombies within the board + if (z instanceof Zombie1 && p instanceof Projectile) { + z.takeDamage(p.damage); + this.flashEnemy(z); + } + } + // Destroy the projectile regardless of the zombie's position + p.destroy(); + } + ); + if (this.gruntAmount === 0) { + this.win(); + } + } + // private shakeScreen( + // zombie: Zombie1, + // platform: Phaser.Physics.Arcade.Sprite + // ) { + // this.cameras.main.shake(500, 0.01); + // } + + private enemyHitWall() { + console.log("hit wall enemy"); + } + + private handleHitWall(baddy: BaddyCharacter): void { + // Assume each collision with the platform causes a fixed amount of damage + this.health -= baddy.dmg; + this.updateHealthBar(); + + if (this.health <= 50) { + this.gameMusic.stop(); + this.gameMusic = this.sound.add("endGameMusic"); + this.gameMusic.play(); + } + + if (this.health <= 0) { + this.lose(); + } + this.baddiesManager.removeCharacter("Zombie1", baddy); + } + flashEnemy(zombie: Zombie1) { + const originalTint = zombie.tint; + zombie.setTint(0xff0000); + setTimeout(() => { + zombie.setTint(originalTint); + }, 500); + } + private win() { + this.gameMusic.stop(); + this.gameMusic = this.sound.add("victoryMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + let winImg = this.add.image(400, 400, "youWin").setInteractive(); + winImg.setScale(0.9); + winImg.on("pointerup", () => { + console.log("button pushed"); + this.scene.restart(); + }); + this.physics.pause(); + this.won = true; + + console.log("Game Won"); + } + private lose() { + this.gameMusic.stop(); + this.gameMusic = this.sound.add("defeatMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + let loseImg = this.add.image(400, 400, "youLose").setInteractive(); + loseImg.setScale(0.9); + loseImg.on("pointerup", () => { + console.log("button pushed"); + this.scene.restart(); + }); + this.gameOver = true; + this.physics.pause(); + console.log("Game Over"); + } + + updateHealthBar() { + this.healthBar.clear(); + this.healthBar.fillStyle(0x00ff00, 1); + this.healthBar.fillRect(200, 10, 200 * (this.health / 100), 20); + } + + increaseCurrency(amount: number) { + this.currency += amount; + this.currencyText.setText(`Currency: ${this.currency}`); + } + + private createInputBox( + x: number, + y: number, + width: number + ): HTMLInputElement { + const input = document.createElement("input"); + input.type = "text"; + input.style.position = "fixed"; // Change to `fixed` position + input.style.width = `${width}px`; + input.style.fontSize = "20px"; + input.style.backgroundColor = "#000"; + input.style.color = "green"; + input.style.border = "1px solid #888"; + input.style.padding = "5px"; + input.style.outline = "none"; + input.style.zIndex = "1"; + + document.body.appendChild(input); + + const updateInputPosition = () => { + const canvas = this.game.canvas; + const rect = canvas.getBoundingClientRect(); // Get the canvas's bounding rectangle + + input.style.left = `${rect.left + x}px`; // Use the canvas's position for `left` + input.style.top = `${rect.top + y}px`; // Use the canvas's position for `top` + }; + + // Set the initial position + updateInputPosition(); + + // Update the position on window resize + window.addEventListener("resize", updateInputPosition); + + // Process the command on 'Enter' key + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + const command = input.value; + this.commandLine?.processCommand(command); + input.value = ""; + } + }); + + return input; + } + + private createOutputBox( + x: number, + y: number, + width: number, + height: number + ): Phaser.GameObjects.Text { + const outputBox = this.add.text(x, y, "", { + fontSize: "24px", + color: "green", + backgroundColor: "#000000", + wordWrap: { width: width }, + padding: { top: 10, bottom: 10, left: 10, right: 10 }, + }); + outputBox.setFixedSize(width, height); + return outputBox; + } + + updateOutputText(output: string) { + this.outputBox?.setText(output); } update() { this.fpsText.update(); + this.waveManager.update(); + + this.projectiles?.children.iterate((child) => { + const projectile = child as Projectile; // Ensure correct casting + projectile.setVelocityX(300); + return true; + }); + this.characterManager.update(); + this.baddiesManager.update(); } } diff --git a/src/scenes/preloadScene.ts b/src/scenes/preloadScene.ts index c17b81ba..9991c0bc 100644 --- a/src/scenes/preloadScene.ts +++ b/src/scenes/preloadScene.ts @@ -6,10 +6,45 @@ export default class PreloadScene extends Phaser.Scene { } preload() { + //images this.load.image("phaser-logo", "assets/img/phaser-logo.png"); + this.load.image("grass", "assets/img/grass-sm.png"); + this.load.image("trainGrounds", "assets/img/trainGrounds.png"); + this.load.image("dude", "assets/img/dude.png"); + this.load.image("finishLine", "assets/img/finishLine.png"); + this.load.image("platform", "assets/img/platform.png"); + this.load.image("button", "assets/img/button.jpg"); + this.load.image("rangerTexture", "assets/img/ranger-small.png"); + this.load.image("soldierTexture", "assets/img/soldier-small.png"); + this.load.image("zombieTexture", "assets/img/zombie-small.png"); + this.load.image("arrowTexture", "assets/img/arrow-small.png"); + this.load.image("wizardTexture", "assets/img/wizard-small.png"); + this.load.image("tree", "assets/img/tree.png"); + this.load.image("fort", "assets/img/file.jpg"); + this.load.image("left-trees", "assets/img/left-trees.jpg"); + this.load.image("right-trees", "assets/img/right-trees.jpg"); + this.load.image("map", "assets/img/map1.png"); + this.load.image("energyBallTexture", "assets/img/energy-ball-sm.png"); + this.load.image("fireBallTexture", "assets/img/fire-ball-sm.png"); + this.load.image("youLose", "assets/img/youLose.jpeg"); + this.load.image("youWin", "assets/img/youWin.jpeg"); + this.load.image("questionMark", "assets/img/questionMark.png"); + this.load.image("wizardNPC", "assets/img/wizardNPC.jpg"); + this.load.image("titlePage", "assets/img/titlePage.jpeg"); + + //audio + this.load.audio("backgroundMusic", "assets/audio/backgroundMusic.mp3"); + this.load.audio("tutorialMusic", "assets/audio/tutorialMusic.mp3"); + this.load.audio("endGameMusic", "assets/audio/endGameMusic.mp3"); + this.load.audio("victoryMusic", "assets/audio/victoryMusic.mp3"); + this.load.audio("defeatMusic", "assets/audio/defeatMusic.mp3"); + this.load.audio( + "titleScreenMusic", + "assets/audio/titleScreenMusic.mp3" + ); } create() { - this.scene.start("MainScene"); + this.scene.start("TitleScene"); } } diff --git a/src/scenes/titleScene.ts b/src/scenes/titleScene.ts new file mode 100644 index 00000000..668d60c3 --- /dev/null +++ b/src/scenes/titleScene.ts @@ -0,0 +1,61 @@ +import Phaser from "phaser"; + +export default class TitleScene extends Phaser.Scene { + menuMusic: Phaser.Sound.BaseSound; + constructor() { + super({ key: "TitleScene" }); + } + + preload() { + this.load.image("play_button", "assets/img/play_button.png"); + this.load.image("tutorial_icon", "assets/img/tutorial_icon.png"); + } + + create() { + const centerX = this.cameras.main.width / 2; + const centerY = this.cameras.main.height / 2; + this.menuMusic = this.sound.add("titleScreenMusic"); + this.menuMusic.play({ volume: 0.4, loop: true }); + + const background = this.add.image(0, -250, "titlePage").setOrigin(0, 0); + const scaleX = this.cameras.main.width / background.width; + const scaleY = this.cameras.main.height / background.height; + const maxScale = Math.max(scaleX, scaleY); + background.setScale(maxScale).setScrollFactor(0); + + let playButton = this.add + .image(centerX, centerY + 50, "play_button") + .setInteractive(); + + playButton.on("pointerover", () => { + playButton.setScale(1.1); + }); + + playButton.on("pointerout", () => { + playButton.setScale(1); + }); + + playButton.on("pointerup", () => { + this.menuMusic.stop(); + this.scene.start("MainScene"); + }); + + let tutorialButton = this.add + .image(centerX, centerY + 100, "tutorial_icon") + .setInteractive() + .setScale(0.2); + + tutorialButton.on("pointerover", () => { + tutorialButton.setScale(0.25); + }); + + tutorialButton.on("pointerout", () => { + tutorialButton.setScale(0.2); + }); + + tutorialButton.on("pointerup", () => { + this.menuMusic.stop(); + this.scene.start("TutorialScene"); + }); + } +} diff --git a/src/scenes/tutorialScene.ts b/src/scenes/tutorialScene.ts new file mode 100644 index 00000000..5cf009b1 --- /dev/null +++ b/src/scenes/tutorialScene.ts @@ -0,0 +1,501 @@ +import Phaser from "phaser"; +//import PhaserLogo from "../objects/phaserLogo"; +import FpsText from "../objects/fpsText"; +import { Projectile } from "../objects/Projectile"; +import { CharacterManager } from "../objects/CharacterManager"; +import { GameCharacter } from "../objects/GameCharacter"; +import { Board } from "../objects/board"; +import { CommandLine } from "../objects/commandLine"; +import { WaveManager } from "../objects/waveManager"; +import { BaddiesManager } from "../objects/baddiesManager"; +import { Zombie1 } from "../objects/Zombie1Char"; +import { BaddyCharacter } from "../objects/baddyCharacter"; + +export default class TutorialScene extends Phaser.Scene { + private inputBox: HTMLInputElement; + private outputBox?: Phaser.GameObjects.Text; + private edge: Phaser.Physics.Arcade.StaticGroup; + //private grunts?: Phaser.Physics.Arcade.Group; + waveManager: WaveManager; + baddiesManager: BaddiesManager; + public projectiles?: Phaser.Physics.Arcade.Group; + fpsText: FpsText; + public board_map: Board; + private map_boardConfig = { + rows: 5, + cols: 7, + cellWidth: 85, + cellHeight: 100, + posX: 280, // Centered X position for the board + posY: 150, // Centered Y position for the board + }; + public spawn_board: Board; + private spawn_boardConfig = { + rows: 5, + cols: 5, + cellWidth: 85, + cellHeight: 100, + posX: 870, // Centered X position for the board + posY: 150, // Centered Y position for the board + }; + private gameOver = false; + private gruntAmount = 50; + private currentWave = 0; + private maxWave = 5; + private score = 0; + private scoreText: Phaser.GameObjects.Text; + public currency: number; // Player currency + public health: number; // Health of the base + private healthBar: Phaser.GameObjects.Graphics; + private end: Phaser.Physics.Arcade.StaticGroup; + private finish: Phaser.Physics.Arcade.StaticGroup; + public characterManager: CharacterManager; //is a list of all the characters + private userInput: string = ""; + private consoleDialogue?: Phaser.GameObjects.Text; + private eventEmitter = new Phaser.Events.EventEmitter(); + private instructionDialogue?: Phaser.GameObjects.Text; + private won: boolean = false; + private readonly prompt: string = ""; + currencyText: Phaser.GameObjects.Text; + private commandLine?: CommandLine; + gameMusic: Phaser.Sound.BaseSound; + private tutorialText: Phaser.GameObjects.Text; + private curDialogueIdx: number = 0; + private dialogueOptions: string[]; + + private textTimer: number = 0; + private dialogueCharCount: number = 0; + private curDialogueText: string = ""; + + constructor() { + super({ key: "TutorialScene" }); + this.characterManager = new CharacterManager(); + this.health = 100; // Initialize health + this.currency = 50; // Starting currency + } + + create() { + //map board + this.add.image(400, 350, "map").setScale(1); + this.board_map = new Board(this, this.map_boardConfig); + //background audio + this.gameMusic = this.sound.add("backgroundMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + //Win and Lose images + //enemy spawn board + this.spawn_board = new Board(this, this.spawn_boardConfig); + this.baddiesManager = new BaddiesManager(this); + + this.waveManager = new WaveManager(this, this.baddiesManager); + //currency + // Add currency text + this.currencyText = this.add.text( + 500, + 5, + `Currency: ${this.currency}`, + { + fontSize: "30px", + color: "white", + } + ); + //help button + var graphics = this.add.graphics(); + // Fill the background with grey color + graphics.fillStyle(0x808080, 1); // Grey color + graphics.fillRoundedRect(125, 50, 600, 100, 20); + + // Create a Text object for the text + var helpText = this.add.text( + graphics.x + 20, + graphics.y + 20, + "cd:Change Directory ; ls:View content of file \n purchase(unit name,xCoord,yCoord): buy units \n remove(unit name,xCoord,yCoord): remove units", + { + fontFamily: "Arial", + fontSize: 24, + color: "#ffffff", // White color for text + } + ); + + // Make the text appear above the background + helpText.setDepth(1); + graphics.setVisible(false); + helpText.setVisible(false); + let help = this.add.image(800, 100, "questionMark").setInteractive(); + help.setScale(1 / 2); + help.on("pointerover", () => { + graphics.setVisible(true); + helpText.setVisible(true); + console.log("hovered over"); + }); + help.on("pointerout", () => { + graphics.setVisible(false); + helpText.setVisible(false); + console.log("not hovered over"); + }); + + //health bar stuff + this.health = 100; // Starting health + this.healthBar = this.add.graphics(); + this.updateHealthBar(); + + this.commandLine = new CommandLine( + this, + this.characterManager, + this.waveManager + ); + + this.time.addEvent({ + delay: 2000, // Attack every 2000 ms (2 seconds) + callback: () => { + this.characterManager.characters.forEach((character) => { + const gameCharacter = character as GameCharacter; + gameCharacter.attack(); + }); + }, + loop: true, + }); + + // When creating projectiles, ensure they are active and can interact + this.projectiles = this.physics.add.group({ + classType: Projectile, + runChildUpdate: true, // Ensures projectile update logic is executed + }); + + this.edge = this.physics.add.staticGroup(); + let platform = this.edge.create( + 270, + 400, + "platform" + ) as Phaser.Physics.Arcade.Sprite; + // After rotating the platform + platform.angle = 90; + // Manually set the size of the physics body + platform.body?.setSize(30, 500); + platform.setVisible(false); + + this.physics.add.collider( + this.baddiesManager.baddies, + this.edge, + (zombie, platform) => { + if ( + zombie instanceof Zombie1 && + platform instanceof Phaser.Physics.Arcade.Sprite + ) { + this.handleHitWall(zombie); + } + }, + undefined, + this + ); + + // Command Line Console + this.consoleDialogue = this.add.text(100, 160, "", { + fontSize: "24px", + color: "green", + backgroundColor: "#000000", + }); + + this.fpsText = new FpsText(this); + + this.outputBox = this.createOutputBox(870, 0, 410, 650); + + this.inputBox = this.createInputBox(670, 575, 310); + + this.score = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let scoreText = this.add.text( + this.currencyText.x, + this.currencyText.y + 50, + "Score: 0", + { + fontSize: "20px", + color: "white", + } + ); + + this.physics.add.collider( + this.baddiesManager.baddies, + this.edge, + (zombie, platform) => { + if ( + zombie instanceof Zombie1 && + platform instanceof Phaser.Physics.Arcade.Sprite + ) { + this.handleHitWall(zombie); + //this.shakeScreen(zombie, platform); + } + }, + undefined, + this + ); + this.dialogueOptions = [ + "Welcome General (click on the wizard to advance the text)", + "We are in dire need of help \n the zombies are close to invading and we need your help to stop them", + "please take accept the position of leader and guide us to victory", + "Get accustumed to the book of spells a.k.a the command line", + "Within this book you will cast spells to call upon your troops and \n defend our castle", + "try casting ls to see the content of the book", + "Great! Now you're getting the hang of it \n try going into the chracters directory using cd followed by chracters", + "Now look into the contents of this directory", + "Thats how you traverse pages (files) of the book", + "here you can type mv followed by \na unit and a x and y coordinate(1-4) to place a unit ", + "At any point you can use rm \nfollowed by a unti and its coordinates to remove a unti", + "When you go into a units directory \nyou can cast 'cat 'unit name' to view the file \nand learn more about the untis", + "Now use this knowledge to keep the zombie hordes at bay", + "Replacing me will be a reminder,\nincase you forget any of these commands", + "Keep in mind that inorder to place a troop \nyou must have right amount of currency to purchase any of them", + "Good luck and may your allies strike true!", + ]; + //background audio + this.gameMusic = this.sound.add("backgroundMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + //NPC button + var rect = new Phaser.GameObjects.Rectangle( + this, + 150, + 50, + 1000, + 200, + 0x000000, + 0.5 + ); + this.add.existing(rect); + + this.tutorialText = this.add.text( + 10, + 100, + this.dialogueOptions[this.curDialogueIdx] + ); + + this.physics.add.collider( + this.baddiesManager.baddies, + this.projectiles, + (zombie, projectile) => { + // Cast zombie and projectile to their expected types + let z = zombie as Phaser.Physics.Arcade.Sprite; + let p = projectile as Phaser.Physics.Arcade.Sprite; + + if (this.board_map.isWithinBounds(z.x, z.y)) { + // Ensure the collision affects only those zombies within the board + if (z instanceof Zombie1 && p instanceof Projectile) { + z.takeDamage(p.damage); + this.flashEnemy(z); + } + } + // Destroy the projectile regardless of the zombie's position + p.destroy(); + } + ); + if (this.gruntAmount === 0) { + this.win(); + } + } + // private shakeScreen( + // zombie: Zombie1, + // platform: Phaser.Physics.Arcade.Sprite + // ) { + // this.cameras.main.shake(500, 0.01); + // } + + private enemyHitWall() { + console.log("hit wall enemy"); + } + + private handleHitWall(baddy: BaddyCharacter): void { + // Assume each collision with the platform causes a fixed amount of damage + this.health -= baddy.dmg; + this.updateHealthBar(); + + if (this.health <= 50) { + this.gameMusic.stop(); + this.gameMusic = this.sound.add("endGameMusic"); + this.gameMusic.play(); + } + + if (this.health <= 0) { + this.lose(); + } + this.baddiesManager.removeCharacter("Zombie1", baddy); + } + flashEnemy(zombie: Zombie1) { + const originalTint = zombie.tint; + zombie.setTint(0xff0000); + setTimeout(() => { + zombie.setTint(originalTint); + }, 500); + } + private win() { + this.gameMusic.stop(); + this.gameMusic = this.sound.add("victoryMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + let winImg = this.add.image(400, 400, "youWin"); + winImg.setScale(0.9); + this.physics.pause(); + this.won = true; + + console.log("Game Won"); + } + private lose() { + this.gameMusic.stop(); + this.gameMusic = this.sound.add("defeatMusic"); + this.gameMusic.play({ volume: 0.4, loop: true }); + let loseImg = this.add.image(400, 400, "youLose"); + loseImg.setScale(0.9); + // Create the rectangular box + const restartButton = this.add.graphics().setInteractive(); + const buttonWidth = 200; + const buttonHeight = 80; + const buttonColor = 0xff0000; + + restartButton.fillStyle(buttonColor, 1); + restartButton.fillRect(200, 200, buttonWidth, buttonHeight); + + // Add text inside the button + const buttonText = this.add.text( + 200 + buttonWidth / 2, + 200 + buttonHeight / 2, + "Restart", + { + fontSize: "32px", + fontFamily: "Arial", + color: "#000000", + align: "center", + } + ); + buttonText.setOrigin(0.5); + restartButton.on("pointerdown", () => { + console.log("button pushed"); + this.scene.restart(); + }); + this.gameOver = true; + this.physics.pause(); + console.log("Game Over"); + } + + updateHealthBar() { + this.healthBar.clear(); + this.healthBar.fillStyle(0x00ff00, 1); + this.healthBar.fillRect(200, 10, 200 * (this.health / 100), 20); + } + + increaseCurrency(amount: number) { + this.currency += amount; + this.currencyText.setText(`Currency: ${this.currency}`); + } + + private createInputBox( + x: number, + y: number, + width: number + ): HTMLInputElement { + const input = document.createElement("input"); + input.type = "text"; + input.style.width = `${width}px`; + input.style.fontSize = "20px"; + input.style.backgroundColor = "#000"; + input.style.color = "green"; + input.style.border = "1px solid #888"; + input.style.padding = "5px"; + input.style.outline = "none"; + input.style.zIndex = "1"; + const offsetDown = 50; // Adjust as needed + const offsetRight = 50; // Adjust as needed + input.style.top = `calc(50% + ${offsetDown}px)`; + input.style.left = `calc(50% + ${offsetRight}px)`; + input.style.position = "fixed"; // Change to `fixed` position + document.body.appendChild(input); + + const updateInputPosition = () => { + const canvas = this.game.canvas; + const rect = canvas.getBoundingClientRect(); // Get the canvas's bounding rectangle + + input.style.left = `${rect.left + x}px`; // Use the canvas's position for `left` + input.style.top = `${rect.top + y}px`; // Use the canvas's position for `top` + }; + + // Set the initial position + updateInputPosition(); + + // Update the position on window resize + window.addEventListener("resize", updateInputPosition); + + // Process the command on 'Enter' key + input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + const command = input.value; + this.commandLine?.processCommand(command); + input.value = ""; + } + }); + + return input; + } + + private createOutputBox( + x: number, + y: number, + width: number, + height: number + ): Phaser.GameObjects.Text { + const outputBox = this.add.text(x, y, "", { + fontSize: "24px", + color: "green", + backgroundColor: "#000000", + wordWrap: { width: width }, + padding: { top: 10, bottom: 10, left: 10, right: 10 }, + }); + outputBox.setFixedSize(width, height); + return outputBox; + } + + updateOutputText(output: string) { + this.outputBox?.setText(output); + } + + update(delta: number) { + this.textTimer += delta; + + // Check if it's time to update the text and there are characters left in the current dialogue option + if ( + this.textTimer >= 10 && + this.dialogueCharCount < + this.dialogueOptions[this.curDialogueIdx].length + ) { + var characterArrayText = + this.dialogueOptions[this.curDialogueIdx].split(""); + + // Clear the current text + this.curDialogueText = ""; + + // Append new characters based on the index + for (let i = 0; i <= this.dialogueCharCount; i++) { + this.curDialogueText += characterArrayText[i]; + } + + this.tutorialText.setText(this.curDialogueText); + this.textTimer = 0; + this.dialogueCharCount += 1; + } + + let NPC = this.add.image(800, 100, "wizardNPC").setInteractive(); + NPC.setScale(1 / 2); + NPC.on("pointerdown", () => { + this.curDialogueIdx++; + console.log("clicked"); + }); + if (this.curDialogueIdx >= this.dialogueOptions.length) { + // Reset dialogue index if it exceeds the length of dialogueOptions array + this.curDialogueIdx = 0; + } + this.fpsText.update(); + this.waveManager.update(); + + this.projectiles?.children.iterate((child) => { + const projectile = child as Projectile; // Ensure correct casting + projectile.setVelocityX(300); + return true; + }); + this.characterManager.update(); + this.baddiesManager.update(); + } +}