diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..d950d4e5 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index e240001b..988bf59f 100644 --- a/README.md +++ b/README.md @@ -1 +1,9 @@ -REPLACE THIS WITH A DESCRIPTION OF YOUR GAME (in the README.md file). +# Cyber Spy - Learn to Hack, Hack to Learn + +Welcome to *Cyber Spy*, an educational puzzle adventure that immerses you in the thrilling world of cyber espionage while teaching you the fundamentals of bash commands and terminal navigation. Designed for incoming freshmen pursuing a computer science degree or anyone eager to master the command line, this game combines a compelling narrative with engaging gameplay to make learning an exhilarating experience. + +## About Cyber Spy + +In *Cyber Spy*, players step into the shoes of a hacker, accompanied by a trusty companion, Alfred, on a mission to dismantle a corrupt organization. Through a series of time-sensitive missions, players will navigate through a terminal interface, employing basic to advanced bash commands to infiltrate enemy facilities, gather intelligence, and thwart the organization's nefarious plans. + +Inspired by the engaging puzzles of *Hacknet*, the adventurous spirit of *Super Mario World*, and the thrilling espionage of *Mission Impossible*, *Cyber Spy* offers a unique educational experience that teaches as it entertains. diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 00000000..78f0cfc6 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/AlfredDeathMusic.mp3 b/assets/AlfredDeathMusic.mp3 new file mode 100644 index 00000000..cad714fc Binary files /dev/null and b/assets/AlfredDeathMusic.mp3 differ diff --git a/assets/AlfredIcon.png b/assets/AlfredIcon.png new file mode 100644 index 00000000..91977208 Binary files /dev/null and b/assets/AlfredIcon.png differ diff --git a/assets/ClosedBook.png b/assets/ClosedBook.png new file mode 100644 index 00000000..d3671cce Binary files /dev/null and b/assets/ClosedBook.png differ diff --git a/assets/CyberSpyTitleScreen.png b/assets/CyberSpyTitleScreen.png new file mode 100644 index 00000000..b69b032b Binary files /dev/null and b/assets/CyberSpyTitleScreen.png differ diff --git a/assets/HoveredBook.png b/assets/HoveredBook.png new file mode 100644 index 00000000..8e947279 Binary files /dev/null and b/assets/HoveredBook.png differ diff --git a/assets/Level1Background.png b/assets/Level1Background.png new file mode 100644 index 00000000..d3f5a12f --- /dev/null +++ b/assets/Level1Background.png @@ -0,0 +1 @@ + diff --git a/assets/LevelSelectBackground.png b/assets/LevelSelectBackground.png new file mode 100644 index 00000000..81851829 Binary files /dev/null and b/assets/LevelSelectBackground.png differ diff --git a/assets/OpenBook.png b/assets/OpenBook.png new file mode 100644 index 00000000..64a2a7e1 Binary files /dev/null and b/assets/OpenBook.png differ diff --git a/assets/PinPadText.png b/assets/PinPadText.png new file mode 100644 index 00000000..6026b8ee Binary files /dev/null and b/assets/PinPadText.png differ diff --git a/assets/PromptBox.png b/assets/PromptBox.png new file mode 100644 index 00000000..74ee728a Binary files /dev/null and b/assets/PromptBox.png differ diff --git a/assets/SpyIcon.png b/assets/SpyIcon.png new file mode 100644 index 00000000..7aeffd7d Binary files /dev/null and b/assets/SpyIcon.png differ diff --git a/assets/Storyboard.jpg b/assets/Storyboard.jpg new file mode 100644 index 00000000..439ef7a7 Binary files /dev/null and b/assets/Storyboard.jpg differ diff --git a/assets/alfred.png b/assets/alfred.png new file mode 100644 index 00000000..fb6f8de0 Binary files /dev/null and b/assets/alfred.png differ diff --git a/assets/arrow.png b/assets/arrow.png new file mode 100644 index 00000000..627babc5 Binary files /dev/null and b/assets/arrow.png differ diff --git a/assets/backwardsDoor.png b/assets/backwardsDoor.png new file mode 100644 index 00000000..f9b1649c Binary files /dev/null and b/assets/backwardsDoor.png differ diff --git a/assets/bomb.png b/assets/bomb.png new file mode 100644 index 00000000..28a0fbbd Binary files /dev/null and b/assets/bomb.png differ diff --git a/assets/cdBackDing.mp3 b/assets/cdBackDing.mp3 new file mode 100644 index 00000000..3265d8a8 Binary files /dev/null and b/assets/cdBackDing.mp3 differ diff --git a/assets/cdDing.mp3 b/assets/cdDing.mp3 new file mode 100644 index 00000000..a5bed9e3 Binary files /dev/null and b/assets/cdDing.mp3 differ diff --git a/assets/closed_metal_door.png b/assets/closed_metal_door.png new file mode 100644 index 00000000..e1f75e49 Binary files /dev/null and b/assets/closed_metal_door.png differ diff --git a/assets/ding.mp3 b/assets/ding.mp3 new file mode 100644 index 00000000..cba44de0 Binary files /dev/null and b/assets/ding.mp3 differ diff --git a/assets/dude.png b/assets/dude.png new file mode 100644 index 00000000..0feddb8c Binary files /dev/null and b/assets/dude.png differ diff --git a/assets/dudes.png b/assets/dudes.png new file mode 100644 index 00000000..6b35f4b4 Binary files /dev/null and b/assets/dudes.png differ diff --git a/assets/dudes1.png b/assets/dudes1.png new file mode 100644 index 00000000..641b60f2 Binary files /dev/null and b/assets/dudes1.png differ diff --git a/assets/evil.jpg b/assets/evil.jpg new file mode 100644 index 00000000..968c9a66 Binary files /dev/null and b/assets/evil.jpg differ diff --git a/assets/fonts/JetBrains.ttf b/assets/fonts/JetBrains.ttf new file mode 100644 index 00000000..70d2ec9e Binary files /dev/null and b/assets/fonts/JetBrains.ttf differ diff --git a/assets/forest.jpg b/assets/forest.jpg new file mode 100644 index 00000000..e931c1b9 Binary files /dev/null and b/assets/forest.jpg differ diff --git a/assets/img/phaser-logo.png b/assets/img/phaser-logo.png deleted file mode 100644 index d1fc105c..00000000 Binary files a/assets/img/phaser-logo.png and /dev/null differ diff --git a/assets/lasdfsa.mp3 b/assets/lasdfsa.mp3 new file mode 100644 index 00000000..cae951c3 Binary files /dev/null and b/assets/lasdfsa.mp3 differ diff --git a/assets/lockedDoor.png b/assets/lockedDoor.png new file mode 100644 index 00000000..28144679 Binary files /dev/null and b/assets/lockedDoor.png differ diff --git a/assets/lsDing.mp3 b/assets/lsDing.mp3 new file mode 100644 index 00000000..c1b941a5 Binary files /dev/null and b/assets/lsDing.mp3 differ diff --git a/assets/manDing.mp3 b/assets/manDing.mp3 new file mode 100644 index 00000000..293e5797 Binary files /dev/null and b/assets/manDing.mp3 differ diff --git a/assets/matrix.png b/assets/matrix.png new file mode 100644 index 00000000..358de66f Binary files /dev/null and b/assets/matrix.png differ diff --git a/assets/menuMusic.mp3 b/assets/menuMusic.mp3 new file mode 100644 index 00000000..7e87c849 Binary files /dev/null and b/assets/menuMusic.mp3 differ diff --git a/assets/metalDoor.png b/assets/metalDoor.png new file mode 100644 index 00000000..979da0d7 Binary files /dev/null and b/assets/metalDoor.png differ diff --git a/assets/num0.png b/assets/num0.png new file mode 100644 index 00000000..cf444e66 Binary files /dev/null and b/assets/num0.png differ diff --git a/assets/num1.png b/assets/num1.png new file mode 100644 index 00000000..f41d87b7 Binary files /dev/null and b/assets/num1.png differ diff --git a/assets/num2.png b/assets/num2.png new file mode 100644 index 00000000..e65de10f Binary files /dev/null and b/assets/num2.png differ diff --git a/assets/num3.png b/assets/num3.png new file mode 100644 index 00000000..c8530ce0 Binary files /dev/null and b/assets/num3.png differ diff --git a/assets/num4.png b/assets/num4.png new file mode 100644 index 00000000..6068edcf Binary files /dev/null and b/assets/num4.png differ diff --git a/assets/num5.png b/assets/num5.png new file mode 100644 index 00000000..ac3c929e Binary files /dev/null and b/assets/num5.png differ diff --git a/assets/num6.png b/assets/num6.png new file mode 100644 index 00000000..689fb348 Binary files /dev/null and b/assets/num6.png differ diff --git a/assets/num7.png b/assets/num7.png new file mode 100644 index 00000000..72e2705e Binary files /dev/null and b/assets/num7.png differ diff --git a/assets/num8.png b/assets/num8.png new file mode 100644 index 00000000..a93a8c21 Binary files /dev/null and b/assets/num8.png differ diff --git a/assets/num9.png b/assets/num9.png new file mode 100644 index 00000000..742ac901 Binary files /dev/null and b/assets/num9.png differ diff --git a/assets/open_metal_door.png b/assets/open_metal_door.png new file mode 100644 index 00000000..c5576cb9 Binary files /dev/null and b/assets/open_metal_door.png differ diff --git a/assets/padCheck.png b/assets/padCheck.png new file mode 100644 index 00000000..343c0b68 Binary files /dev/null and b/assets/padCheck.png differ diff --git a/assets/padx.png b/assets/padx.png new file mode 100644 index 00000000..3a8c2140 Binary files /dev/null and b/assets/padx.png differ diff --git a/assets/pin.png b/assets/pin.png new file mode 100644 index 00000000..36fa2121 Binary files /dev/null and b/assets/pin.png differ diff --git a/assets/platform.png b/assets/platform.png new file mode 100644 index 00000000..1e4a3f86 Binary files /dev/null and b/assets/platform.png differ diff --git a/assets/radar.gif b/assets/radar.gif new file mode 100644 index 00000000..ce7aea12 Binary files /dev/null and b/assets/radar.gif differ diff --git a/assets/shield.png b/assets/shield.png new file mode 100644 index 00000000..de5e78f5 Binary files /dev/null and b/assets/shield.png differ diff --git a/assets/sky.png b/assets/sky.png new file mode 100644 index 00000000..5972639f Binary files /dev/null and b/assets/sky.png differ diff --git a/assets/space.png b/assets/space.png new file mode 100644 index 00000000..61d74ca2 Binary files /dev/null and b/assets/space.png differ diff --git a/assets/star.png b/assets/star.png new file mode 100644 index 00000000..bfc2d298 Binary files /dev/null and b/assets/star.png differ diff --git a/assets/styles.css b/assets/styles.css new file mode 100644 index 00000000..36cf5920 --- /dev/null +++ b/assets/styles.css @@ -0,0 +1,8 @@ +@font-face { + font-family: "jetbrains"; + src: url("/assets/fonts/JetBrains.ttf") format("truetype"); +} + +body { + font-family: "jetbrains"; +} diff --git a/assets/wallDoor.png b/assets/wallDoor.png new file mode 100644 index 00000000..d43a3075 Binary files /dev/null and b/assets/wallDoor.png differ diff --git a/docs/egdd.md b/docs/egdd.md index 51ed6536..8986d0c8 100644 --- a/docs/egdd.md +++ b/docs/egdd.md @@ -1 +1,185 @@ -REPLACE THIS TEXT WITH YOUR EGDD MARKDOWN. +# Cyber Spy + +## Elevator Pitch + +Cyber Spy is an educational puzzle game with a dramatic storyline attached. Cyber Spy will introduce players to basic bash commands through a time based environment. The story follows a hacker, and his companion Alfred, through several missions to dethrone a corrupt organization. Players should be able to traverse through the terminal efficiently with the knowledge gained after completing this game. + +## Influences (Brief) + +- Super Mario World: + - Medium: Game + - Explanation: *We plan to mimic the style of super mario world and the pixel art. The movement in between levels will resemble the movement of a platformer like mario* +- Hacknet (https://en.wikipedia.org/wiki/Hacknet): + - Medium: Game + - Explanation: Much like our planned game, Hacknet is a hacking-based puzzle game in which the majority of gameplay comes in the form of typing commands in a Unix-like terminal. The main goal in Hacknet is to ‘hack’ into other computer systems, steal imaginary files, and acquire superuser privileges. It incorporates gameplay aspects like memory management (you can only run a certain amount of programs before you run out of memory) and a timer later on in the game to make doing so more difficult. We plan on taking inspiration from the terminal design, as well as aspects like the timer. +- Mission Impossible + - Medium: Movie + - Explanation: We plan on creating a mission based game where the user must break into a facility (directory) using terminal commands. There will also be a time constraint. There will also be a butler/narrator character, much like the tech team in Mission impossible, that will guide you through your mission, providing hints and instructions as needed. + +## Core Gameplay Mechanics (Brief) + +- Typing commands into terminal to perform actions +- 2D Navigation Throughout the facility +- Item Collection within your inventory +- Display success screen after completing a mission +- Display defeat screen after failing a mission + +# Learning Aspects + +## Learning Domains + +Introduction Systems Programming +Machine Organization and Assembly Language +Introduction to Software Engineering + +## Target Audiences + +Incoming freshmen in college pursuing a computer science degree. Specifically students learning how to use bash commands. + +## Target Contexts + +This could be used as an introductory assignment for CISC210. Students looking to practice their skills in the terminal can also play this game in their free time. + +## Learning Objectives + +After playing Cyber Spy, students should be able to traverse the terminal using the command line efficiently. + +- By the end of the game, students should be able to access files using terminal commands +- By the end of the game, students should be able to modify the contents a file using basic nano commands +- By the end of the game, students should be able to navigate through different directories +- By the end of the game, students should be able to move/remove files across different directories using terminal commands. +- By the end of the game, students should be able to create and write to new directories and files. + +## Prerequisite Knowledge + +- Players should know how to type a minimum of 30 wpm +- Players should know that commands can cause their computer to perform actions + +## Assessment Measures + +A short pre-test, and matching post-test. + +Given a directory name, utilize bash commands to access it. +Given a file name, utilize bash commands to delete it. +Given a file and directory name, utilize bash commands to move it to another given directory. + +# What sets this project apart? + +- Most coding assessments that introduce students to the terminal don't intend to be entertaining, but our game will have fun graphics, an engaging story, and thrilling gameplay. +- The game will be educational yet still have aspects of a fun video game integrated seamlessly into it. + +# Player Interaction Patterns and Modes + +## Player Interaction Pattern + +This is a single player game. The player will interact with the via typing keyboard inputs, moving with arrows, and clicking on menu buttons when needed. + +## Player Modes + +- Single-player: You repeatedly complete missions to advance through the story. + +# Gameplay Objectives + +- Reach the end of the mission: + - Description: Do what the prompt at the top of the screen requests + - Alignment: This aligns with our learning objective because it forces players to hone the skills they've learned and will help them memorize commands after repeated use. +- Complete all missions to finish the game: + - Description: Traverse through each mission by accomplishing their respective goals + - Alignment: Each mission will establish a new topic for the player to learn. Each topic will build upon each other, and by the end players will know what each command does and when to use them. + +# Procedures/Actions + +You can type when a command line is shown, and enter these commands. In between missions you can move around using arrow keys, and press a button to start mission. + +# Rules + +- If the player enters an invalid command, they will get an error and hint +- If the player enters a valid command, it will work and they will advance through the current mission +- They will have a toolset of commands available to them to reference and utilize +- The list of commands that they will learn through the missions are as follows: +ls +cd +rm +touch +cat +mv +cp + +# Objects/Entities + +- There is a text input for users to type into +- There is a command line that user input is displayed when entered +- There is a prompt +- There is a main character to control in between missions +- There is a companion character that will speak to you during missions +- There is a timer during missions + +## Core Gameplay Mechanics (Detailed) + +- Typing commands into your “terminal”*: *The majority of game interaction will take place by the player typing commands into their terminal. Assuming the user types the correct command sequence, they will be moved to their next task. The player will initially start with a limited number of basic commands such as ‘ls’, then as they progress further, unlock new commands through their companion. E.g., “use ‘cd’ to access a new directory, or in this case your backpack, with ‘cd backpack’”. +- Picking up tools*: *A large part of the education in Cyber Spy will take place because of an increase in difficulty as a player progresses through levels. This increase will be due to the player picking up tools along the way in the form of new commands. Some of these commands will be highlighted/vibrating in levels for the player to collect, while others will be transferred directly to the players backpack via their companion. It is then up to the player to decide when the right time to use a given tool is. +- Navigation through 2D levels*: *The high-level goal of this game is to navigate your player sprite through several rooms of an advanced technology facility and destroy the internal corruption taking place there. You will navigate your player using terminal commands, and with the help of your companion to explain what new tools do as you collect them. + + +## Feedback + +If a command is wrong, your companion will tell you it is wrong and give you a hint in the right direction. Text on the screen shows if a command worked or did not work. + +# Story and Gameplay + +## Presentation of Rules + +Players have a virtual assistant like Alfred is to Batman. Alfred will introduce your mission, tasks, and give you your tools (ls, cd, touch, etc.) as they become needed. + +## Presentation of Content + +Players will slowly be given tools as mentioned before, and expected to use them at least one time before being given a different tool by alfred, e.g. Alfred: “You’re gonna need a wrench to break that glass. Use 'ls' to view your backpack items, including the wrench I just gave you.” + +## Story (Brief) + +Our story will follow a hacker and his trusty companion through a series of missions to dethrone a corrupt organization. They will do this using a variety of commands to access the opposition's files, and take down their servers. All while attempting to be stealthy and quick enough to finish these missions before the time runs out. Will your coding abilities be able to save the world? Or will you fold under pressure? Who can be trusted? Cyber Spy will answer all of your questions. + +## Storyboarding +![alttext](../assets/Storyboard.jpg) + +# Assets Needed + +## Aethestics + +This game should feel high pressure, but rewarding. It should encourage players to code fast and also avoid mistakes. The aesthetics of the game will be dark, dramatic, and retro. + +## Graphical + +- Characters List + - Agent User + - Companion (Woz/Alfred) +- Textures: + - Level entrances +- Environment Art/Textures: + - Coding background + - Level select background + - Unique backgrounds for special directories + + +## Audio + +- Music List (Ambient sound) + - General gameplay: 8 bit, eerie stealth music like the music from Batman Begins GBA: https://www.youtube.com/watch?v=MIppc7zfqis&list=PLO4jlmGoc6uAy9S9J3SPXv-UrHBm9Bgz9&index=2 + - General gameplay: 8 bit, action music like the music from Batman Begins GBA: +https://www.youtube.com/watch?v=Px-bsXoXdhc&list=PLO4jlmGoc6uAy9S9J3SPXv-UrHBm9Bgz9&index=1 + +*Game Interactions are things that trigger SFX, like character movement, hitting a spiky enemy, collecting a coin.* + +- Sound List (SFX) + - Entering a line of code: pleasant ping noise found online + - Typing sounds: recorded by us or found on YouTube + - Error: Buzz/dud sound + - Dialogue: digital noises made to queue while speaking occurs, but all dialogue will be text written on the screen + + +# 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..6b313580 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { - "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": { - "phaser": "^3.70.0" + "phaser": "^3.80.1" }, "devDependencies": { "@types/jest": "^29.5.11", @@ -17,6 +17,7 @@ "@typescript-eslint/parser": "^6.2.0", "@yandeu/prettier-config": "^0.0.3", "copy-webpack-plugin": "^10.1.0", + "css-loader": "^7.0.0", "eslint": "^8.46.0", "html-webpack-plugin": "^5.5.0", "javascript-obfuscator": "^4.0.0", @@ -24,6 +25,7 @@ "prettier": "^2.5.1", "rimraf": "^3.0.2", "serve": "^14.1.2", + "style-loader": "^3.3.4", "ts-jest": "^29.1.1", "ts-loader": "^9.2.6", "ts-node": "^10.9.2", @@ -4957,6 +4959,41 @@ "node": ">=8" } }, + "node_modules/css-loader": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.0.0.tgz", + "integrity": "sha512-WrO4FVoamxt5zY9CauZjoJgXRi/LZKIk+Ta7YvpSGr5r/eMYPNp5/T9ODlMe4/1rF5DYlycG1avhV4g3A/tiAw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -4985,6 +5022,18 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6814,6 +6863,18 @@ "node": ">=0.10.0" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -8789,6 +8850,24 @@ "node": ">=8" } }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -9183,9 +9262,9 @@ } }, "node_modules/phaser": { - "version": "3.70.0", - "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.70.0.tgz", - "integrity": "sha512-2g+gh+Jp9f/Ho9FOXOYbIJMGf3UZXyMbW2iLScFaLQw11e/LqVyxj/YmaBauWbHabeTnZjiWkPklDnxhesMH0g==", + "version": "3.80.1", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.80.1.tgz", + "integrity": "sha512-VQGAWoDOkEpAWYkI+PUADv5Ql+SM0xpLuAMBJHz9tBcOLqjJ2wd8bUhxJgOqclQlLTg97NmMd9MhS75w16x1Cw==", "dependencies": { "eventemitter3": "^5.0.1" } @@ -9234,6 +9313,112 @@ "node": ">=8" } }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -10251,6 +10436,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -10570,6 +10764,22 @@ "node": ">=0.10.0" } }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15837,6 +16047,22 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "css-loader": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.0.0.tgz", + "integrity": "sha512-WrO4FVoamxt5zY9CauZjoJgXRi/LZKIk+Ta7YvpSGr5r/eMYPNp5/T9ODlMe4/1rF5DYlycG1avhV4g3A/tiAw==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + } + }, "css-select": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", @@ -15856,6 +16082,12 @@ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -17222,6 +17454,13 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, "idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -18701,6 +18940,12 @@ } } }, + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -18999,9 +19244,9 @@ "dev": true }, "phaser": { - "version": "3.70.0", - "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.70.0.tgz", - "integrity": "sha512-2g+gh+Jp9f/Ho9FOXOYbIJMGf3UZXyMbW2iLScFaLQw11e/LqVyxj/YmaBauWbHabeTnZjiWkPklDnxhesMH0g==", + "version": "3.80.1", + "resolved": "https://registry.npmjs.org/phaser/-/phaser-3.80.1.tgz", + "integrity": "sha512-VQGAWoDOkEpAWYkI+PUADv5Ql+SM0xpLuAMBJHz9tBcOLqjJ2wd8bUhxJgOqclQlLTg97NmMd9MhS75w16x1Cw==", "requires": { "eventemitter3": "^5.0.1" }, @@ -19040,6 +19285,69 @@ "find-up": "^4.0.0" } }, + "postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + } + }, + "postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -19803,6 +20111,12 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, + "source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true + }, "source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -20053,6 +20367,13 @@ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true }, + "style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "requires": {} + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index e285b2ae..8fb7d604 100644 --- a/package.json +++ b/package.json @@ -36,30 +36,32 @@ }, "license": "MIT", "dependencies": { - "phaser": "^3.70.0" + "phaser": "^3.80.1" }, "devDependencies": { + "@types/jest": "^29.5.11", + "@typescript-eslint/eslint-plugin": "^6.2.0", + "@typescript-eslint/parser": "^6.2.0", "@yandeu/prettier-config": "^0.0.3", "copy-webpack-plugin": "^10.1.0", + "css-loader": "^7.0.0", + "eslint": "^8.46.0", "html-webpack-plugin": "^5.5.0", "javascript-obfuscator": "^4.0.0", + "jest": "^29.7.0", "prettier": "^2.5.1", "rimraf": "^3.0.2", "serve": "^14.1.2", + "style-loader": "^3.3.4", + "ts-jest": "^29.1.1", "ts-loader": "^9.2.6", + "ts-node": "^10.9.2", "typescript": "^4.5.3", "webpack": "^5.65.0", "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.6.0", "webpack-merge": "^5.8.0", "webpack-obfuscator": "^3.5.0", - "workbox-webpack-plugin": "^6.4.2", - "eslint": "^8.46.0", - "@types/jest": "^29.5.11", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "@typescript-eslint/eslint-plugin": "^6.2.0", - "@typescript-eslint/parser": "^6.2.0" + "workbox-webpack-plugin": "^6.4.2" } } diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..caa8e6c3 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/config.ts b/src/config.ts index 9776bc5c..878f9276 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,12 +1,26 @@ import Phaser from "phaser"; -import MainScene from "./scenes/mainScene"; +import TitleScene from "./scenes/titleScene"; import PreloadScene from "./scenes/preloadScene"; +import TerminalScene from "./scenes/terminalScene"; +import LevelSelect from "./scenes/levelSelect"; +import Level01 from "./scenes/level01"; +import LoadingScene1 from "./scenes/level01_load"; +import IntroScene from "./scenes/intro"; +import LoginScene from "./scenes/login"; +import Tutorial from "./scenes/tutorial"; +import SecurityBreachScene from "./scenes/securityBreach"; +import Level2Scene from "./scenes/level02"; +import LoadingScene2 from "./scenes/level02_load"; +import LoadingScene2Part2 from "./scenes/level02_load2"; +import LevelThreeIntro from "./scenes/lvl03_intro"; +import LevelThreeIntro2 from "./scenes/LevelThreeIntro2"; +import Level03 from "./scenes/lvl03Main"; const DEFAULT_WIDTH = 1280; const DEFAULT_HEIGHT = 720; export const CONFIG = { - title: "My Untitled Phaser 3 Game", + title: "Cyber Spy", version: "0.0.1", type: Phaser.AUTO, backgroundColor: "#ffffff", @@ -17,7 +31,24 @@ export const CONFIG = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, }, - scene: [PreloadScene, MainScene], + scene: [ + PreloadScene, + TitleScene, + LevelSelect, + TerminalScene, + Level01, + LoadingScene1, + IntroScene, + LoginScene, + Tutorial, + Level2Scene, + LoadingScene2, + LoadingScene2Part2, + LevelThreeIntro, + LevelThreeIntro2, + Level03, + SecurityBreachScene, + ], physics: { default: "arcade", arcade: { diff --git a/src/game.ts b/src/game.ts index c730e347..76b4f824 100644 --- a/src/game.ts +++ b/src/game.ts @@ -2,6 +2,18 @@ import Phaser from "phaser"; import { CONFIG } from "./config"; window.addEventListener("load", () => { - const game = new Phaser.Game(CONFIG); + const game = new Phaser.Game({ + ...CONFIG, + physics: { + ...CONFIG.physics, + arcade: { + ...CONFIG.physics.arcade, + gravity: { + x: 0, // Add the missing 'x' property + y: 450, + }, + }, + }, + }); console.info("Started main game:", game); }); diff --git a/src/objects/file.ts b/src/objects/file.ts new file mode 100644 index 00000000..ff103eae --- /dev/null +++ b/src/objects/file.ts @@ -0,0 +1,82 @@ +// // export class Folder { +// // name: string; +// // type: string; +// // children: Folder[]; +// // color: string; +// // constructor(name: string, type: string, children: Folder[] = []) { +// // this.name = name; +// // this.type = type; +// // if (this.type === "folder") { +// // this.children = children; +// // this.color = "blue"; +// // } +// // else { +// // this.color = "green"; +// // this.children = []; + +// // } + +// // } + +// // display() { +// // print(this.name(color == color) +// // } +// // } + +// export default class BaseScene extends Phaser.Scene{ +// alfredLogo: Phaser.GameObjects.Image; +// manual: Manual; +// promptBox: PromptBox; +// inputBox: InputBox; +// commands: Command[]; +// pin: Pin; + +// constructor(config: string | Phaser.Types.Scenes.SettingsConfig) { +// super(config); +// } + +// preload() { +// this.load.image('logo', 'assets/logo.png'); +// } + +// create() { +// //add alfred logo +// //add manual +// //add prompt box +// //add input box +// //add commands +// //add pin +// } +// setPromptText(){ +// //set prompt text +// } +// setManualText(){ +// //set manual text +// } +// setCommands(){ +// //set commands +// } + +// } + +// export default class Level01 extends BaseScene{ +// constructor(config: string | Phaser.Types.Scenes.SettingsConfig) { +// super(config); +// } +// create() { +// super.create(); +// this.setCommands(list of commands) +// this.setPromptText("prompt text") +// this.setManualText("manual text") +// //add level 01 specific stuff +// } +// } + +// export default class Command { +// command: string; +// description: string; +// constructor(command: string, description: string){ +// this.command = command; +// this.description = description; +// } +// } diff --git a/src/objects/manual.ts b/src/objects/manual.ts new file mode 100644 index 00000000..f69d76e1 --- /dev/null +++ b/src/objects/manual.ts @@ -0,0 +1,82 @@ +import Phaser from "phaser"; + +export default class Manual extends Phaser.GameObjects.Sprite { + manualText: string; + manualOpen: boolean; + manualContainer: Phaser.GameObjects.Container; + scene: Phaser.Scene; + + constructor(scene: Phaser.Scene, x: number, y: number, text: string) { + super(scene, x, y, "ClosedBook"); + this.scene = scene; + this.manualText = text; + this.manualOpen = false; + + // Adding the sprite to the scene and scaling it down + this.scene.add.existing(this); + this.setScale(0.3); // Scale down the sprite + this.setInteractive({ cursor: "pointer" }); + + // Handling hover events + this.on("pointerover", () => { + this.setTexture(this.manualOpen ? "OpenBook" : "HoveredBook"); + this.setScale(0.35); // Scale up a bit when hovered + }); + + this.on("pointerout", () => { + this.setTexture(this.manualOpen ? "OpenBook" : "ClosedBook"); + this.setScale(0.3); // Return to original scaled down size + }); + + // Handling click events + this.on("pointerdown", () => { + this.manualOpen = !this.manualOpen; + this.setTexture(this.manualOpen ? "OpenBook" : "HoveredBook"); + this.manualContainer.setVisible(this.manualOpen); + if (this.manualOpen) { + this.displayManual(); + } + }); + + // Create a container for manual text right under the book + let manualHeight = 355; // available space between the sprite and the text entry box + let manualWidth = 330; // an approximate width based on the screenshot + let manualX = 20; // starting X coordinate under the sprite + let manualY = 175; // starting Y coordinate right below the sprite + this.manualContainer = this.scene.add.container(manualX, manualY); + + let graphics = this.scene.add.graphics(); + graphics.fillStyle(0x21201f, 0.8); // Grey background with opacity + graphics.lineStyle(4, 0xffd700); // Yellow border + graphics.strokeRoundedRect(0, 0, manualWidth, manualHeight, 20); // x, y, width, height, radius + graphics.fillRoundedRect(0, 0, manualWidth, manualHeight, 20); + this.manualContainer.add(graphics); + + let textObject = this.scene.add.text(5, 20, this.manualText, { + font: "bold 16px 'Courier New'", + color: "#ffd700", // Text color matches the border + wordWrap: { width: manualWidth - 10 }, + align: "left", // Left align text + }); + this.manualContainer.add(textObject); + this.manualContainer.setVisible(false); + } + + displayManual() { + this.scene.tweens.add({ + targets: this.manualContainer.list, + scaleY: 1, + scaleX: 1, + duration: 300, + ease: "Power2", + }); + this.manualContainer.setVisible(true); + } + + setManualText(newText: string) { + this.manualText = newText; + (this.manualContainer.list[1] as Phaser.GameObjects.Text).setText( + newText + ); + } +} diff --git a/src/objects/ui.ts b/src/objects/ui.ts new file mode 100644 index 00000000..53439921 --- /dev/null +++ b/src/objects/ui.ts @@ -0,0 +1,98 @@ +import Phaser from "phaser"; +import Manual from "./manual"; + +export default class UIElements extends Phaser.GameObjects.Container { + scene: Phaser.Scene; + username: string; + promptImage: Phaser.GameObjects.Image; + alfredImage: Phaser.GameObjects.Image; + pinImage: Phaser.GameObjects.Image; + manual: Manual; + inputField: HTMLInputElement; + constructor(scene: Phaser.Scene, x: number, y: number, username: string) { + super(scene, x, y); + this.scene = scene; + this.username = username; + + this.initUI(); + } + + initUI() { + // Load Images and Text dynamically + this.promptImage = this.scene.add + .image(0, -260, "prompt") + .setDisplaySize(560, 110); + this.alfredImage = this.scene.add + .image(-440, -260, "alfredicon") + .setDisplaySize(130, 130); + this.pinImage = this.scene.add + .image(410, -260, "pin") + .setDisplaySize(30, 40); + + // Manual object instantiation + this.manual = new Manual( + this.scene, + -590, + -260, + "Initial manual text here." + ); + + // Add all the elements to the container + this.add([ + this.promptImage, + this.alfredImage, + this.pinImage, + this.manual, + ]); + + // Add and configure the text input field + this.createInputField(); + } + + createInputField() { + // Ensure the previous input field is removed if it exists + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + + // Create new input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "50%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; + this.inputField.style.color = "#fff"; + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + + // Optional: Add event listeners specific to the input field + this.inputField.addEventListener( + "keydown", + this.handleInput.bind(this) + ); + } + + handleInput(event: KeyboardEvent) { + if (event.key === "Enter") { + // Logic for what happens when the user presses Enter + console.log(this.inputField.value); + this.inputField.value = ""; // Reset the input field after enter + } + } + + updateManualText(text: string) { + this.manual.setManualText(text); + } + + removeInputField() { + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + } + + // Add other necessary methods to update or interact with UI components +} diff --git a/src/scenes/LevelThreeIntro2.ts b/src/scenes/LevelThreeIntro2.ts new file mode 100644 index 00000000..d9816286 --- /dev/null +++ b/src/scenes/LevelThreeIntro2.ts @@ -0,0 +1,142 @@ +import Phaser from "phaser"; + +export default class LevelThreeIntro2 extends Phaser.Scene { + private content: string[]; // text to display + private charDelay: number; // delay between characters + private lineDelay: number; // delay between lines + private startX: number; // start X position of the text + private startY: number; // start Y position of the text + private currentLine: Phaser.GameObjects.Text; // Text object to display the current line + private lineIndex: number; // index of the current line + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private lvl5: boolean; + private username: string; + + constructor() { + super({ key: "LevelThreeIntro2" }); + } + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: boolean; + lvl4: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + } + + preload() { + this.load.image("alfredicon", "assets/alfredicon.png"); + } + + create() { + this.resetScene(); + + this.add.rectangle(640, 360, 1280, 720, 0x000); + this.add.image(150, 100, "alfredicon").setDisplaySize(130, 130); + + //display text + this.displayNextLine(); + + // On enter, transition to Level 1 + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("Level03", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + }); + } + + resetScene() { + // helper to reset intial values on load + this.charDelay = 30; + this.lineDelay = 120; + this.startX = 250; + this.startY = 90; + this.lineIndex = 0; + this.content = [ + "Note that the 'cat' command is used to display the contents", + "of one or more files. It can be used on a single file, or", + "multiple files, and display their outputs.", + " ", + "Below is an example using 'cat' to display the contents", + "of a file named 'secret.txt':", + " ", + " - 'cat secret.txt'", + " ", + "If you need to display the contents of multiple files,", + "you can provide their filenames separated by spaces.", + " ", + "For example, to display the contents of two files, 'secret1.txt'", + "and 'secret2.txt', you can use:", + " ", + " - 'cat secret1.txt secret2.txt'", + " ", + "Note, the 'cat' command displays the contents of files,", + "not directories which you're used to. 'Cat' is to files what 'ls' is", + "to directories. In your mission, files are marked with '.txt' at", + "the end of their names. E.g. 'secret_message.txt', and thats where", + "you'll find the hidden codes for this mission.", + " ", + "If you are ever unsure about how to use the 'cat' command, you", + "can use 'man cat' to help you.", + "Or use 'man alfred' to hear from me directly.", + " ", + "Good luck " + this.username.toLowerCase() + ".", + " ", + " [Enter] to Continue", + ]; + } + + // helper to display text line by line, calling typeText to animate + displayNextLine() { + if (this.lineIndex < this.content.length) { + const line = this.content[this.lineIndex++]; + // Create a new text object for the current line + this.currentLine = this.add.text( + this.startX, + this.startY + 22 * (this.lineIndex - 1), + "", + { + fontSize: "24px", + color: "#fff", + } + ); + // Start typing the line + this.typeText(line); + } + } + + // helper to animate text typing + typeText(line: string) { + // split the line into characters + const characters = line.split(""); + let i = 0; + // add a delayed event for each character + this.time.addEvent({ + delay: this.charDelay, + repeat: characters.length - 1, + callback: () => { + this.currentLine.text += characters[i++]; + if (i === characters.length) { + // once all characters are added, add a delayed event to display the next line + this.time.delayedCall( + this.lineDelay, + this.displayNextLine, + [], + this + ); + } + }, + callbackScope: this, + }); + } +} diff --git a/src/scenes/intro.ts b/src/scenes/intro.ts new file mode 100644 index 00000000..202b2720 --- /dev/null +++ b/src/scenes/intro.ts @@ -0,0 +1,140 @@ +import Phaser from "phaser"; + +export default class IntroScene extends Phaser.Scene { + private content: string[]; // text to display + private charDelay: number; // delay between characters + private lineDelay: number; // delay between lines + private startX: number; // start X position of the text + private startY: number; // start Y position of the text + private currentLine: Phaser.GameObjects.Text; // Text object to display the current line + private lineIndex: number; // index of the current line + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private username: string; + + constructor() { + super({ key: "IntroScene" }); + } + + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: boolean; + lvl4: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + console.log(this.username); + } + preload() { + this.load.image("alfredicon", "assets/alfredicon.png"); + } + + create() { + this.resetScene(); // helper to reset intial values on load + + //adding assets + this.add.rectangle(640, 360, 1280, 720, 0x000); + this.add.image(150, 100, "alfredicon").setDisplaySize(130, 130); + this.add.image(150, 480, "spyicon").setDisplaySize(130, 130); + + //display text + this.displayNextLine(); + + // On enter, transition to Level 1 + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("Tutorial", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + }); + }); + } + + resetScene() { + // helper to reset intial values on load + this.charDelay = 30; + this.lineDelay = 120; + this.startX = 250; + this.startY = 90; + this.lineIndex = 0; + console.log(this.username); + this.content = [ + "Agent " + + this.username.charAt(0).toUpperCase() + + this.username.slice(1) + + ", this is Alfred speaking.", + " ", + "Sorry to call you so late into the evening, but it", + "appears we've got a situation on our hands. Namuh Yortsed, CEO of", + "Yortsed Corp, has set his sights on the city's power supply.", + "Intelligence suggests he's planning a hostile takeover and if", + "he succeeds, he'll wield unprecedented control over the", + "very lifeblood of this city.", + " ", + "Amidst the shadow of Namuh, emerge as the city's beacon of hope.", + "Utilize the tools given to you " + this.username + ".", + " ", + "Just as you have in the past.", + " ", + " ", + " ", + " ", + + "...", + " ", + " ", + " ", + " [Enter] to Continue", + ]; + } + + // helper to display text line by line, calling typeText to animate + displayNextLine() { + if (this.lineIndex < this.content.length) { + const line = this.content[this.lineIndex++]; + // Create a new text object for the current line + this.currentLine = this.add.text( + this.startX, + this.startY + 22 * (this.lineIndex - 1), + "", + { + fontSize: "24px", + color: "#fff", + } + ); + // Start typing the line + this.typeText(line); + } + } + + // helper to animate text typing + typeText(line: string) { + // split the line into characters + const characters = line.split(""); + let i = 0; + // add a delayed event for each character + this.time.addEvent({ + delay: this.charDelay, + repeat: characters.length - 1, + callback: () => { + this.currentLine.text += characters[i++]; + if (i === characters.length) { + // once all characters are added, add a delayed event to display the next line + this.time.delayedCall( + this.lineDelay, + this.displayNextLine, + [], + this + ); + } + }, + callbackScope: this, + }); + } +} diff --git a/src/scenes/level01.ts b/src/scenes/level01.ts new file mode 100644 index 00000000..cf5422ae --- /dev/null +++ b/src/scenes/level01.ts @@ -0,0 +1,482 @@ +import Phaser from "phaser"; +import Manual from "../objects/manual"; + +export default class Level1Scene extends Phaser.Scene { + private stateText: Phaser.GameObjects.Text; + private inputField: HTMLInputElement; + private inputContainer: Phaser.GameObjects.Container; + private timer: Phaser.GameObjects.Text; + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private username: string; + private lvl5: boolean; + private objectiveCompleted: boolean = false; + private lastText: string[] = [""]; + private manual: Manual; + private lastPosition: number = -1; + + constructor() { + super({ key: "Level01" }); + } + + init(data: { + username: string; + + lvl1: boolean; + + lvl2: boolean; + + lvl3: boolean; + + lvl4: boolean; + + lvl5: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + this.lvl5 = data.lvl5; + } + preload() { + this.load.image("ClosedBook", "../assets/ClosedBook.png"); + this.load.image("HoveredBook", "../assets/HoveredBook.png"); + this.load.image("OpenBook", "../assets/OpenBook.png"); + } + + create() { + this.objectiveCompleted = false; + this.add.rectangle(640, 360, 1280, 720, 0x000); + + this.add.image(640, 100, "prompt").setDisplaySize(560, 110); + this.add.image(200, 100, "alfredicon").setDisplaySize(130, 130); + this.add.image(1050, 100, "pin").setDisplaySize(30, 40); + this.manual = new Manual(this, 50, 100, "Initial manual text here."); + + let ding = this.sound.add("ding", { loop: false }); + let lsDing = this.sound.add("lsDing", { loop: false }); + let cdDing = this.sound.add("cdDing", { loop: false }); + let cdBackDing = this.sound.add("cdBackDing", { loop: false }); + let manDing = this.sound.add("manDing", { loop: false }); + + this.inputContainer = this.add.container(360, 520); + + const maskGraphics = this.make.graphics(); + maskGraphics.fillRect(300, 185, 1080, 500); + const mask = new Phaser.Display.Masks.GeometryMask(this, maskGraphics); + + this.inputContainer.setMask(mask); + + this.addTextToContainer("Alfred: Welcome back " + this.username + "!"); + + let state: string = "home"; + + const lsMap = new Map(); + const cdMap = new Map(); + const cdBack = new Map(); + const manMap = new Map(); + const rmMap = new Map(); // Map to track removable files + + lsMap.set("home", "dir_break_room dir_closet dir_control_room"); + lsMap.set( + "break_room", + "dir_suitcase file_vending_machine file_chair file_table" + ); + lsMap.set("closet", "dir_cardboard_box file_wires file_hazmat_suit"); + lsMap.set( + "control_room", + "file_surveillance_camera file_monitor file_apple_juice" + ); + lsMap.set( + "suitcase", + "file_namuhs_glasses file_batteries file_papers file_apple" + ); + lsMap.set("cardboard_box", "file_papers"); + + cdMap.set("home", ["break_room", "closet", "control_room"]); + cdMap.set("break_room", ["suitcase"]); + cdMap.set("closet", ["cardboard_box"]); + + cdBack.set("break_room", "home"); + cdBack.set("closet", "home"); + cdBack.set("control_room", "home"); + cdBack.set("suitcase", "break_room"); + cdBack.set("cardboard_box", "closet"); + + rmMap.set("control_room", ["surveillance_camera"]); + + manMap.set( + "ls", + "Alfred: Remember, the 'ls' command\nis useful for viewing your surroundings." + ); + manMap.set( + "rm", + "Alfred: Remember, the 'rm' command\nneutralizes enemy files." + ); + manMap.set( + "cd", + "Alfred: Do recall, the 'cd' command\npermits you to navigate through rooms and items." + ); + manMap.set( + "alfred", + "Alfred: Try using the 'cd' command to traverse through\ndifferent areas. Then use 'rm' to remove critical files." + ); + + // Add text input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "80%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; // Change background color to white + this.inputField.style.color = "#fff"; // Change text color to black + this.inputField.placeholder = ">$"; // Placeholder text + this.inputField.style.border = "2px solid gold"; + + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + + this.add.text( + 410, + 59, + "Enter the 'control_room' and remove the \n'surveillance_camera' so you can proceed\ninto the next area. Namuh has security\nroaming the area so time is of the essence.", + { + color: "#fff", + fontSize: "17px", + fontFamily: "Monospace", + } + ); + + this.input.keyboard?.removeCapture( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); + + this.input.keyboard?.on("keydown", (event: KeyboardEvent) => { + if (event.key === "Enter") { + const newText = this.inputField.value; + this.lastText.push(newText.trim()); + + if (newText.trim() !== "") { + if (newText.trim() == "ls") { + lsDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addLsToContainer(lsMap.get(state) as string); + } else if (newText.substring(0, 3) == "cd ") { + let cdInput: string = newText.substring( + 3, + newText.length + ); + // CD .. FUNCTIONALITY BELOW + const backState = cdBack.get(state); + const cdState = cdMap.get(state); + if (backState !== undefined && cdInput == "..") { + cdBackDing.play(); + + state = backState; + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + } + // CD FUNCTIONALITY BELOW + else if ( + cdState !== undefined && + cdMap.get(state)?.includes(cdInput) + ) { + cdDing.play(); + + state = newText.substring(3); + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + } + // CD DIRECTORY NOT FOUND BELOW + else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer("Directory not found"); + } + // MAN INPUT BELOW + } else if (newText.substring(0, 4) == "man ") { + let manInput: string = newText.substring(4); + + const manState = manMap.get(manInput); + if (manState !== undefined) { + manDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + manMap.get(manInput) as string + ); + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Command '" + manInput + "' not found" + ); + } + } else if (newText.substring(0, 3) == "rm ") { + let rmInput: string = newText.substring(3); + if (rmMap.get(state)?.includes(rmInput)) { + // Remove the file from the listing and update the map + let files = lsMap.get(state) || ""; + files = files + .replace(rmInput, "") + .trim() + .replace(/\s{2,}/g, " "); // Remove the file and extra spaces + lsMap.set(state, files); + + // Optionally, remove the file from the rmMap if you want to prevent further references + // rmMap.get(state)?.splice(rmMap.get(state)?.indexOf(rmInput), 1); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + rmInput + "' removed successfully." + ); + + // Check if the level's objective is achieved, e.g., if all required files are removed + if ( + state === "control_room" && + !files.includes("surveillance_camera") + ) { + this.objectiveCompleted = true; + // Level completion logic here + this.addTextToContainer( + "\nObjective complete: Classified file removed. \nGood work, " + + this.username + + "!" + ); + this.time.delayedCall( + 3000, + this.loadLevel, + [], + this + ); + } + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + + rmInput + + "' cannot be found or removed." + ); + } + } + // NONSENSE INPUT BELOW + else { + ding.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Command '" + newText + "' not found" + ); + } + } + } + + if (event.key === "ArrowUp") { + let index = this.lastText.length + this.lastPosition; + if (index > 0) { + this.inputField.value = this.lastText[index]; + this.lastPosition -= 1; + } + } + if (event.key === "ArrowDown") { + let index = this.lastText.length + this.lastPosition; + if (index < this.lastText.length - 2) { + this.inputField.value = this.lastText[index + 2]; + this.lastPosition += 1; + } + } + }); + + let time = 60; + let lastUpdateTime = Date.now(); + + this.timer = this.add.text(75, 655, time.toFixed(2), { + fontSize: "50px", + color: "red", + }); + + const updateTimer = () => { + if (!this.objectiveCompleted) { + const currentTime = Date.now(); + const elapsedTime = currentTime - lastUpdateTime; + + time -= elapsedTime / 1000; // Adjust time based on elapsed time in seconds + lastUpdateTime = currentTime; // Update the last update time + + if (time > 0) { + this.timer.setText(time.toFixed(2)); // Update the timer text + this.time.delayedCall(10, updateTimer); + } else { + this.timer.setText("0.00"); + this.scene.start("SecurityBreachScene", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + }); + } + } + }; + + updateTimer(); + + this.stateText = this.add.text(1075, 95, state, { + fontSize: "24px", + color: "#fff", + }); + this.events.on("shutdown", this.removeInputField, this); + } + removeInputField() { + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + } + update() {} + + addLsToContainer(text: string) { + const words = text.split(" "); + + const numNewlines = words.length; + + this.inputContainer.y -= numNewlines * 24.7; + + for (let word of words) { + if (word.substring(0, 5) === "file_") { + let newWord = word.substring(5); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#77C3EC", + }); + this.inputContainer.add(newText); + } else if (word.substring(0, 4) === "dir_") { + let newWord = word.substring(4); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#86DC3D", + }); + this.inputContainer.add(newText); + } else { + const newText = this.add.text(0, 0, word, { + fontSize: "24px", + color: "#fff", + }); + this.inputContainer.add(newText); + } + + this.repositionTextObjects(); + } + } + + addTextToContainer(text: string) { + const newText = this.add.text(0, 0, text, { + fontSize: "24px", + color: "#fff", + }); + + const numNewlines = (text.match(/\n/g) || []).length + 1; + + // Adjust y position based on the number of newline characters + this.inputContainer.y -= numNewlines * 24.7; + + if (text.includes("Alfred: ")) { + newText.setColor("gold"); + } + if (text.includes("Objective complete: ")) { + newText.setColor("lime"); + } + + // Add the new text object to the container + this.inputContainer.add(newText); + + // Reposition text objects vertically within the container + this.repositionTextObjects(); + } + + repositionTextObjects() { + let yPos = 0; + + // Loop through all text objects in the container and position them vertically + this.inputContainer.iterate((child: Phaser.GameObjects.GameObject) => { + if (child instanceof Phaser.GameObjects.Text) { + child.y = yPos; + yPos += child.height; + } + }); + } + + loadLevel() { + this.removeInputField(); + this.scene.start("LevelSelect", { + username: this.username, + lvl2: true, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + } +} diff --git a/src/scenes/level01_load.ts b/src/scenes/level01_load.ts new file mode 100644 index 00000000..b3b10c26 --- /dev/null +++ b/src/scenes/level01_load.ts @@ -0,0 +1,132 @@ +import Phaser from "phaser"; + +export default class LoadingScene1 extends Phaser.Scene { + private content: string[]; // text to display + private charDelay: number; // delay between characters + private lineDelay: number; // delay between lines + private startX: number; // start X position of the text + private startY: number; // start Y position of the text + private currentLine: Phaser.GameObjects.Text; // Text object to display the current line + private lineIndex: number; // index of the current line + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private lvl5: boolean; + private username: string; + + constructor() { + super({ key: "LoadingScene1" }); + } + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: boolean; + lvl4: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + } + + preload() { + this.load.image("alfredicon", "assets/alfredicon.png"); + } + + create() { + this.resetScene(); + + this.add.rectangle(640, 360, 1280, 720, 0x000); + this.add.image(150, 100, "alfredicon").setDisplaySize(130, 130); + + //display text + this.displayNextLine(); + + // On enter, transition to Level 1 + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("Level01", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + }); + } + + resetScene() { + // helper to reset intial values on load + this.charDelay = 30; + this.lineDelay = 120; + this.startX = 250; + this.startY = 90; + this.lineIndex = 0; + this.content = [ + "Your mission, should you choose to accept it,", + "involves critical file manipulation. You need to", + "navigate to the 'control_room' and disable the 'surveillance_camera'.", + " ", + "Here are the commands at your disposal:", + " ", + " - 'ls' to list the contents of the current directory.", + " ", + " - 'cd ' to change the current directory.", + " Use 'cd ..' to go back.", + " ", + " - 'man ' to display the manual for a specific command.", + " ", + "You can always run 'man alfred' for additional", + "assistance to reach the end of a mission.", + " ", + "Disable the camera to advance further into Yortsed Corp.", + " ", + + " [Enter] to Continue", + ]; + } + + // helper to display text line by line, calling typeText to animate + displayNextLine() { + if (this.lineIndex < this.content.length) { + const line = this.content[this.lineIndex++]; + // Create a new text object for the current line + this.currentLine = this.add.text( + this.startX, + this.startY + 22 * (this.lineIndex - 1), + "", + { + fontSize: "24px", + color: "#fff", + } + ); + // Start typing the line + this.typeText(line); + } + } + + // helper to animate text typing + typeText(line: string) { + // split the line into characters + const characters = line.split(""); + let i = 0; + // add a delayed event for each character + this.time.addEvent({ + delay: this.charDelay, + repeat: characters.length - 1, + callback: () => { + this.currentLine.text += characters[i++]; + if (i === characters.length) { + // once all characters are added, add a delayed event to display the next line + this.time.delayedCall( + this.lineDelay, + this.displayNextLine, + [], + this + ); + } + }, + callbackScope: this, + }); + } +} diff --git a/src/scenes/level02.ts b/src/scenes/level02.ts new file mode 100644 index 00000000..7208394d --- /dev/null +++ b/src/scenes/level02.ts @@ -0,0 +1,579 @@ +import Phaser from "phaser"; + +export default class Level2Scene extends Phaser.Scene { + private stateText: Phaser.GameObjects.Text; + private inputField: HTMLInputElement; + private inputContainer: Phaser.GameObjects.Container; + private timer: Phaser.GameObjects.Text; + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private username: string; + private lvl5: boolean; + private genComplete: boolean = false; + private objectiveCompleted: boolean = false; + private lastText: string[] = [""]; + private lastPosition: number = -1; + + constructor() { + super({ key: "Level02" }); + } + + init(data: { + username: string; + + lvl1: boolean; + + lvl2: boolean; + + lvl3: boolean; + + lvl4: boolean; + + lvl5: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + this.lvl5 = data.lvl5; + } + preload() {} + + create() { + this.objectiveCompleted = false; + this.add.rectangle(640, 360, 1280, 720, 0x000); + + this.add.image(640, 100, "prompt").setDisplaySize(560, 110); + this.add.image(155, 100, "alfredicon").setDisplaySize(130, 130); + this.add.image(1050, 100, "pin").setDisplaySize(30, 40); + + let ding = this.sound.add("ding", { loop: false }); + let lsDing = this.sound.add("lsDing", { loop: false }); + let cdDing = this.sound.add("cdDing", { loop: false }); + let cdBackDing = this.sound.add("cdBackDing", { loop: false }); + let manDing = this.sound.add("manDing", { loop: false }); + + this.inputContainer = this.add.container(360, 520); + + const maskGraphics = this.make.graphics(); + maskGraphics.fillRect(300, 185, 1080, 500); + const mask = new Phaser.Display.Masks.GeometryMask(this, maskGraphics); + + this.inputContainer.setMask(mask); + + this.addTextToContainer("Alfred: Welcome back " + this.username + "!"); + + let state: string = "home"; + + const lsMap = new Map(); + const cdMap = new Map(); + const cdBack = new Map(); + const manMap = new Map(); + const rmMap = new Map(); + const mvMap = new Map(); + + lsMap.set( + "home", + "dir_generator1 dir_generator2 dir_laboratory file_emp_bomb1 file_emp_bomb2" + ); + lsMap.set("generator1", ""); + lsMap.set("generator2", ""); + + cdMap.set("home", ["generator1", "generator2", "laboratory"]); + + cdBack.set("generator1", "home"); + cdBack.set("generator2", "home"); + + rmMap.set("control_room", ["surveillance_camera"]); + + mvMap.set("home", ["emp_bomb1", "emp_bomb2"]); + mvMap.set("generator1", ["emp_bomb1"]); + mvMap.set("generator2", ["emp_bomb2"]); + + manMap.set( + "ls", + "Alfred: Remember, the 'ls' command\nis useful for viewing your surroundings." + ); + manMap.set( + "rm", + "Alfred: Remember, the 'rm' command\nneutralizes enemy files." + ); + manMap.set( + "cd", + "Alfred: Do recall, the 'cd' command\npermits you to navigate through rooms and items." + ); + manMap.set( + "alfred", + "Alfred: Try using the 'cd' command to traverse through\ndifferent areas. Then use 'rm' to remove critical files." + ); + + // Add text input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "80%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; // Change background color to white + this.inputField.style.color = "#fff"; // Change text color to black + this.inputField.placeholder = ">$"; // Placeholder text + this.inputField.style.border = "2px solid gold"; + + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + + this.add.text( + 410, + 59, + "Move the 'emp_bomb' files into their\nrespective 'generator' directories.\n", + { + color: "#fff", + fontSize: "20px", + fontFamily: "Monospace", + } + ); + + this.input.keyboard?.removeCapture( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); + + this.input.keyboard?.on("keydown", (event: KeyboardEvent) => { + if (event.key === "Enter") { + this.lastPosition = -1; + const newText = this.inputField.value; + this.lastText.push(newText.trim()); + + if (newText.trim() !== "") { + if (newText.trim() == "ls") { + lsDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addLsToContainer(lsMap.get(state) as string); + } else if (newText.substring(0, 3) == "cd ") { + let cdInput: string = newText.substring( + 3, + newText.length + ); + // CD .. FUNCTIONALITY BELOW + const backState = cdBack.get(state); + const cdState = cdMap.get(state); + if (backState !== undefined && cdInput == "..") { + cdBackDing.play(); + + state = backState; + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + } + // CD FUNCTIONALITY BELOW + else if ( + cdState !== undefined && + cdMap.get(state)?.includes(cdInput) + ) { + if (cdInput == "laboratory" && !this.genComplete) { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Alfred: You cannot enter the laboratory until \nthe generators are shut down. Shut them down by \nmoving your emp_bombs into their directories." + ); + } else if ( + cdInput == "laboratory" && + this.genComplete + ) { + this.objectiveCompleted = true; + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + // Level completion logic here + this.addTextToContainer( + "\nObjective complete: Laboratory access granted.\nGood work, " + + this.username + + "." + ); + this.time.delayedCall( + 3000, + this.loadLevel, + [], + this + ); + } else { + cdDing.play(); + + state = newText.substring(3); + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + } + } + // CD DIRECTORY NOT FOUND BELOW + else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer("Directory not found"); + } + // MAN INPUT BELOW + } else if (newText.substring(0, 4) == "man ") { + let manInput: string = newText.substring(4); + + const manState = manMap.get(manInput); + if (manState !== undefined) { + manDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + manMap.get(manInput) as string + ); + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Command '" + manInput + "' not found" + ); + } + } else if (newText.substring(0, 3) == "rm ") { + let rmInput: string = newText.substring(3); + if (rmMap.get(state)?.includes(rmInput)) { + // Remove the file from the listing and update the map + let files = lsMap.get(state) || ""; + files = files + .replace(rmInput, "") + .trim() + .replace(/\s{2,}/g, " "); // Remove the file and extra spaces + lsMap.set(state, files); + + // Optionally, remove the file from the rmMap if you want to prevent further references + // rmMap.get(state)?.splice(rmMap.get(state)?.indexOf(rmInput), 1); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + rmInput + "' removed successfully." + ); + + // Check if the level's objective is achieved, e.g., if all required files are removed + if ( + state === "control_room" && + !files.includes("surveillance_camera") + ) { + this.objectiveCompleted = true; + // Level completion logic here + this.addTextToContainer( + "Objective complete: Classified file removed. \nGood work, " + + this.username + + "!" + ); + this.time.delayedCall( + 3000, + this.loadLevel, + [], + this + ); + } + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + + rmInput + + "' cannot be found or removed." + ); + } + } else if (newText.substring(0, 3) == "mv ") { + let mvInput: string = newText.substring(3); + + let words: string[] = mvInput.split(" "); + + let word1 = words[0]; + let word2 = words[1]; + + if ( + mvMap.get(word2)?.includes(word1) && + lsMap.get(state)?.includes(word1) + ) { + let files = lsMap.get(state) || ""; + files = files + .replace(word1, "") + .trim() + .replace(/\s{2,}/g, " "); // Remove the file and extra spaces + lsMap.set(state, files); + + let originalValue: string | undefined = + lsMap.get(word2); + + let newValue: string = originalValue + " " + word1; + + lsMap.set(word2, newValue); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + word1 + "' moved successfully." + ); + if ( + !lsMap.get("home")?.includes("emp_bomb1") && + !lsMap.get("home")?.includes("emp_bomb2") && + !this.genComplete + ) { + this.genComplete = true; + + this.addTextToContainer( + "\nAlfred: Masterful work " + + this.username + + "!\nBoth generators have shut down. \nNow enter the laboratory to finish the mission." + ); + } + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + + word1 + + "' cannot be moved to " + + word2 + + "." + ); + } + } + // NONSENSE INPUT BELOW + else { + ding.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Command '" + newText + "' not found" + ); + } + } + } + + if (event.key === "ArrowUp") { + let index = this.lastText.length + this.lastPosition; + if (index > 0) { + this.inputField.value = this.lastText[index]; + this.lastPosition -= 1; + } + } + if (event.key === "ArrowDown") { + let index = this.lastText.length + this.lastPosition; + if (index < this.lastText.length - 2) { + this.inputField.value = this.lastText[index + 2]; + this.lastPosition += 1; + } + } + }); + + let time = 60; + let lastUpdateTime = Date.now(); + + this.timer = this.add.text(75, 655, time.toFixed(2), { + fontSize: "50px", + color: "red", + }); + + const updateTimer = () => { + if (!this.objectiveCompleted) { + const currentTime = Date.now(); + const elapsedTime = currentTime - lastUpdateTime; + + time -= elapsedTime / 1000; // Adjust time based on elapsed time in seconds + lastUpdateTime = currentTime; // Update the last update time + + if (time > 0) { + this.timer.setText(time.toFixed(2)); // Update the timer text + this.time.delayedCall(10, updateTimer); + } else { + this.timer.setText("0.00"); + this.scene.start("SecurityBreachScene", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + }); + } + } + }; + + updateTimer(); + + this.stateText = this.add.text(1075, 95, state, { + fontSize: "24px", + color: "#fff", + }); + this.events.on("shutdown", this.removeInputField, this); + } + removeInputField() { + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + } + update() {} + + addLsToContainer(text: string) { + const words = text.split(" "); + + const numNewlines = words.length; + + this.inputContainer.y -= numNewlines * 24.7; + + for (let word of words) { + if (word.substring(0, 5) === "file_") { + let newWord = word.substring(5); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#77C3EC", + }); + this.inputContainer.add(newText); + } else if (word.substring(0, 4) === "dir_") { + let newWord = word.substring(4); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#86DC3D", + }); + this.inputContainer.add(newText); + } else { + const newText = this.add.text(0, 0, word, { + fontSize: "24px", + color: "#fff", + }); + this.inputContainer.add(newText); + } + + this.repositionTextObjects(); + } + } + + addTextToContainer(text: string) { + const newText = this.add.text(0, 0, text, { + fontSize: "24px", + color: "#fff", + }); + + const numNewlines = (text.match(/\n/g) || []).length + 1; + + // Adjust y position based on the number of newline characters + this.inputContainer.y -= numNewlines * 24.7; + + if (text.includes("Alfred: ")) { + newText.setColor("gold"); + } + if (text.includes("Objective complete: ")) { + newText.setColor("lime"); + } + + // Add the new text object to the container + this.inputContainer.add(newText); + + // Reposition text objects vertically within the container + this.repositionTextObjects(); + } + + repositionTextObjects() { + let yPos = 0; + + // Loop through all text objects in the container and position them vertically + this.inputContainer.iterate((child: Phaser.GameObjects.GameObject) => { + if (child instanceof Phaser.GameObjects.Text) { + child.y = yPos; + yPos += child.height; + } + }); + } + + loadLevel() { + this.removeInputField(); + this.scene.start("LevelSelect", { + username: this.username, + lvl2: true, + lvl3: true, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + } +} diff --git a/src/scenes/level02_load.ts b/src/scenes/level02_load.ts new file mode 100644 index 00000000..92fe1fde --- /dev/null +++ b/src/scenes/level02_load.ts @@ -0,0 +1,137 @@ +import Phaser from "phaser"; + +export default class LoadingScene2 extends Phaser.Scene { + private content: string[]; // text to display + private charDelay: number; // delay between characters + private lineDelay: number; // delay between lines + private startX: number; // start X position of the text + private startY: number; // start Y position of the text + private currentLine: Phaser.GameObjects.Text; // Text object to display the current line + private lineIndex: number; // index of the current line + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private lvl5: boolean; + private username: string; + + constructor() { + super({ key: "LoadingScene2" }); + } + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: boolean; + lvl4: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + } + + preload() { + this.load.image("alfredicon", "assets/alfredicon.png"); + } + + create() { + this.resetScene(); + + this.add.rectangle(640, 360, 1280, 720, 0x000); + this.add.image(150, 100, "alfredicon").setDisplaySize(130, 130); + + //display text + this.displayNextLine(); + + // On enter, transition to Level 1 + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("LoadingScene2part2", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + }); + } + + resetScene() { + // helper to reset intial values on load + this.charDelay = 30; + this.lineDelay = 120; + this.startX = 250; + this.startY = 90; + this.lineIndex = 0; + this.content = [ + "Wonderful, you've disabled the camera and can advance into", + "Yortsed Corp. You are one step closer to stopping", + "Namuh from taking total control of the city's power.", + " ", + "Along with all of your previous commands, you will", + "need to utilize the 'mv' command to move objects", + "into different directories.", + " ", + "The command works as follows:", + " ", + " - 'mv '", + " ", + "Below is an example of moving an existing file named 'file1'", + "into a folder named 'folder3'", + " ", + "mv file1 folder3", + " ", + "Remember that for you to move a file you must be in the", + "directory in which that file exists.", + " ", + "You will use this command to move 'emp_bomb1' and 'emp_bomb2'", + "into each 'generator' directory. Only then will you be able", + "to access the 'laboratory' directory, and finish the mission.", + " ", + " [Enter] to Continue", + ]; + } + + // helper to display text line by line, calling typeText to animate + displayNextLine() { + if (this.lineIndex < this.content.length) { + const line = this.content[this.lineIndex++]; + // Create a new text object for the current line + this.currentLine = this.add.text( + this.startX, + this.startY + 22 * (this.lineIndex - 1), + "", + { + fontSize: "24px", + color: "#fff", + } + ); + // Start typing the line + this.typeText(line); + } + } + + // helper to animate text typing + typeText(line: string) { + // split the line into characters + const characters = line.split(""); + let i = 0; + // add a delayed event for each character + this.time.addEvent({ + delay: this.charDelay, + repeat: characters.length - 1, + callback: () => { + this.currentLine.text += characters[i++]; + if (i === characters.length) { + // once all characters are added, add a delayed event to display the next line + this.time.delayedCall( + this.lineDelay, + this.displayNextLine, + [], + this + ); + } + }, + callbackScope: this, + }); + } +} diff --git a/src/scenes/level02_load2.ts b/src/scenes/level02_load2.ts new file mode 100644 index 00000000..6b9884a9 --- /dev/null +++ b/src/scenes/level02_load2.ts @@ -0,0 +1,136 @@ +import Phaser from "phaser"; + +export default class LoadingScene2Part2 extends Phaser.Scene { + private content: string[]; // text to display + private charDelay: number; // delay between characters + private lineDelay: number; // delay between lines + private startX: number; // start X position of the text + private startY: number; // start Y position of the text + private currentLine: Phaser.GameObjects.Text; // Text object to display the current line + private lineIndex: number; // index of the current line + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private lvl5: boolean; + private username: string; + + constructor() { + super({ key: "LoadingScene2part2" }); + } + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: boolean; + lvl4: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + } + + preload() { + this.load.image("alfredicon", "assets/alfredicon.png"); + } + + create() { + this.resetScene(); + + this.add.rectangle(640, 360, 1280, 720, 0x000); + this.add.image(150, 100, "alfredicon").setDisplaySize(130, 130); + + //display text + this.displayNextLine(); + + // On enter, transition to Level 1 + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("Level02", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + }); + } + + resetScene() { + // helper to reset intial values on load + this.charDelay = 30; + this.lineDelay = 120; + this.startX = 250; + this.startY = 90; + this.lineIndex = 0; + this.content = [ + "Note that the second argument of the mv command", + "is also used to rename a file. This works when", + "the second argument is not an existing directory.", + " ", + "Below is an example of renaming file1 into updated_file1:", + " ", + " - 'mv file1 updated_file1'", + " ", + "In this mission you will only be using the command to", + "move existing files into existing directories.", + + "If you are ever in need of assistance you can utiilize", + "'man' on a specific command to retrieve its manual.", + " ", + "Or if you require a hint for the mission, you can run", + "'man alfred' to dial me in.", + " ", + "Security has been notified of the faulty camera you disabled", + "so they are in the area. Time will be of the essence.", + " ", + "Good luck " + this.username.toLowerCase() + ".", + " ", + + " [Enter] to Continue", + ]; + } + + // helper to display text line by line, calling typeText to animate + displayNextLine() { + if (this.lineIndex < this.content.length) { + const line = this.content[this.lineIndex++]; + // Create a new text object for the current line + this.currentLine = this.add.text( + this.startX, + this.startY + 22 * (this.lineIndex - 1), + "", + { + fontSize: "24px", + color: "#fff", + } + ); + // Start typing the line + this.typeText(line); + } + } + + // helper to animate text typing + typeText(line: string) { + // split the line into characters + const characters = line.split(""); + let i = 0; + // add a delayed event for each character + this.time.addEvent({ + delay: this.charDelay, + repeat: characters.length - 1, + callback: () => { + this.currentLine.text += characters[i++]; + if (i === characters.length) { + // once all characters are added, add a delayed event to display the next line + this.time.delayedCall( + this.lineDelay, + this.displayNextLine, + [], + this + ); + } + }, + callbackScope: this, + }); + } +} diff --git a/src/scenes/levelSelect.ts b/src/scenes/levelSelect.ts new file mode 100644 index 00000000..1c91d998 --- /dev/null +++ b/src/scenes/levelSelect.ts @@ -0,0 +1,289 @@ +import Phaser from "phaser"; + +export default class LevelSelect extends Phaser.Scene { + private cursors?: Phaser.Types.Input.Keyboard.CursorKeys; + private player?: Phaser.Physics.Arcade.Sprite; + private platforms?: Phaser.Physics.Arcade.StaticGroup; + private doors?: Phaser.Physics.Arcade.StaticGroup; + private lvl1?: boolean = true; + private lvl2?: boolean = false; + private lvl3?: boolean = false; + private lvl4?: boolean = false; + private lvl5?: boolean = false; + private username: string; + private lastDirection: string; + + constructor() { + super({ key: "LevelSelect" }); + } + + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: boolean; + lvl4: boolean; + lvl5: boolean; + }) { + this.username = data.username; + + if (this.username === "admin") { + this.lvl2 = true; + this.lvl3 = true; + this.lvl4 = true; + this.lvl5 = true; + } else { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.lvl5 = data.lvl5; + } + this.lvl1 = true; + } + + create() { + this.platforms = this.physics.add.staticGroup(); + this.cameras.main.setBackgroundColor("#A9A9A9"); + + const groundWidth = this.scale.width; + const groundX = this.scale.width / 2; + const groundX2 = groundWidth + groundWidth / 2; + + const ground = this.platforms.create( + groundX, + 568, + "ground" + ) as Phaser.Physics.Arcade.Sprite; + + ground + .setScale(groundWidth / ground.width, 1) + .refreshBody() + .setTint(808080); + const ground2 = this.platforms.create(groundX2, 568, "ground"); + ground2 + .setScale(groundWidth / ground.width, 1) + .refreshBody() + .setTint(808080); + + this.add.image(1175, 330, "arrow").setScale(0.5); + + const walls = [{ x: -357 }, { x: 2555 }]; + + walls.forEach((pos) => { + this.platforms + ?.create(pos.x, 0, "ground") + .setOrigin(0, 0) + .setScale(1, this.scale.height) + .refreshBody() + .setTint(808080); + }); + + // Title Text + + const title = this.add.text(475, 100, "Level Select", { + fontFamily: "Arial", + fontSize: 60, + color: "#000000", + }); + title.setStroke("#FFFF00", 6); + const levelTextPositions = [ + { x: 490, level: "1" }, + { x: 940, level: "2" }, + { x: 1390, level: "3" }, + { x: 1840, level: "4" }, + { x: 2290, level: "5" }, + ]; + levelTextPositions.forEach((pos) => { + const text = this.add.text(pos.x, 430, pos.level, { + fontFamily: "Arial", + fontSize: 24, + color: "#000000", + }); + text.setStroke("#FFFF00", 6); + }); + + // this.player = this.physics.add.sprite(250, 370, "spy"); + this.player = this.physics.add.sprite(250, 370, "dude").setScale(0.2); + this.player.setCollideWorldBounds(false); + this.player.setDepth(1); + this.cameras.main.setBounds(0, 0, 2595, this.scale.height); + + this.cameras.main.startFollow(this.player); + + this.anims.create({ + key: "left", + frames: this.anims.generateFrameNumbers("dude", { + start: 7, + end: 0, + }), + frameRate: 12, + repeat: -1, + }); + + this.anims.create({ + key: "idleLeft", + frames: [{ key: "dude", frame: 8 }], + frameRate: 12, + }); + this.anims.create({ + key: "idleRight", + frames: [{ key: "dude", frame: 9 }], + frameRate: 12, + }); + + this.anims.create({ + key: "right", + frames: this.anims.generateFrameNumbers("dude", { + start: 10, + end: 18, + }), + frameRate: 12, + repeat: -1, + }); + + this.physics.add.collider(this.player, this.platforms); + this.cursors = this.input.keyboard?.createCursorKeys(); + + this.doors = this.physics.add.staticGroup(); + this.doors.setDepth(0); + + const wallDoor = this.doors.create( + 48, + 507, + "wallDoor" + ) as Phaser.Physics.Arcade.Sprite; + wallDoor.setScale(0.25).refreshBody(); + const backDoor = this.doors.create( + 79, + 507, + "backwardsDoor" + ) as Phaser.Physics.Arcade.Sprite; + backDoor.setScale(0.25).refreshBody().setVisible(false); + + this.physics.add.overlap(this.player, wallDoor, () => { + wallDoor.setVisible(false); + backDoor.setVisible(true); + this.player?.setVisible(false); + this.player?.setY(590); + this.time.delayedCall(600, () => { + wallDoor.setVisible(true); + backDoor.setVisible(false); + this.time.delayedCall(600, () => { + console.log(this.username); + this.scene.start("IntroScene", { + username: this.username, + lvl1: this.lvl1, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + }); + }); + }); + }); + + //level doors + + const doorPositions = [ + { + x: 500, + state: this.lvl1, + scene: "LoadingScene1", + }, + { x: 950, state: this.lvl2, scene: "LoadingScene2" }, + { x: 1400, state: this.lvl3, scene: "LevelThreeIntro" }, + { x: 1850, state: this.lvl4, scene: "" }, + { x: 2300, state: this.lvl5, scene: "" }, + ]; + + doorPositions.forEach((pos) => { + const closedDoor = this.doors?.create( + pos.x, + 507, + pos.state ? "closed_metal_door" : "lockedDoor" + ) as Phaser.Physics.Arcade.Sprite; + closedDoor.setScale(0.25).refreshBody(); + closedDoor.setVisible(true); + const openDoor = this.doors?.create( + pos.x, + 507, + "open_metal_door" + ) as Phaser.Physics.Arcade.Sprite; + openDoor.setScale(0.25).refreshBody(); + openDoor.setVisible(false); + + pos.state && this.player + ? this.physics.add.overlap(this.player, closedDoor, () => { + closedDoor.setVisible(false); + openDoor.setVisible(true); + this.time.delayedCall(50, () => { + closedDoor.setVisible(true); + openDoor.setVisible(false); + }); + + if ( + openDoor.visible && + this.input.keyboard?.checkDown( + this.input.keyboard.addKey( + Phaser.Input.Keyboard.KeyCodes.UP + ), + 500 + ) + ) { + this.tweens.add({ + targets: this.player, + duration: 500, + scaleX: 0, + scaleY: 0, + angle: 360, + y: "-=40", + onComplete: () => { + this.time.delayedCall(1000, () => { + this.sound.stopAll(); + this.scene.start(pos.scene, { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + }); + }, + }); + } + }) + : null; + }); + } + update() { + if (!this.cursors) { + return; + } + if (!this.lastDirection) { + this.lastDirection = "right"; // Assuming player starts facing right + } + + if (this.cursors.left.isDown) { + this.player?.setVelocityX(-400); + this.player?.anims.play("left", true); + this.lastDirection = "left"; // Update last movement direction + } else if (this.cursors.right.isDown) { + this.player?.setVelocityX(400); + this.player?.anims.play("right", true); + this.lastDirection = "right"; // Update last movement direction + } else { + this.player?.setVelocityX(0); + + if (this.lastDirection === "left") { + this.player?.anims.play("idleLeft", true); + } else { + this.player?.anims.play("idleRight", true); + } + } + + if (this.cursors.up.isDown && this.player?.body?.touching.down) { + this.player.setVelocityY(-300); + } else if (this.cursors.down.isDown) { + this.player?.setVelocityY(300); + } + } +} diff --git a/src/scenes/login.ts b/src/scenes/login.ts new file mode 100644 index 00000000..63c7d3e9 --- /dev/null +++ b/src/scenes/login.ts @@ -0,0 +1,164 @@ +import Phaser from "phaser"; + +export default class LoginScene extends Phaser.Scene { + private stateText: Phaser.GameObjects.Text; + private inputField: HTMLInputElement; + private inputContainer: Phaser.GameObjects.Container; + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + + constructor() { + super({ key: "LoginScene" }); + } + + init(data: { lvl1: boolean; lvl2: boolean; lvl3: boolean; lvl4: boolean }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + } + preload() {} + + create() { + this.add.rectangle(640, 360, 1280, 720, 0x000); + + let lsDing = this.sound.add("cdDing", { loop: false }); + + // Add text input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "60%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; + this.inputField.style.color = "#fff"; + this.inputField.placeholder = "Enter Username"; // Placeholder text + this.inputField.style.border = "2px solid gold"; + + const authorizationText = this.add.text( + 470, + 257, + "Authorization Required", + { + color: "#fff", + fontSize: "24px", + fontFamily: "Monospace", + } + ); + + const loginText = this.add.text(590, 340, "LOGIN", { + color: "#fff", + fontSize: "24px", + fontFamily: "Monospace", + }); + + const accessGranted = this.add.text(480, 400, "ACCESS GRANTED", { + color: "gold", + fontSize: "32px", + fontFamily: "Monospace", + }); + + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + + this.add.text(410, 59, "", { + color: "#fff", + fontSize: "17px", + fontFamily: "Monospace", + }); + + const loadingBars = [ + this.add.rectangle(360, 430, 80, 20, 0xffd700), + this.add.rectangle(400, 430, 160, 20, 0xffd700), + this.add.rectangle(440, 430, 240, 20, 0xffd700), + this.add.rectangle(480, 430, 320, 20, 0xffd700), + this.add.rectangle(520, 430, 400, 20, 0xffd700), + this.add.rectangle(560, 430, 480, 20, 0xffd700), + this.add.rectangle(600, 430, 560, 20, 0xffd700), + this.add.rectangle(640, 430, 640, 20, 0xffd700), + this.add.rectangle(640, 430, 640, 20, 0xffd700), + ]; + + // Set all loading bars initially invisible + loadingBars.forEach((bar) => { + bar.visible = false; + }); + + // Index to keep track of which loading bar to show + let currentBarIndex = 0; + + accessGranted.visible = false; + + // Keyboard event listener for the Enter key + const enterListener = (event: KeyboardEvent) => { + if (event.key === "Enter") { + const username = this.inputField.value; + + this.inputField.value = ""; // Empty the input field + loginText.visible = false; + authorizationText.visible = false; + + // Play sound + lsDing.play(); + + this.input.keyboard?.removeListener("keydown", enterListener); + + this.removeInputField(); + + // Function to show the next loading bar and schedule its hiding + const showNextLoadingBar = () => { + // Show the current loading bar + loadingBars[currentBarIndex].visible = true; + + // Increment the index for the next loading bar + currentBarIndex++; + + // If all loading bars have been shown, return + if (currentBarIndex >= loadingBars.length) { + if (currentBarIndex >= loadingBars.length) { + accessGranted.visible = true; + + loadingBars[8].visible = false; + + this.time.delayedCall(2200, () => { + if (username === "admin") { + this.scene.start("LevelSelect", { + username: username, + }); + } else { + this.scene.start("IntroScene", { + username: username, + }); + } + }); + + return; + } + } + + this.time.delayedCall(100, () => { + loadingBars[currentBarIndex - 1].visible = false; + + showNextLoadingBar(); + }); + }; + + showNextLoadingBar(); + } + }; + + this.input.keyboard?.on("keydown", enterListener); + + this.events.on("shutdown", this.removeInputField, this); + } + removeInputField() { + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + } + + update() {} +} diff --git a/src/scenes/lvl03Main.ts b/src/scenes/lvl03Main.ts new file mode 100644 index 00000000..0856105e --- /dev/null +++ b/src/scenes/lvl03Main.ts @@ -0,0 +1,498 @@ +import Phaser from "phaser"; + +export default class Level03 extends Phaser.Scene { + private stateText: Phaser.GameObjects.Text; + private inputField: HTMLInputElement; + private inputContainer: Phaser.GameObjects.Container; + private timer: Phaser.GameObjects.Text; + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private username: string; + private lvl5: boolean; + private objectiveCompleted: boolean = false; + private lastText: string[] = [""]; + private lastPosition: number = -1; + + constructor() { + super({ key: "Level03" }); + } + + init(data: { + username: string; + + lvl1: boolean; + + lvl2: boolean; + + lvl3: boolean; + + lvl4: boolean; + + lvl5: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + this.lvl5 = data.lvl5; + } + preload() { + this.load.image("1", "assets/num1.png"); + this.load.image("2", "assets/num2.png"); + this.load.image("3", "assets/num3.png"); + this.load.image("4", "assets/num4.png"); + this.load.image("5", "assets/num5.png"); + this.load.image("6", "assets/num6.png"); + this.load.image("7", "assets/num7.png"); + this.load.image("8", "assets/num8.png"); + this.load.image("9", "assets/num9.png"); + this.load.image("0", "assets/num0.png"); + this.load.image("padCheck", "assets/padCheck.png"); + this.load.image("padX", "assets/padx.png"); + this.load.image("pinPadText", "assets/PinPadText.png"); + } + + create() { + this.objectiveCompleted = false; + this.add.rectangle(640, 360, 1280, 720, 0x000); + + this.add.image(640, 100, "prompt").setDisplaySize(560, 110); + this.add.image(155, 100, "alfredicon").setDisplaySize(130, 130); + this.add.image(1050, 100, "pin").setDisplaySize(30, 40); + + function getRandomInt(min: number, max: number): number { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; + } + const randomNum1 = getRandomInt(1, 9).toString(); + const randomNum2 = getRandomInt(1, 9).toString(); + const randomNum3 = getRandomInt(1, 9).toString(); + const randomNum4 = getRandomInt(1, 9).toString(); + + //Padlock code + const imagePositions = [ + { x: 1040, y: 310, key: "1" }, + { x: 1110, y: 310, key: "2" }, + { x: 1180, y: 310, key: "3" }, + { x: 1040, y: 380, key: "4" }, + { x: 1110, y: 380, key: "5" }, + { x: 1180, y: 380, key: "6" }, + { x: 1040, y: 450, key: "7" }, + { x: 1110, y: 450, key: "8" }, + { x: 1180, y: 450, key: "9" }, + { x: 1040, y: 520, key: "padX" }, + { x: 1110, y: 520, key: "0" }, + { x: 1180, y: 520, key: "padCheck" }, + ]; + + const hoverTintColor = 0xd3d3d3; + const answer = randomNum1 + randomNum2 + randomNum3 + randomNum4; + + const displayScreen = this.add + .text(1065, 207, "", { + fontSize: "55px", + fontFamily: "Arial", + color: "#ffff00", + }) + .setDepth(1); + this.add + .image(1109, 239, "pinPadText") + .setDisplaySize(240, 80) + .setDepth(0); + //padlock hover tint code + imagePositions.forEach((pos) => { + const image = this.add + .image(pos.x, pos.y, pos.key) + .setDisplaySize(70, 70); + + image.setInteractive(); + + image.on("pointerover", () => { + image.setTint(hoverTintColor); + }); + + image.on("pointerout", () => { + image.clearTint(); + }); + + image.on("pointerdown", () => { + if (pos.key !== "padX" && pos.key !== "padCheck") { + if (displayScreen.text.length < 4) { + displayScreen.text += pos.key; + } + } else if (pos.key === "padX") { + // Handle backspace functionality + displayScreen.text = displayScreen.text.slice(0, -1); + } else { + if (displayScreen.text === answer) { + this.objectiveCompleted = true; + this.addTextToContainer("Access Granted"); + this.time.delayedCall(2000, () => { + this.scene.start("LevelSelect"); + }); + } else { + this.addTextToContainer("Access denied."); + } + } + }); + }); + + let ding = this.sound.add("ding", { loop: false }); + let lsDing = this.sound.add("lsDing", { loop: false }); + let cdDing = this.sound.add("cdDing", { loop: false }); + let cdBackDing = this.sound.add("cdBackDing", { loop: false }); + let manDing = this.sound.add("manDing", { loop: false }); + + this.inputContainer = this.add.container(360, 520); + + const maskGraphics = this.make.graphics(); + maskGraphics.fillRect(300, 185, 1080, 500); + const mask = new Phaser.Display.Masks.GeometryMask(this, maskGraphics); + + this.inputContainer.setMask(mask); + + this.addTextToContainer("Alfred: Welcome back " + this.username + "!"); + + let state: string = "back_door"; + + const lsMap = new Map(); + const catMap = new Map(); + const cdMap = new Map(); + const cdBack = new Map(); + const manMap = new Map(); + + lsMap.set("back_door", "dir_brick_pile dir_garbage_can dir_file_box"); + lsMap.set("brick_pile", "file_bricks file_rocks file_dirt"); + lsMap.set( + "garbage_can", + "file_soda_can file_sticks dir_cracked_phone file_broken_chair" + ); + lsMap.set( + "file_box", + "file_pencils dir_secret_folder_#1 dir_graph_paper" + ); + lsMap.set("cracked_phone", "dir_notes_app"); + lsMap.set("graph_paper", "file_code_#4.txt"); + lsMap.set("notes_app", "file_code_#1.txt"); + lsMap.set("secret_folder_#1", "file_code_#2.txt file_code_#3.txt"); + + catMap.set("code_#1.txt", randomNum1); + catMap.set("code_#2.txt", randomNum2); + catMap.set("code_#3.txt", randomNum3); + catMap.set("code_#4.txt", randomNum4); + + catMap.set("bricks.txt", "bricks"); + catMap.set("rocks.txt", "rocks"); + catMap.set("dirt.txt", "dirt"); + catMap.set("pencils.txt", "pencils"); + catMap.set("soda_can.txt", "soda_can"); + catMap.set("sticks.txt", "sticks"); + catMap.set("broken_chair.txt", "broken_chair"); + + cdMap.set("back_door", ["brick_pile", "garbage_can", "file_box"]); + cdMap.set("garbage_can", ["cracked_phone"]); + cdMap.set("file_box", ["secret_folder_#1", "graph_paper"]); + cdMap.set("cracked_phone", ["notes_app"]); + + cdBack.set("brick_pile", "back_door"); + cdBack.set("garbage_can", "back_door"); + cdBack.set("file_box", "back_door"); + cdBack.set("cracked_phone", "garbage_can"); + cdBack.set("graph_paper", "file_box"); + cdBack.set("notes_app", "cracked_phone"); + cdBack.set("secret_folder_#1", "file_box"); + + manMap.set( + "ls", + "Alfred: Remember, the 'ls' command\nis useful for viewing your surroundings." + ); + manMap.set( + "cd", + "Alfred: Do recall, the 'cd' command\npermits you to navigate through rooms and items." + ); + manMap.set( + "alfred", + "Alfred: Try using the 'cd' to look\nfor directories labeled secret_folder. Remember,\norder is important when typing in the on the pin-pad." + ); + manMap.set( + "cat", + "Alfred: The 'cat' command permits you\nto read a file's contents. Kind of like\nthe 'ls' command reads a directory's contents." + ); + + // Add text input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "80%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; // Change background color to white + this.inputField.style.color = "#fff"; // Change text color to black + this.inputField.placeholder = ">$"; // Placeholder text + this.inputField.style.border = "2px solid gold"; + + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + + this.add.text( + 410, + 59, + "Search directories for codes \nand type them into the pin-pad\nto advance further into the facility.", + { + color: "#fff", + fontSize: "17px", + fontFamily: "Monospace", + } + ); + + this.input.keyboard?.removeCapture( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); + + this.input.keyboard?.on("keydown", (event: KeyboardEvent) => { + if (event.key === "Enter") { + this.lastPosition = -1; + const newText = this.inputField.value; + if (newText.trim() !== "") { + if (newText.trim() == "ls") { + lsDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addLsToContainer(lsMap.get(state) as string); + } else if (newText.substring(0, 4) == "cat ") { + let catInput: string = newText.substring(4); + const catState = catMap.get(catInput); + if (catState !== undefined) { + lsDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer(catState); + } else { + ding.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer( + "File '" + catInput + "' not found" + ); + } + } else if (newText.substring(0, 3) == "cd ") { + let cdInput: string = newText.substring( + 3, + newText.length + ); + // CD .. FUNCTIONALITY BELOW + const backState = cdBack.get(state); + const cdState = cdMap.get(state); + if (backState !== undefined && cdInput == "..") { + cdBackDing.play(); + + state = backState; + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + } + // CD FUNCTIONALITY BELOW + else if ( + cdState !== undefined && + cdMap.get(state)?.includes(cdInput) + ) { + cdDing.play(); + + state = newText.substring(3); + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + } + // CD DIRECTORY NOT FOUND BELOW + else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer("Directory not found"); + } + // MAN INPUT BELOW + } else if (newText.substring(0, 4) == "man ") { + let manInput: string = newText.substring(4); + + const manState = manMap.get(manInput); + if (manState !== undefined) { + manDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer( + manMap.get(manInput) as string + ); + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer( + "Command '" + manInput + "' not found" + ); + } + } + // NONSENSE INPUT BELOW + else { + ding.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer( + "Command '" + newText + "' not found" + ); + } + } + } + if (event.key === "ArrowUp") { + let index = this.lastText.length + this.lastPosition; + if (index > 0) { + this.inputField.value = this.lastText[index]; + this.lastPosition -= 1; + } + } + if (event.key === "ArrowDown") { + let index = this.lastText.length + this.lastPosition; + if (index < this.lastText.length - 2) { + this.inputField.value = this.lastText[index + 2]; + this.lastPosition += 1; + } + } + }); + + let time = 120; + let lastUpdateTime = Date.now(); + + this.timer = this.add.text(75, 655, time.toFixed(2), { + fontSize: "50px", + color: "red", + }); + + const updateTimer = () => { + if (!this.objectiveCompleted) { + const currentTime = Date.now(); + const elapsedTime = currentTime - lastUpdateTime; + + time -= elapsedTime / 1000; // Adjust time based on elapsed time in seconds + lastUpdateTime = currentTime; // Update the last update time + + if (time > 0) { + this.timer.setText(time.toFixed(2)); // Update the timer text + this.time.delayedCall(10, updateTimer); + } else { + this.timer.setText("0.00"); + this.scene.start("SecurityBreachScene", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + }); + } + } + }; + + updateTimer(); + + this.stateText = this.add.text(1075, 95, state, { + fontSize: "24px", + color: "#fff", + }); + this.events.on("shutdown", this.removeInputField, this); + } + removeInputField() { + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + } + update() {} + + addLsToContainer(text: string) { + const words = text.split(" "); + + const numNewlines = words.length; + + this.inputContainer.y -= numNewlines * 24.7; + + for (let word of words) { + if (word.substring(0, 5) === "file_") { + let newWord = word.substring(5); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#77C3EC", + }); + this.inputContainer.add(newText); + } else if (word.substring(0, 4) === "dir_") { + let newWord = word.substring(4); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#86DC3D", + }); + this.inputContainer.add(newText); + } else { + const newText = this.add.text(0, 0, word, { + fontSize: "24px", + color: "#fff", + }); + this.inputContainer.add(newText); + } + + this.repositionTextObjects(); + } + } + + addTextToContainer(text: string) { + const newText = this.add.text(0, 0, text, { + fontSize: "24px", + color: "#fff", + }); + + const numNewlines = (text.match(/\n/g) || []).length + 1; + + // Adjust y position based on the number of newline characters + this.inputContainer.y -= numNewlines * 24.7; + + if (text.includes("Alfred: ")) { + newText.setColor("gold"); + } + if (text.includes("Objective complete: ")) { + newText.setColor("lime"); + } + + // Add the new text object to the container + this.inputContainer.add(newText); + + // Reposition text objects vertically within the container + this.repositionTextObjects(); + } + + repositionTextObjects() { + let yPos = 0; + + // Loop through all text objects in the container and position them vertically + this.inputContainer.iterate((child: Phaser.GameObjects.GameObject) => { + if (child instanceof Phaser.GameObjects.Text) { + child.y = yPos; + yPos += child.height; + } + }); + } + + loadLevel() { + this.removeInputField(); + this.scene.start("LevelSelect", { + username: this.username, + lvl2: true, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + } +} diff --git a/src/scenes/lvl03_intro.ts b/src/scenes/lvl03_intro.ts new file mode 100644 index 00000000..b613697a --- /dev/null +++ b/src/scenes/lvl03_intro.ts @@ -0,0 +1,139 @@ +import Phaser from "phaser"; + +export default class LevelThreeIntro extends Phaser.Scene { + private content: string[]; // text to display + private charDelay: number; // delay between characters + private lineDelay: number; // delay between lines + private startX: number; // start X position of the text + private startY: number; // start Y position of the text + private currentLine: Phaser.GameObjects.Text; // Text object to display the current line + private lineIndex: number; // index of the current line + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private lvl5: boolean; + private username: string; + + constructor() { + super({ key: "LevelThreeIntro" }); + } + init(data: { + username: string; + lvl1: boolean; + lvl2: boolean; + lvl3: true; + lvl4: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + } + + preload() { + this.load.image("alfredicon", "assets/alfredicon.png"); + } + + create() { + this.resetScene(); + + this.add.rectangle(640, 360, 1280, 720, 0x000); + this.add.image(150, 100, "alfredicon").setDisplaySize(130, 130); + + //display text + this.displayNextLine(); + + // On enter, transition to Level 3 + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("LevelThreeIntro2", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + }); + } + + resetScene() { + // helper to reset intial values on load + this.charDelay = 30; + this.lineDelay = 120; + this.startX = 250; + this.startY = 90; + this.lineIndex = 0; + this.content = [ + "Great job handling those generators, Agent " + this.username + ".", + "We've breached Yortsed Corp's perimeter, but now we must", + "delve deeper into the facility itself.", + " ", + "Your objective is to locate numerical codes", + "that Namuh has spread in files across various directories.", + " ", + "Once you've gathered them, you'll input these numbers", + "into the pin-pad to gain access to the inner sanctum.", + " ", + "Here are the commands at your disposal:", + " ", + " - 'ls' to list the contents of the current directory.", + " ", + " - 'cd ' to change the current directory.", + " Use 'cd ..' to go back.", + " ", + " - 'cat ' to display the contents of a file.", + " ", + "You can always run 'man alfred' for additional", + "assistance to reach the end of a mission.", + " ", + "Type in the correct code to access Yortsed Corp's facility.", + "Time is of the essence, Agent. Good luck.", + " ", + " ", + " [Enter] to Continue", + ]; + } + + // helper to display text line by line, calling typeText to animate + displayNextLine() { + if (this.lineIndex < this.content.length) { + const line = this.content[this.lineIndex++]; + // Create a new text object for the current line + this.currentLine = this.add.text( + this.startX, + this.startY + 22 * (this.lineIndex - 1), + "", + { + fontSize: "24px", + color: "#fff", + } + ); + // Start typing the line + this.typeText(line); + } + } + + // helper to animate text typing + typeText(line: string) { + // split the line into characters + const characters = line.split(""); + let i = 0; + // add a delayed event for each character + this.time.addEvent({ + delay: this.charDelay, + repeat: characters.length - 1, + callback: () => { + this.currentLine.text += characters[i++]; + if (i === characters.length) { + // once all characters are added, add a delayed event to display the next line + this.time.delayedCall( + this.lineDelay, + this.displayNextLine, + [], + this + ); + } + }, + callbackScope: this, + }); + } +} diff --git a/src/scenes/mainScene.ts b/src/scenes/mainScene.ts deleted file mode 100644 index 1c6b6089..00000000 --- a/src/scenes/mainScene.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Phaser from "phaser"; -import PhaserLogo from "../objects/phaserLogo"; -import FpsText from "../objects/fpsText"; - -export default class MainScene extends Phaser.Scene { - fpsText: FpsText; - - constructor() { - super({ key: "MainScene" }); - } - - create() { - new PhaserLogo(this, this.cameras.main.width / 2, 0); - 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); - } - - update() { - this.fpsText.update(); - } -} diff --git a/src/scenes/preloadScene.ts b/src/scenes/preloadScene.ts index c17b81ba..1ae928c9 100644 --- a/src/scenes/preloadScene.ts +++ b/src/scenes/preloadScene.ts @@ -6,10 +6,51 @@ export default class PreloadScene extends Phaser.Scene { } preload() { - this.load.image("phaser-logo", "assets/img/phaser-logo.png"); + this.load.image("ClosedBook", "assets/ClosedBook.png"); + this.load.image("HoveredBook", "assets/HoveredBook.png"); + this.load.image("OpenBook", "assets/OpenBook.png"); + this.load.image("titlescreen", "assets/CyberSpyTitleScreen.png"); + this.load.image("alfred", "assets/alfred.png"); + this.load.image("spy", "assets/spy.png"); + this.load.image("alfredicon", "assets/AlfredIcon.png"); + this.load.image("spyicon", "assets/SpyIcon.png"); + this.load.image("pin", "assets/pin.png"); + this.load.image("radar", "assets/radar.gif"); + this.load.image("shield", "assets/shield.png"); + this.load.image("ground", "assets/platform.png"); + this.load.image("prompt", "assets/PromptBox.png"); + + this.load.image("closed_metal_door", "assets/closed_metal_door.png"); + this.load.image("lockedDoor", "assets/lockedDoor.png"); + this.load.image("arrow", "assets/arrow.png"); + this.load.image("backwardsDoor", "assets/backwardsDoor.png"); + this.load.image("wallDoor", "assets/wallDoor.png"); + + this.load.image( + "LevelSelectBackground", + "assets/LevelSelectBackground.png" + ); + this.load.image("Level1Background", "assets/Level1Background.png"); + this.load.image("open_metal_door", "assets/open_metal_door.png"); + this.load.spritesheet("dude", "assets/dude.png", { + frameWidth: 240, + frameHeight: 405, + }); + this.load.spritesheet("spy", "assets/idle.png", { + frameWidth: 180, + frameHeight: 405, + }); + + this.load.audio("ding", ["assets/ding.mp3"]); + this.load.audio("cdDing", ["assets/cdDing.mp3"]); + this.load.audio("lsDing", ["assets/lsDing.mp3"]); + this.load.audio("cdBackDing", ["assets/cdBackDing.mp3"]); + this.load.audio("manDing", ["assets/manDing.mp3"]); + this.load.audio("menuMusic", ["assets/menuMusic.mp3"]); + this.load.audio("alfredDeathMusic", ["assets/alfredDeathMusic.mp3"]); } create() { - this.scene.start("MainScene"); + this.scene.start("TitleScene"); } } diff --git a/src/scenes/securityBreach.ts b/src/scenes/securityBreach.ts new file mode 100644 index 00000000..22b3eba9 --- /dev/null +++ b/src/scenes/securityBreach.ts @@ -0,0 +1,76 @@ +import Phaser from "phaser"; + +export default class SecurityBreachScene extends Phaser.Scene { + private text: Phaser.GameObjects.Text; + private backgroundColor: string; + private textColor: string; + private timer: Phaser.Time.TimerEvent; + private graphics: Phaser.GameObjects.Graphics; + + constructor() { + super({ key: "SecurityBreachScene" }); + } + + create() { + // Set initial colors + this.backgroundColor = "#FF0000"; // Start with red background + this.textColor = "#000000"; // black text + this.cameras.main.setBackgroundColor(this.backgroundColor); // Set initial background color + + // Create text + this.text = this.add + .text( + this.cameras.main.centerX, + this.cameras.main.centerY, + "SECURITY BREACH DETECTED", + { + font: "bold 70px arial", + color: this.textColor, + } + ) + .setOrigin(0.5); + + // Create an unfilled rectangle as a border around the text + this.graphics = this.add.graphics(); + this.updateGraphics(this.backgroundColor); + + // Timer for color change + this.timer = this.time.addEvent({ + delay: 500, // changes color every third of a second + callback: this.toggleColors, + callbackScope: this, + loop: true, + }); + + // Scene transition after 3 seconds + this.time.delayedCall(3000, () => { + this.scene.start("LevelSelect"); + }); + } + + toggleColors() { + this.backgroundColor = + this.backgroundColor === "#FF0000" ? "#000000" : "#FF0000"; + this.textColor = this.textColor === "#FF0000" ? "#000000" : "#FF0000"; + this.cameras.main.setBackgroundColor(this.backgroundColor); + this.text.setColor(this.textColor); + this.updateGraphics(this.backgroundColor); + } + + updateGraphics(backgroundColor: string) { + // Clear previous graphics first + this.graphics.clear(); + + // Update border based on background color for better visibility + const borderColor = backgroundColor === "#000000" ? 0xff0000 : 0x000000; + this.graphics.lineStyle(4, borderColor, 1); + + const bounds = this.text.getBounds(); + this.graphics.strokeRect( + bounds.x - 10, + bounds.y - 10, + bounds.width + 20, + bounds.height + 20 + ); + } +} diff --git a/src/scenes/terminalScene.ts b/src/scenes/terminalScene.ts new file mode 100644 index 00000000..6699570d --- /dev/null +++ b/src/scenes/terminalScene.ts @@ -0,0 +1,166 @@ +import Phaser from "phaser"; + +export default class TextInputScene extends Phaser.Scene { + private stateText: Phaser.GameObjects.Text; + + private inputField: HTMLInputElement; + + private inputContainer: Phaser.GameObjects.Container; + + constructor() { + super({ key: "TerminalScene" }); + } + + preload() {} + + create() { + // Add a background + this.add.rectangle(640, 360, 1280, 720, 0x333); + + this.add.image(100, 700, "spy"); + this.add.image(1150, 100, "alfredicon").setDisplaySize(130, 130); + this.add.image(75, 100, "pin").setDisplaySize(30, 40); + + this.inputContainer = this.add.container(360, 520); + + // Create a mask for the container + const maskGraphics = this.make.graphics(); + maskGraphics.fillRect(300, 125, 1080, 500); + const mask = new Phaser.Display.Masks.GeometryMask(this, maskGraphics); + + this.inputContainer.setMask(mask); + + this.addTextToContainer("Alfred: Welcome back agent09!"); + + let state: string = "home"; + + const lsMap = new Map(); + const cdMap = new Map(); + const cdBack = new Map(); + const manMap = new Map(); + + lsMap.set("home", "dog cat backpack"); + lsMap.set("backpack", "camera wrench zapgun"); + lsMap.set("dog", "dogToy"); + lsMap.set("cat", "catToy"); + + cdMap.set("home", ["dog", "cat", "backpack"]); + + cdBack.set("dog", "home"); + cdBack.set("cat", "home"); + cdBack.set("backpack", "home"); + + manMap.set("alfred", "Alfred: How can I be of service agent09?"); + + // Add text input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "80%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; // Change background color to white + this.inputField.style.color = "#fff"; // Change text color to black + + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + + this.input.keyboard?.on("keydown", (event: KeyboardEvent) => { + if (event.key === "Enter") { + const newText = this.inputField.value; + if (newText.trim() !== "") { + if (newText.trim() == "ls") { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer(lsMap.get(state) as string); + } else if (newText.substring(0, 3) == "cd ") { + let cdInput: string = newText.substring(3); + // CD .. FUNCTIONALITY BELOW + const backState = cdBack.get(state); + const cdState = cdMap.get(state); + if (backState !== undefined && cdInput == "..") { + state = backState; + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + } + // CD FUNCTIONALITY BELOW + else if ( + cdState !== undefined && + cdMap.get(state)?.includes(cdInput) + ) { + state = newText.substring(3); + this.stateText.setText(state); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + } + // CD DIRECTORY NOT FOUND BELOW + else { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer("Directory not found"); + } + } else if (newText.substring(0, 4) == "man ") { + let manInput: string = newText.substring(4); + + const manState = manMap.get(manInput); + if (manState !== undefined) { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer( + manMap.get(manInput) as string + ); + } else { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + this.addTextToContainer( + "Command '" + manInput + "' not found" + ); + } + } + // NONSENSE INPUT BELOW + else { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer("agent09: " + newText); + } + } + } + }); + + this.stateText = this.add.text(95, 90, state, { + fontSize: "27px", + color: "#fff", + }); + } + + update() {} + + addTextToContainer(text: string) { + this.inputContainer.y -= 32.9; + // Create a text object for the provided string + const newText = this.add.text(0, 0, text, { + fontSize: "32px", + color: "#fff", + }); + + // Add the new text object to the container + this.inputContainer.add(newText); + + // Reposition text objects vertically within the container + this.repositionTextObjects(); + } + + repositionTextObjects() { + let yPos = 0; + + // Loop through all text objects in the container and position them vertically + this.inputContainer.iterate((child: Phaser.GameObjects.GameObject) => { + if (child instanceof Phaser.GameObjects.Text) { + child.y = yPos; + yPos += child.height; + } + }); + } +} diff --git a/src/scenes/titleScene.ts b/src/scenes/titleScene.ts new file mode 100644 index 00000000..ae01db2f --- /dev/null +++ b/src/scenes/titleScene.ts @@ -0,0 +1,52 @@ +import Phaser from "phaser"; + +export default class TitleScene extends Phaser.Scene { + clickCountText: Phaser.GameObjects.Text; + clickButton: Phaser.GameObjects.Text; + constructor() { + super({ key: "TitleScene" }); + } + + init() {} + + preload() {} + + create() { + let menuMusic = this.sound.add("menuMusic", { loop: true }); + menuMusic.play(); + // menuMusic.setSeek(10); + this.add.image(640, 360, "titlescreen"); + + this.clickButton = this.add + .text(515, 440, "[Enter] to Start", { + color: "#fff", + fontSize: "25px", + fontFamily: "Monospace", + }) + .setInteractive() + .on("pointerdown", () => { + this.scene.start("LoginScene"); + }) + .on("pointerover", () => { + this.enterButtonHoverState(); + }) + .on("pointerout", () => { + this.enterButtonRestState(); + }); + + this.input.keyboard?.once("keydown-ENTER", () => { + this.scene.start("LoginScene"); + // this.scene.start("LoginScene"); + }); + } + + enterButtonHoverState() { + this.clickButton.setStyle({ fill: "#ff0" }); + } + + enterButtonRestState() { + this.clickButton.setStyle({ fill: "#fff" }); + } + + update() {} +} diff --git a/src/scenes/tutorial.ts b/src/scenes/tutorial.ts new file mode 100644 index 00000000..c36274bc --- /dev/null +++ b/src/scenes/tutorial.ts @@ -0,0 +1,553 @@ +import Phaser from "phaser"; + +export default class Tutorial extends Phaser.Scene { + private stateText: Phaser.GameObjects.Text; + private inputField: HTMLInputElement; + private inputContainer: Phaser.GameObjects.Container; + private timer: Phaser.GameObjects.Text; + private lvl2: boolean; + private lvl3: boolean; + private lvl4: boolean; + private username: string; + private lvl5: boolean; + private firstLsObjective: boolean = false; + private secondLsObjective: boolean = false; + private cdObjective: boolean = false; + private cdBackObjective: boolean = false; + private manObjective: boolean = false; + private rmObjective: boolean = false; + private lastText: string[] = [""]; + private lastPosition: number = -1; + + constructor() { + super({ key: "Tutorial" }); + } + resetScene() { + this.firstLsObjective = false; + this.secondLsObjective = false; + this.cdObjective = false; + this.cdBackObjective = false; + this.manObjective = false; + this.rmObjective = false; + this.lastText = [""]; + } + init(data: { + username: string; + + lvl1: boolean; + + lvl2: boolean; + + lvl3: boolean; + + lvl4: boolean; + + lvl5: boolean; + }) { + this.lvl2 = data.lvl2; + this.lvl3 = data.lvl3; + this.lvl4 = data.lvl4; + this.username = data.username; + this.lvl5 = data.lvl5; + } + preload() {} + + create() { + this.resetScene(); + this.add.rectangle(640, 360, 1280, 720, 0x000); + + // this.add.image(640, 100, "prompt").setDisplaySize(560, 110); + this.add.image(155, 100, "alfredicon").setDisplaySize(130, 130); + this.add.image(1050, 100, "pin").setDisplaySize(30, 40); + + let ding = this.sound.add("ding", { loop: false }); + let lsDing = this.sound.add("lsDing", { loop: false }); + let cdDing = this.sound.add("cdDing", { loop: false }); + let cdBackDing = this.sound.add("cdBackDing", { loop: false }); + let manDing = this.sound.add("manDing", { loop: false }); + + this.inputContainer = this.add.container(360, 520); + + const maskGraphics = this.make.graphics(); + maskGraphics.fillRect(300, 185, 1080, 500); + const mask = new Phaser.Display.Masks.GeometryMask(this, maskGraphics); + + this.inputContainer.setMask(mask); + + this.addTextToContainer( + "Alfred: Welcome back " + + this.username + + ".\n\nIt has been quite a while so let's make sure\nyou are familiar with all the basic commands.\n\nType 'ls' to display the surroundings of \nyour current directory.\n" + ); + + let state: string = "home"; + + const lsMap = new Map(); + const cdMap = new Map(); + const cdBack = new Map(); + const manMap = new Map(); + const rmMap = new Map(); // Map to track removable files + + lsMap.set("home", "dir_headquarters"); + lsMap.set("headquarters", "file_door_lock"); + + cdMap.set("home", ["headquarters"]); + + cdBack.set("headquarters", "home"); + + rmMap.set("headquarters", ["door_lock"]); + + manMap.set( + "ls", + "\nAlfred: The 'ls' command is useful\nfor viewing your surroundings." + ); + manMap.set("rm", "Alfred: The 'rm' command\nneutralizes enemy files."); + manMap.set( + "cd", + "\nAlfred: The 'cd' command\npermits you to navigate through rooms and items." + ); + manMap.set( + "alfred", + "\nAlfred: Try using the 'cd' command to traverse through\ndifferent areas. Then use 'rm' to remove critical files." + ); + + // Add text input field + this.inputField = document.createElement("input"); + this.inputField.type = "text"; + this.inputField.style.position = "absolute"; + this.inputField.style.width = "600px"; + this.inputField.style.height = "40px"; + this.inputField.style.fontSize = "20px"; + this.inputField.style.top = "80%"; + this.inputField.style.left = "50%"; + this.inputField.style.backgroundColor = "#000"; // Change background color to white + this.inputField.style.color = "#fff"; // Change text color to black + this.inputField.placeholder = ">$"; // Placeholder text + this.inputField.style.border = "2px solid gold"; + + this.inputField.style.transform = "translate(-50%, -50%)"; + document.body.appendChild(this.inputField); + this.add.text(500, 59, "Tutorial", { + color: "#fff", + fontSize: "47px", + fontFamily: "Monospace", + }); + + this.input.keyboard?.removeCapture( + Phaser.Input.Keyboard.KeyCodes.SPACE + ); + + this.input.keyboard?.on("keydown", (event: KeyboardEvent) => { + if (event.key === "Enter") { + this.lastPosition = -1; + const newText = this.inputField.value; + this.lastText.push(newText.trim()); + if (newText.trim() !== "") { + if (newText.trim() == "ls") { + lsDing.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addLsToContainer(lsMap.get(state) as string); + + if ( + !this.cdObjective && + this.firstLsObjective && + !this.secondLsObjective + ) { + this.addTextToContainer( + "\nAlfred: Try the 'cd headquarters' command first.\n" + ); + } else if ( + this.firstLsObjective && + !this.secondLsObjective + ) { + this.secondLsObjective = true; + + this.time.delayedCall(1500, () => { + this.addTextToContainer( + "\nAlfred: There is a door_lock.\n\nTry removing it with 'rm door_lock'.\n" + ); + }); + } else if ( + !this.firstLsObjective && + !this.secondLsObjective + ) { + this.time.delayedCall(1500, () => { + this.firstLsObjective = true; + + this.addTextToContainer( + "\nAlfred: Good job, now you can see that Namuh's \nheadquarters is nearby.\n\nUse the 'cd headquarters' command to enter the directory.\n" + ); + }); + } + } else if (newText.substring(0, 3) == "cd ") { + let cdInput: string = newText.substring( + 3, + newText.length + ); + // CD .. FUNCTIONALITY BELOW + const backState = cdBack.get(state); + const cdState = cdMap.get(state); + if (backState !== undefined && cdInput == "..") { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + if (!this.secondLsObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'ls' command again.\n" + ); + } else if (!this.rmObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'rm door_lock' command before leaving.\n" + ); + } else if (!this.cdBackObjective) { + cdBackDing.play(); + + this.cdBackObjective = true; + + state = backState; + this.stateText.setText(state); + + this.time.delayedCall(1500, () => { + this.addTextToContainer( + "\nAlfred: Great. Remember to use 'man' if you need assistance.\nTry it now with 'man ls'.\n" + ); + }); + } + } + // CD FUNCTIONALITY BELOW + else if ( + cdState !== undefined && + cdMap.get(state)?.includes(cdInput) + ) { + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + + if (!this.firstLsObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'ls' command first.\n" + ); + } else if (!this.cdObjective) { + cdDing.play(); + + state = newText.substring(3); + this.stateText.setText(state); + this.cdObjective = true; + this.time.delayedCall(1500, () => { + this.addTextToContainer( + "\nAlfred: Great work. Your location has updated\nin the top right.\n\nNow view what's in the headquarters with the 'ls' command.\n" + ); + }); + } + } + // CD DIRECTORY NOT FOUND BELOW + else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer("Directory not found"); + } + // MAN INPUT BELOW + } else if (newText.substring(0, 4) == "man ") { + let manInput: string = newText.substring(4); + this.inputField.value = ""; // Empty the input field + + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + + const manState = manMap.get(manInput); + if (manState !== undefined) { + if (!this.firstLsObjective) { + ding.play(); + + this.addTextToContainer( + "\nAlfred: Try the 'ls' command first.\n" + ); + } else if (!this.cdObjective) { + ding.play(); + + this.addTextToContainer( + "\nAlfred: Try the 'cd headquarters' command first.\n" + ); + } else if (!this.secondLsObjective) { + ding.play(); + + this.addTextToContainer( + "\nAlfred: Try the 'ls' command again.\n" + ); + } else if (!this.rmObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'rm door_lock' command first.\n" + ); + } else if (!this.cdBackObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'cd ..' command first.\n" + ); + } else if (manInput != "ls" && !this.manObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'man ls' command first.\n" + ); + } else if (!this.manObjective) { + this.manObjective = true; + + manDing.play(); + + this.addTextToContainer( + manMap.get(manInput) as string + ); + + this.time.delayedCall(2000, () => { + this.addTextToContainer( + "\nAlfred: It seems you are ready to take on the mission.\n\nRemember that typing 'man alfred' will call me in for help.\n" + ); + }); + } else { + manDing.play(); + } + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Command '" + manInput + "' not found" + ); + } + } else if (newText.substring(0, 3) == "rm ") { + let rmInput: string = newText.substring(3); + if (rmMap.get(state)?.includes(rmInput)) { + let files = lsMap.get(state) || ""; + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + + if (!this.secondLsObjective) { + ding.play(); + this.addTextToContainer( + "\nAlfred: Try the 'ls' command again.\n" + ); + } else if (!this.rmObjective) { + this.rmObjective = true; + + files = files + .replace(rmInput, "") + .trim() + .replace(/\s{2,}/g, " "); // Remove the file and extra spaces + lsMap.set(state, files); + + this.addTextToContainer( + "File '" + + rmInput + + "' removed successfully." + ); + + this.time.delayedCall(1500, () => { + this.addTextToContainer( + "\nAlfred: Perfect. You've removed the lock on the door.\n\nTry leaving the area with 'cd ..'.\n" + ); + }); + } + } else { + ding.play(); + + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username + .toLowerCase() + .replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "File '" + + rmInput + + "' cannot be found or removed." + ); + } + } + // NONSENSE INPUT BELOW + else { + ding.play(); + this.inputField.value = ""; // Empty the input field + this.addTextToContainer( + this.username.toLowerCase().replace(/\s+/g, "_") + + ": " + + newText + ); + this.addTextToContainer( + "Command '" + newText + "' not found" + ); + } + } + } + + if (event.key === "ArrowUp") { + let index = this.lastText.length + this.lastPosition; + if (index > 0) { + this.inputField.value = this.lastText[index]; + this.lastPosition -= 1; + } + } + if (event.key === "ArrowDown") { + let index = this.lastText.length + this.lastPosition; + if (index < this.lastText.length - 2) { + this.inputField.value = this.lastText[index + 2]; + this.lastPosition += 1; + } + } + if ( + this.firstLsObjective && + this.secondLsObjective && + this.cdBackObjective && + this.rmObjective && + this.cdObjective && + this.manObjective + ) { + this.time.delayedCall(6000, () => { + this.addTextToContainer( + "Objective complete: Passed basic training. \nGood work, " + + this.username + + "!" + ); + this.time.delayedCall(2000, this.loadLevel, [], this); + }); + } + }); + + this.stateText = this.add.text(1075, 95, state, { + fontSize: "24px", + color: "#fff", + }); + this.events.on("shutdown", this.removeInputField, this); + } + removeInputField() { + if (this.inputField.parentElement) { + this.inputField.parentElement.removeChild(this.inputField); + } + } + update() {} + + addLsToContainer(text: string) { + const words = text.split(" "); + + const numNewlines = words.length; + + this.inputContainer.y -= numNewlines * 24.7; + + for (let word of words) { + if (word.substring(0, 5) === "file_") { + let newWord = word.substring(5); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#77C3EC", + }); + this.inputContainer.add(newText); + } else if (word.substring(0, 4) === "dir_") { + let newWord = word.substring(4); + const newText = this.add.text(0, 0, newWord, { + fontSize: "24px", + color: "#86DC3D", + }); + this.inputContainer.add(newText); + } else { + const newText = this.add.text(0, 0, word, { + fontSize: "24px", + color: "#fff", + }); + this.inputContainer.add(newText); + } + + this.repositionTextObjects(); + } + } + + addTextToContainer(text: string) { + const newText = this.add.text(0, 0, text, { + fontSize: "24px", + color: "#fff", + }); + + const numNewlines = (text.match(/\n/g) || []).length + 1; + + // Adjust y position based on the number of newline characters + this.inputContainer.y -= numNewlines * 24.7; + + if (text.includes("Alfred: ")) { + newText.setColor("gold"); + } + if (text.includes("Objective complete: ")) { + newText.setColor("lime"); + } + + // Add the new text object to the container + this.inputContainer.add(newText); + + // Reposition text objects vertically within the container + this.repositionTextObjects(); + } + + repositionTextObjects() { + let yPos = 0; + + // Loop through all text objects in the container and position them vertically + this.inputContainer.iterate((child: Phaser.GameObjects.GameObject) => { + if (child instanceof Phaser.GameObjects.Text) { + child.y = yPos; + yPos += child.height; + } + }); + } + + loadLevel() { + this.removeInputField(); + this.scene.start("LevelSelect", { + username: this.username, + lvl2: this.lvl2, + lvl3: this.lvl3, + lvl4: this.lvl4, + lvl5: this.lvl5, + }); + } +} diff --git a/webpack/Storyboard.jpg b/webpack/Storyboard.jpg new file mode 100644 index 00000000..439ef7a7 Binary files /dev/null and b/webpack/Storyboard.jpg differ