diff --git a/.gitignore b/.gitignore index 2873e189e1..130ba8d3d1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +data/savefile.txt +save_slot_1.dat diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..c5f3f6b9c7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/FETCH_HEAD b/FETCH_HEAD new file mode 100644 index 0000000000..e69de29bb2 diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..c34e6e3e35 --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: Rolladie + diff --git a/README.md b/README.md index f3d0bded12..e3f3e64630 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Duke project template +# Rolladie project template -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. +This is a project template for a greenfield Java project. Given below are instructions on how to use it. ## Setting up in Intellij diff --git a/build.gradle b/build.gradle index ea82051fab..157e748dfd 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("Rolladie") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("rolladie") archiveClassifier.set("") } @@ -43,4 +43,9 @@ checkstyle { run{ standardInput = System.in + enableAssertions = true } +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + diff --git a/data/savefile1.txt b/data/savefile1.txt new file mode 100644 index 0000000000..1d0f5f63bd --- /dev/null +++ b/data/savefile1.txt @@ -0,0 +1,2 @@ +1 +HDFDGH | 100 | 100 | 5 | 3 | Armor -1 | Boots -1 | Weapon -1 | 0 | 50 | 100 diff --git a/data/savefile3.txt b/data/savefile3.txt new file mode 100644 index 0000000000..02f6b97289 --- /dev/null +++ b/data/savefile3.txt @@ -0,0 +1,2 @@ +3 +HTTORKHDOFPGHKFGDPOHDGF | 62 | 100 | 5 | 3 | Armor -1 | Boots -1 | Weapon -1 | 27 | 100 | 100 diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..5d6d46c772 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -2,8 +2,8 @@ Display | Name | Github Profile | Portfolio --------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +![](https://via.placeholder.com/100.png?text=Photo) | Vincent | [Github](https://github.com/vincesum) | [Portfolio](team/vincesum.md) +![](https://via.placeholder.com/100.png?text=Photo) | Lee ying ying | [Github](https://github.com/yyingg-243) | [Portfolio](team/yyingg-243.md) +![](https://via.placeholder.com/100.png?text=Photo) | James Koh | [Github](https://github.com/James17042002) | [Portfolio](team/james17042002.md) +![](https://via.placeholder.com/100.png?text=Photo) | Tiang Soon Yong | [Github](https://github.com/TiangSoonYong) | [Portfolio](team/tiangsoonyong.md) +![](https://via.placeholder.com/100.png?text=Photo) | Irwin Teo | [Github](https://github.com/zavsky) | [Portfolio](team/zavsky.md) diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..5cdeabb4b8 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,38 +1,434 @@ -# Developer Guide +# RollaDie Developer Guide -## Acknowledgements +## Acknowledgement +This project was made possible with the help of the listed tools below: +- Junit (Provides a robust testing framework) +- Gradle (Simplifying dependency management and automating the build process) -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +## Setting Up and Getting Started -## Design & implementation +Follow these steps to set up and run the Game on your local machine. -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +### Prerequisites +Ensure you have the following installed: +- Intellij IDEA (highly recommended), or any other IDE +- Java 17 (Required to run the application) +- Gradle (Used for dependency management and building the project) +### Step 1: Fork and Clone the Repository +1. **Fork** the repository to your GitHub account. +2. **Clone** the forked repository to your local machine using: +``` +git clone https://github.com/AY2425S2-CS2113-T13-4/tp.git +``` + +### Step 2: Import the Project into IntelliJ IDEA +1. Open IntelliJ IDEA. +2. Click File > Open and select the cloned project folder. +3. When prompted, import the project as a Gradle project. +4. Wait for the dependencies to be downloaded. + + + +## Design + +## Architecture +![Architecture diagram](images/ArchitectureDiagram.png) + +The Architecture diagram given above explain the high-level design of the application. + + +### Class Structure + +``` + +--- characters + | └── abilities + | | ├── Ability.java + | | ├── AbilityType.java + | | ├── BasicAttack.java + | | ├── Crush.java + | | ├── Flee.java + | | ├── Heal.java + | | ├── PowerStrike.java + | | └── Whirlwind.java + | └── players + | ├── Aria.java + | ├── Blaze.java + | └── Player.java + +--- equipment + | ├── Armor.java + | ├── ArmorDatabase.java + | ├── Boots.java + | ├── BootsDatabase.java + | ├── DragonShield.java + | ├── EmptySlot.java + | ├── Equipment.java + | ├── EquipmentList.java + | ├── FlamingSword.java + | ├── IronChainmail.java + | ├── ThunderAxe.java + | ├── Weapon.java + | └── WeaponDatabase.java + +--- events + | ├── Battle.java + | ├── Event.java + | ├── Loot.java + | └── Shop.java + +--- functions + | ├── DiceBattleAnimation.java + | ├── Pair.java + | ├── Storage.java + | ├── TerminalClear.java + | ├── TypewriterEffect.java + | └── UI.java + +--- game + | └── Game.java + +--- Rolladie.java + +--- ui + | ├── AnsiColor.java + | ├── BattleDisplay.java + | ├── HpBar.java + | ├── LootUI.java + | ├── Narrator.java + | └── ShopUI.java + └── exceptions + └── RolladieException.java +``` + +## Component Details + +### 1. Main component +`RollaDie` class is the starting point of the program. It will call the mainMenu() method which displays a list of choices to the player, namely, starting a new game, loading from a previous save, and to exit from the game completely. + +The main menu is also displayed when the player is defeated and ends a game. This allows for convenient game continuation. + +The bulk of the app’s work is done by the following components: +- `Functions`: Encapsulates all printing methods and terminal animations. +- `Game`: Gameplay mechanics and loop logic. +- `Storage`: Manages saving and loading data to and from the hard disk. +- `Exception`: Handle Exceptions during runtime. + +### 2. Functions component + +The `UI` class manages all the console prints including the ASCII characters and model representations. When an animation is required, it directs the call to the respective packages that hold the methods, such as `DiceBattleAnimation`, `TypewriterEffect` and `HpBar`, to invoke the correct animation sequence to play for the player. + +It also encompasses the methods to read inputs from the player. + +The following are all the essential methods used for UI display within the game. + +#### BattleDisplay pkg +`showPlayerStatus(Player)` - draw Player stats at the start of each round. +#### HpBar +`animate(Player1, Player2, prevHP1, prevHP2, diceDisplay)` - animate the HP bar to show the changes in HP visually, with colours representing the HP status. +#### DiceBattleAnimation pkg +`animateBattle(int[] player1rolls, int[]player2rolls` - draw dice rolling animation for both players, side-by-side. Vary the number of dice by passing different sized integer arrays into the method. +#### TypewriterEffect pkg +`print(text)` or `print(text, withDelayAfterwards)` - prints the text character by character, with a longer pause at commas and full stops. +#### TerminalClear pkg +`clearAndWrite(contentToWriteAfterClearingScreen)` - simplifies the clear screen method and reduces stuttering or flickering observed on Windows terminals. +#### Narrator +`commentOnHealth(Player)` - Narrate the amount of Hit Points a certain player has remaining. +`commentOnMomentum(Player1, Player2, damageDealt, p2PrevHp)` - Spot the dynamics changing within the battle and announce to boost or quash Player morale. + +### 3. Game component +The game component stores the main game logic. The game runs in two loops, one external loop managing the waves of enemy encounters, and another responsible for the battle sequence. + +The waves indicate the number of enemies the player has faced from the beginning of the game. Rounds show the number of bouts that the player has made against the current enemy. + +When the player faces against an enemy, the `StartBattle()` method is called. This puts the game inside a loop until either character falls. The outcome of the battle determines if the player proceeds to the next encounter. + +`StartBattle(Player1, Player2)` - begin battle sequence for a wave + + +### 4. Storage component + +**Class Diagram** +![Class Diagram](uml_image/StorageClassDiagram.png) + +The `Storage` class handles the translation between game data and a human-editable-file (.txt) +within Rolladie game. It requires `Game` attributes, namely `Player` which has a `List`, +to both implement the `toText()` method that returns an encoded string of data +which can then be written into the text save file. + +The Storage class provides the following functionalities: + +1. Save the Game: + +- The `saveGame(saveSlot: int, wave: int, player: Player)` method writes the relevant data into the specified save file +- It converts `Game` objects into a text format using their `toText()` methods. + +2. Load the Game: + +- The `loadGame(saveSlot: int)` method reads the specified save file and reconstructs the Game state. +- It calls helper methods `parsePlayeFromText(...)` and `parseEquipmentListFromText(...)` to convert text data back into `Game` objects +- If the save file is missing or corrupted, it creates a `new Game` instance instead. + +3. Data Parsing: + +- `parsePlayeFromText(...)` deciphers player stats (health, attack, defense, etc.) from text. +- `parseEquipmentListFromText(...)` reconstructs player equipments from saved data. + + +### 5. Exception component +- Exceptions: The program throws `RolladieException` when encountering errors, and prints a custom message to the terminal with more details. + +![Class Diagram](uml_image/exceptionClassDiagram.png) + + +## Implementation +## Key Features + +### 1. Save +**Overview** +The Save feature allows user to store their game data before proceeding into a `Battle` event. + +**Implementation Details** +1. During standard running of the game, if the current event is a `Battle` event, it will prompt the user to save +2. User can then input the save file number from **1 to 3** +3. `Storage` class is then called to save the current `Game` progress +4. `int wave` is first saved to record the current wave the user is in +5. `player` is then converted into an encoded `String` of text +6. Since `player` has `equipments`, it is also converted into an encoded `String` of text +7. Lastly, the full encoded data of the player is then saved into a save file of the user choosing + +**Sequence Diagram** +![Sequence Diagram](uml_image/saveSequenceDiagram.png) + +### 2. Load +**Overview** +The Load feature provides user the option to restore their saved data within the `Rolladie` main menu + +**Implementation Details** +1. User select to load game by inputting **2** +2. User can then input the save file number from **1 to 3** +3. `Storage` class is then called to load the save file +4. `int wave` is first loaded to be used for generating events and player abilities +5. `player` data is then parsed partially +6. **3** equipments are then parsed from its databases to create player's `equipments` +7. The `player` object can be fulled created with the loaded data +8. Finally, the `game` object is created and its events generation is automatically performed + +In the scenario where the save file does not exists, `Rolladie` will simply start a new `Game` + +**Sequence Diagram** +![Sequence Diagram](uml_image/loadSequenceDiagram.png) + +### 3. Attack +**Overview** + +The Attack Feature in RollaDie is a core component of the Game's battle system, allowing the player and enemy to take turns attacking each other. The feature manages input handling, attack calculations, and battle progression. +- During the player's turn, the Game reads the player's command, determines the action, cooldown, power, and roll dice to calculate attack bonuses. +If the player chooses to attack, the attack is executed, and damage is applied to the enemy. The Game then prints the attack message and checks if the battle has ended. +- The player can choose from the numbers provided on screen to choose different attacks during his turn. +- During the enemy's turn, the enemy follows a similar process which is attacking the player, applying damage, and displaying attack messages. The Game continues alternating between player and enemy turns until either the player or the enemy is defeated. This feature ensures smooth battle flow, handles attack mechanics, and updates battle status dynamically, keeping the combat engaging and strategic. + + +**Implementation Details** + +The Attack Feature in RollaDie handles the player's and enemy's turn-based combat interactions. +1. The process begins with the player's turn, where the user enters a command, which is read and parsed by the player class. +2. The parsed command is processed by Player, which sets chosen ability based on the command parsed. +3. If the player's attack requires power, deduct the required power. If power is insufficient, return invalid attack. +4. The player’s attack chosen will start its cooldown is sent to the **Battle** class where the attack will be stored as the player's ability chosen. The enemy will then choose ability based on `chooseAIAction()` method in Player class. +5. Both player and enemy will then roll dices to determine the damage that they do based on the formula: damage = +`[(dice roll result) + (num of dice) * (weapon bonus) - (opponent armor defense)] * [(power) / (max power) * 0.5 * (ability damage multiplier)]` +5. Both the player and enemy will then carry out their move and attack each other, deducting both characters' health based on damage formula. +6. Cooldown of all abilities decreases by 1. +7. This loop repeats until the battle ends. + +**Sequence Diagram** + +The sequence diagram below illustrates the process that occurs when the player inputs an attack command. + +![Sequence Diagram](uml_image/attackSequence.png) + +### 4. Heal +**Overview** + +The heal Feature in RollaDie allows players to adopt a heal during their turn instead of attacking. +This feature enhances strategic gameplay by giving the player an option to heal and recover both +health and power which is needed for casting skills.The battle sequence continues until either +the player or the enemy is defeated. + +**Implementation Details** +The implementation is generally very similar to the attack feature above, with the only difference being the action used. + +During the player’s turn: +1. The user inputs command "2", which is the heal action. +2. The parsed command is processed by Player, which sets chosen heal ability based on the command parsed. +3. The heal action will start its cooldown and is sent to the **Battle** class where the heal action +is stored as the player's ability chosen. The enemy will also choose an ability. +4. Only the enemy rolls a die and the player will be healed for a flat amount of health. The enemy will deal damage +based on die roll while the player will heal a fixed amount. + +**Sequence Diagram** + +The sequence diagram is shared with the attack feature above. + + + +### 5. Flee +**Overview** +The flee feature in Rolladie is to allow the player to escape from nasty situations. +This feature allows players to make a strategic retreat to re gear up before fighting strong enemies. +When the player chooses to flee, the battle ends immediately and the player goes onto the next event. + +**Implementation Details** +1. During the player's turn, **PlayerTurn** class gets the current action of player. +2. If player chooses to flee, **PlayerTurn** sets hasSurrendered in **Turn** to true. +3. **BattleLogic** checks if hasSurrendered in **Turn** is true and sets hasWon to false and hasBattleEnded to true. +4. **BattleLogic** returns hasWon = false to **Battle** class to end the battle. + +**Sequence Diagram** +![Sequence Diagram](uml_image/fleeSequence.png) + + + +### 6. Loot +**Overview** +The loot feature in Rolladie allows the player to get gold after winning a battle. +This feature enables players to earn gold to upgrade himself in the shop. +If player does not win battle, he does not get gold. + +**Implementation Details** +1. After the battle, **Battle** object will send hasWon to **Game** to set the hasWonCurrBattle in Game to true +or false depending on whether the player won or fled from the battle. +2. **Game** sets the hasWon variable in **Loot** based on hasWonCurrBattle. +3. If hasWon in **loot** is true, add gold to player and print the loot that the player got. +4. If hasWon in **loot** is false, print that the player got no loot. + +**Sequence Diagram** +![Sequence Diagram](uml_image/lootSequenceDiagram.png) + + +### 7. Buy +**Overview** +The buy feature in Rolladie allows the player to equip themselves with stronger equipment by spending their gold. +This feature enables players to be strong enough to put up a fight with the stronger enemies as the wave number progresses. +The player cannot buy equipment that is too expensive. +Buying an Equipment type that the player already has will automatically remove it and replace it with the bought equipment. +Player will not gain gold when equipment is removed this way. + +**Implementation Details** +1. After every even-numbered enemy encounter, the player will be sent to **Shop**. +2. The main shop screen will display the player's gold amount, a list of **Equipment** and its details for the user to buy, each marked with an index, and a list of commands to select. +3. If the player want to buy an equipment, they will input 1 to select [1. Buy]. +4. The shop will then print instructions on how to buy the desired equipment, that is, enter the index of it. +5. If the player has enough gold, the player will equip the current equipment, and the value of the equipment will be deducted from the gold of the player. +6. Any existing equipment of the same type as the bought equipment will be automatically removed, but the player do not gain gold from such a removal. +7. The **Player** toString() method will be called to show the new stats and equipment of the player, then return to the main shop screen. +8. If the player does not have enough gold, the game will only print "not enough gold" and return to the main shop screen. +9. In the main shop screen, they can input 3 to select [3. Exit the Shop] to exit the Shop. + +**Sequence Diagram** +![Sequence Diagram](uml_image/buySequenceDiagram.png) + +### 8. Sell +**Overview** +The sell feature in Rolladie allows the player to sell old equipment to earn gold, so that they can better afford higher-end equipment in the shop. +This feature enables players to have an additional way to gain gold, as well as remove equipment they no longer want.. +The player cannot sell equipment types they do not have. + +**Implementation Details** +1. After every even-numbered enemy encounter, the player will be sent to **Shop**. +2. The main shop screen will display the player's gold amount, a list of **Equipment** and its details for the user to buy, each marked with an index, and a list of commands to select. +3. If the player want to buy an equipment, they will input 1 to select [2.Sell]. +4. The shop will then print instructions on how to sell the desired equipment and the index for the three types of equipment. +5. If the Player does not own the Equipment type selected, an error message will be printed and the player will return to the main shop screen. +6. If the Player owns the Equipment type selected, an amount of gold equal to equipment.getValue() will be earned by the Player, and the equipment will be removed. +7. The **Player** toString() method will be called to show the new stats and equipment of the player, then return to the main shop screen. +8. In the main shop screen, they can input 3 to select [3. Exit the Shop] to exit the Shop. + +**Sequence Diagram** +![Sequence Diagram](uml_image/sellSequenceDiagram.png) + + + + +## Appendix ## Product scope -### Target user profile -{Describe the target user profile} +### Target user profile: -### Value proposition +RollaDie is designed for CS2113 students who want a fun and simple way to relax, and to enjoy the easy-to-use text-based interface and clear Game rules. +The Game is also great for DnD fans who like turn-based battles, strategy, and storytelling, without the hassle of setting up a full Game. -{Describe the value proposition: what problem does it solve?} +### Value proposition: + +RollaDie is a fun and nostalgic text-based RPG that brings the adventurous spirit of Dungeon & Dragons (DnD) to life in a simple way. It brings the excitement of classic role-playing games to a simple Command Line Interface (CLI), making it lightweight and easy to play anytime, anywhere. +Instead of dealing with complicated setups, players can jump straight into the action, rolling dice, battling enemies, and making crucial choices. ## User Stories +*** -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +| Version | As a ... | I want to ... | So that I can ... | +|---------|-----------------|-------------------------------------------------------|-------------------------------------| +| v1.0 | student player | attack during the battle phase | deal damage to enemy. | +| v1.0 | student player | heal during the battle phase | reduce the damage taken. | +| v1.0 | student player | fight enemies | collect points for a final score. | +| v1.0 | student player | input simple commands ( attack, defend ) | get quickly used to the controls. | +| v1.0 | student player | see my health bar | better decide my next move. | +| v2.0 | student player | save Game progress | continue my Game. | +| v2.0 | student player | roll dice | determine the outcome of an action. | +| v2.0 | student player | fight different enemies with different battle effects | make the journey more dynamic. | +| v2.0 | student player | change my equipment | determine the outcome of an action. | +| v2.0 | student player | collect currency | upgrade my equipment. | ## Non-Functional Requirements +1. Should work on any mainstream OS as long as it has Java 17 or above installed. +2. This Game is designed as a single-player experience. +3. This Game is optimized for users with an average typing speed. -{Give non-functional requirements} -## Glossary -* *glossary item* - Definition +## Testing +### Manual testing +#### Create a new player character +Steps: +Start the program +Create a new game +Enter your chosen name +Save the progress +Exit to main menu and load the save file +Verify that the created user retains its name and stats -## Instructions for manual testing +#### Storage verification +Steps: +Fully quit the program (close it via the `exit` command or by selecting exit on the main menu) +Reopen the program and load the save file. The program should seamlessly load your previously saved progress. -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +### Structure +Tests are organized according to the package structure: + +``` ++---data +| \---StorageTest +| ValidData.txt +| +\---java ++---Game +| | GameTest.java +| | RolladieTest.java +| | RollDiceTest.java +| | +| +---Battle +| | BattleTest.java +| | +| +---Characters +| | CharacterTest.java +| | +| \---Functionalities +| UITest.java +| +\---seedu +\---duke + +``` + + +## Glossary +* *User* - A person who plays the Game. +* *Mainstream OS* - Windows, Linux, Unix, MacOS +* *Wave* - The number of enemy encounters +* *Round* - The bout number of the current battle \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..8a24807b83 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,11 @@ -# Duke +# RollaDie -{Give product intro here} +RollaDie is a Dungeon & Dragons (DnD) text-based RPG,designed for CLI play. +It offers a retro-style gaming experience where players can embark on adventures, make choices, +and roll the dice to determine their fate. With its simple interface and replayable gameplay, +RollaDie provides a fun and engaging escape for CS2113 students. -Useful links: +## Useful links: * [User Guide](UserGuide.md) * [Developer Guide](DeveloperGuide.md) * [About Us](AboutUs.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d6cf4c3b3a..035a0be5c7 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,222 @@ -# User Guide +# RollaDie User Guide + +## Table of content + +- [Introduction](#introduction) + +- [Quick Start](#quick-start) + +- [Features](#features) + +- [1. Starting a new game](#starting-a-new-game) + +- [2. Loading from a save](#loading-from-a-save) + +- [3. Making your character](#making-your-character) + +- [4. Choosing your battle ability](#choosing-your-battle-ability) + +- [5. Looting the enemy](#looting-the-enemy) + +- [6. Purchasing from the shop](#purchasing-from-the-shop) + +- [7. Saving the game](#saving-the-game) + +- [8. Exiting the game](#exiting-the-game) + +- [Command Summary](#command-summary) + +- [FAQ](#faq) ## Introduction -{Give a product intro} +RollaDie is a Dungeon & Dragons (DnD) text-based RPG, optimised to play using Command Line Interface (CLI) and has a simple text-ui display that reminisces games of the 1960s. Players face off hordes of enemies with the ultimate goal of achieving the legendary status in the annals of history. Join the story and learn the secrets that lay within! + +The target audience of this program are CS2113 students, but everyone is invited! It serves primarily as a stress reliever, and it aims to provide a dynamic and replay-able experience! + ## Quick Start -{Give steps to get started quickly} +1. Ensure that you have Java 17 installed. -1. Ensure that you have Java 17 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `Rolladie` from [here](https://github.com/AY2425S2-CS2113-T13-4/tp/releases/). -## Features +3. From the folder containing the downloaded JAR file, run the following command in your terminal to start the game: +- `java -jar ./rolladie.jar` -{Give detailed description of each feature} +## Features -### Adding a todo: `todo` -Adds a new item to the list of todo items. +> [!NOTE] +> The dice outcome listed below are randomly generated, meaning the results will vary each time. As a result, the damage calculation will depend highly on the situation and can only be used as a reference -Format: `todo n/TODO_NAME d/DEADLINE` +### Starting a new game -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. + ![](images/rolladie_mainmenu.png) -Example of usage: +Upon first launch, input `1` to start a new game. +### Loading from a save -`todo n/Write the rest of the User Guide d/next week` +To load from a previous save, input `2`. -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +![](images/rolladie_mainmenu_loadsave.png) -## FAQ +You can choose the specific save slot to load from. Select your choice and begin! +### Making your character + +If starting from a new game, the game will then ask you for your hero's name. Choose a name and type it in! +### Choosing your battle ability + +![](images/rolladie_battle.png) + +The game presents the round number (aka the bout number with the current opponent), your player stats, and the current enemy stats below. + +Let's break it down: +`p` is our hero's name and he has 100 Hit Points (HP). Our player has 50 Power (out of a max possible 100), akin to mana that you encounter in other games of similar genre. The Weapon is an equipment that boosts your damage stats, while the Armor increases your durability. Below, we see our hero has three abilities he can use, namely Basic Attack, Power Strike and Heal. + +The enemy is mostly similar in makeup, but this one has 50 Hit Points and a different Weapon and Armor. You can see how the equipment affects each player differently during combat. He has only one ability, Power Strike. Generally, the AI will intelligently choose its ability based on the situation. However, when the AI has no valid moves available, it will default to use a Basic Attack. + +Select the ability you want to use for the round. Note that each ability comes with different costs (Power cost, Cooldown cost) and you will need to strategise well to survive! + +#### Battle sequence + +![](images/rolladie_battle1.png) + +In this example, we begin with using the Basic Attack ability. + +![](images/rolladie_battle2.png) + +The game proceeds to roll both players' dice, with your hero's dice shown on the left and the opponent's shown on the right. The rolled value is critical in the calculation of the final damage dealt to each player. Here, you are looking for a high value. + +![](images/rolladie_battle3.png) + +An animation plays out to visually represent the changes to Hit Points at the present turn. The bar changes colour depending on the status of your character. + +![](Pimages/rolladie_battle4.png) + +In the next turn, you will see the changes to the player stats at the top, any ability cooldown related blockers will be shown with a turn counter. + +![](images/rolladie_battle5.png) + +The narrator will announce the effects of your attacks, as well as those of your opponents. + +![](images/rolladie_battle6.png) + +Here, the game continues the battle until one of the players is down. If the player survives into the next encounter, he/she may be awarded a new skill and be able to loot from the fallen opponent. +#### Strategizing for a win + +There are various strategies to thriving in the world of RollaDie. Careful planning of your skills to leverage on each of their strengths is crucial in maintaining the upper edge in each encounter. + +The damage calculation is as follows: +\[(dice roll result) + (num of dice) * (weapon bonus) - (opponent armor defense)] * \[(power) / (max power) * 0.5 * (ability damage multiplier)] + +To break it down further: +- The number of dice impacts your damage calculation two-fold, the total roll result, as well as the weapon bonus per die rolled. +- The available amount of power you have scales your damage output up to a total of 50%. +- The used ability has an innate damage multiplier that will drastically change the total damage output. +- Finally, the equipped armor and player's defense stat will have a fixed damage reduction bonus applied on top. +### Looting the enemy + +After every battle, there will be a loot event! +If you defeated the enemy, the loot event will grant you gold. +If you fled from the enemy, no gold for you! + +### Buying and Selling in the shop + +Every 2 waves, the player will be able to purchase Equipment from the Shop. These Equipment consists of Armor, Boots, Weapon. -**Q**: How do I transfer my data to another computer? +Upon entering the Shop, the equipment on sale will be shown with their costs. There will be three commands to choose from. This is the main shop screen. -**A**: {your answer here} +![](images/rolladie_shop_entry.png) + +To select a command, just enter the corresponding index of the command. + +Entering "1", will select [1. Buy] and buy instructions will be shown. + +![](images/rolladie_buy_instructions.png) + +Entering an index within the list of equipments on sale will attempt purchase the corresponding equipment. + +It will be successful only if the player has enough gold. + +In this example, the user enters "2" to buy "Leather Soles", and it is successfully equipped: + +![](images/rolladie_buy_success.png) + +In this example, the user enters "2" to buy "Leather Soles", but the purchase does not go through due to insufficient gold: + +![](images/rolladie_buy_fail.png) + +After a command is executed, the main shop screen will be shown again. + +Entering "2", will select [2. Sell]. Sell instructions and a list of equipment Types will be shown. + +![](images/rolladie_sell_instructions.png) + +Entering an index within the list of equipment types will attempt to sell the Equipment owned by the player with the corresponding equipment type. + +It will be successful only if the player owns such an equipment type. + +In this example, the user enters "1" to sell the "Leather Soles" previously equipped. The user earns 5 gold, which is half the price of "Leather Soles". + +![](images/rolladie_sell_success.png) + +Since the "Leather Soles" are already sold, the player is left with an empty equipment slot for boots. + +Therefore, trying to sell boots again will be a failure, as illustrated in the example below. + +![](images/rolladie_sell_fail.png) + +Entering "3" in the main shop screen will select [3. Exit the Shop] and proceed to the next event. + +![](images/rolladie_shop_exit.png) + +. + + +### Saving the game + +The game will periodically ask you to save the game at checkpoints. Type `y` when prompted to save and `n` to skip through. Specify a save slot (1-3) to save multiple progresses and return to them when necessary! + +### Exiting the game + +At any point in the game when it asks for input, you can insert the command, `exit`, to quit the game. Do note however, that any unsaved progress will be lost! + +### Game Rounds + +The total number of battle rounds is 10. The goal is to win the 10th round of the game to beat the game. ## Command Summary -{Give a 'cheat sheet' of commands here} +There are no commands to remember. Simply follow and let the game guide you along! + +| Function | Scenario | Input | +|----------------------|-------------|------------------------------------------------| +| Starting a new game | In main menu | `1` | +| Loading a previous save | In main menu | `2` | +| Creating a new character | In character screen | {your_hero_name} | +| Choosing your battle ability | In combat | Integer corresponding to your choice of ability {1..*} | +| Saving the game | At checkpoints | Input `y` and choose a slot number {1-3} | + + +## FAQ + +**Q: How do I transfer my data to another computer?** + +A: Copy the `.dat` files created in the same directory over to the other computer you wish to continue your save from. + +**Q: What happens if I enter invalid input?** + +A: The game will not accept the input and will prompt for another valid one. + +**Q: What is the difference between HP, Power and Cooldown?** + +A: Remaining HP is a measure of your character's durability against incoming damage. When HP falls to zero, your character also falls. Power acts like mana as seen frequently in other games of such genre, in that it is an ability casting requirement to prevent skill spamming, but also gives you a damage boost the more you stack it. So it helps add strategic depth. Cooldown applies to individual skills so that players are forced to choose their combat sequence tactically for the best outcome. + +**Q: What is the difficulty of the game?** + +A: The enemy will get stronger as the battles progresses, until the player reaches the final enemy. Therefore, players have to upgrade themself through the loot +and shop events that occur after battles, with loot appearing after every battle and shop appearing after every 2 battles. + +**Q: What happens if I close the application without using the `exit` command?** -* Add todo `todo n/TODO_NAME d/DEADLINE` +A: Any previously saved progress will remain available. However, if there are any progress made between the point of exit and the point of last save, then those progress will be lost. diff --git a/docs/class_structure.txt b/docs/class_structure.txt new file mode 100644 index 0000000000..5e62139e84 --- /dev/null +++ b/docs/class_structure.txt @@ -0,0 +1,84 @@ +Folder PATH listing for volume Local Disk +Volume serial number is A43C-0BC4 +C:. +| class_structure.txt +| ++---exceptions +| RolladieException.java +| ++---functionalities +| | Parser.java +| | Storage.java +| | +| \---UI +| BattleUI.java +| LootUI.java +| ShopUI.java +| UI.java +| ++---game +| | game.java +| | Rolladie.java +| | RollDice.java +| | +| +---Actions +| | | Action.java +| | | DefaultAction.java +| | | ExitAction.java +| | | HelpAction.java +| | | StartAction.java +| | | +| | +---BattleAction +| | | AttackAction.java +| | | BattleAction.java +| | | DefendAction.java +| | | FleeAction.java +| | | +| | \---ShopAction +| | BuyAction.java +| | LeaveAction.java +| | SellAction.java +| | ShopAction.java +| | +| +---Characters +| | Character.java +| | Enemy.java +| | EnemyDatabase.java +| | Player.java +| | +| +---Currency +| | Gold.java +| | +| +---Equipment +| | Armor.java +| | ArmorDatabase.java +| | Boots.java +| | BootsDatabase.java +| | Equipment.java +| | EquipmentList.java +| | Weapon.java +| | WeaponDatabase.java +| | +| +---Events +| | | Event.java +| | | EventType.java +| | | +| | +---Battle +| | | Battle.java +| | | BattleLogic.java +| | | EnemyTurn.java +| | | PlayerTurn.java +| | | Turn.java +| | | +| | +---Loot +| | | Loot.java +| | | +| | \---Shop +| | Shop.java +| | +| \---Menu +| Menu.java +| MenuSystem.java +| TerminalUtils.java +| +\---seedu diff --git a/docs/diagrams/Aarch.puml b/docs/diagrams/Aarch.puml new file mode 100644 index 0000000000..6d96bf2ffe --- /dev/null +++ b/docs/diagrams/Aarch.puml @@ -0,0 +1,45 @@ +@startuml +title RollaDie\n Architecture Diagram + +skinparam componentStyle rectangle + +actor User + +package "Main Component" { + [RollaDie] +} + +package "Functions Component" { + [UI] +} + +package "Game Component" { + [Game] + [Menu] + [Player] + [Event] +} + + +package "Storage Component" { + [Storage] +} + +package "Exception Component" { + [RolladieException] +} + + +User --> RollaDie : start program +RollaDie -> Menu : display main menu +Menu -> Game : start game loop +RollaDie --> Storage : load game +RollaDie --> UI : print messages + +Game -> Event +Game -> Player +Game --> UI : update UI +Game -> Storage : save game +Game --> RolladieException : handle errors + +@enduml diff --git a/docs/diagrams/Aclass-diag.puml b/docs/diagrams/Aclass-diag.puml new file mode 100644 index 0000000000..e4a54e9a4a --- /dev/null +++ b/docs/diagrams/Aclass-diag.puml @@ -0,0 +1,35 @@ +@startuml + +abstract class AbstractList +abstract AbstractCollection +interface List +interface Collection + +List <|-- AbstractList +Collection <|-- AbstractCollection + +Collection <|- List +AbstractCollection <|- AbstractList +AbstractList <|-- ArrayList + +class ArrayList { + Object[] elementData + size() +} + +enum TimeUnit { + DAYS + HOURS + MINUTES +} + +annotation SuppressWarnings + +annotation Annotation { + annotation with members + String foo() + String bar() +} + + +@enduml diff --git a/docs/diagrams/Agame-loop.puml b/docs/diagrams/Agame-loop.puml new file mode 100644 index 0000000000..b61aaeefd7 --- /dev/null +++ b/docs/diagrams/Agame-loop.puml @@ -0,0 +1,36 @@ +@startuml + +!include Style.puml + +hide footbox +skinparam sequenceReferenceBackgroundColor #f7807c + +participant ":Game" as game gameC +participant ":Player" as player playerC +participant ":Storage" as storage storageC +participant ":Event" as event eventC +participant ":UI" as ui uiC + +-> game : new Game() +activate game +game -> player : new Player() +activate player +deactivate player +game -> game : generateEvents() +deactivate game +-> game : run() +activate game +loop player.isAlive && currentEvent.hasNext +game -> game : get next Event +game -> event : run() +activate event +event -> storage : saveGame() +deactivate event +game -> game : increment wave +end +game -> ui : print GameOver +activate ui +deactivate ui + <- game : return + deactivate game +@enduml diff --git a/docs/diagrams/Amain.puml b/docs/diagrams/Amain.puml new file mode 100644 index 0000000000..8226c23ba1 --- /dev/null +++ b/docs/diagrams/Amain.puml @@ -0,0 +1,37 @@ +@startuml + +!include Style.puml + +hide footbox +skinparam sequenceReferenceBackgroundColor #f7807c + +actor Player + +participant ":Rolladie" as rolladie rolladieC +participant ":UI" as ui uiC +participant ":Game" as game gameC +participant ":Storage" as storage storageC + +Player -> rolladie : run +activate rolladie +rolladie -> rolladie : mainMenu() +'return +rolladie -> ui : print Welcome +activate ui +deactivate ui +loop not validUserCommand over rolladie, ui +alt startNewGame +rolladie -> game : new Game() +activate game +else loadSaveGame +rolladie -> storage : loadGame() +storage --> game : new Game(save) +else exitGame +rolladie -> ui : print Exit +activate ui +deactivate ui +rolladie -> Player +deactivate rolladie +end + +@enduml diff --git a/docs/diagrams/StorageClass.puml b/docs/diagrams/StorageClass.puml new file mode 100644 index 0000000000..629d01c1ed --- /dev/null +++ b/docs/diagrams/StorageClass.puml @@ -0,0 +1,59 @@ +@startuml +hide circle +skinparam classAttributeIconSize 0 + +package functions { + class Storage { + -{static} final FILE_DIRECTORY: String + -{static} final FILE_NAME: String + -{static} final FILE_TYPE: String + -{static} final LOAD_DELIMITER: String + +{static} final SAVE_DELIMITER: String + + - final fileDirectory: String + - final fileName: String + - final fileType: String + + +saveGame(saveSlot: int, wave: int, player: Player): + +loadGame(saveSlot: int): Game + -parsePlayerFromText(wave: int, playerData: String[]): Player + -parseEquipmentListFromText(equipmentsData: String[]): List + } +} +package players { + class Player { + +toText():String + } +} +package equipments { + class "{abstract} \n Equipment" { + +toText():String { abstract } + } + class Weapon { + +toText():String + } + class Boots { + +toText():String + } + class Armor { + +toText():String + } +} + +package game { + class Game { + wave: int + -saveGame(): void + } +} + +Game ->"1" Player +Player ->"3" "{abstract} \n Equipment" : Has list of > + +Weapon --|> "{abstract} \n Equipment" +Boots --|> "{abstract} \n Equipment" +Armor --|> "{abstract} \n Equipment" + +Storage --> Player + +@enduml \ No newline at end of file diff --git a/docs/diagrams/Style.puml b/docs/diagrams/Style.puml new file mode 100644 index 0000000000..709346c23a --- /dev/null +++ b/docs/diagrams/Style.puml @@ -0,0 +1,9 @@ +!define LOGIC_COLOR #3333C4 +!define storageC #7777DB +!define rolladieC #5252CE +!define LOGIC_COLOR_T3 #1616B0 +!define playerC #13bac9 + +!define uiC #EE82EE +!define gameC #90EE90 +!define eventC #e2e841 \ No newline at end of file diff --git a/docs/diagrams/architecture.puml b/docs/diagrams/architecture.puml new file mode 100644 index 0000000000..70aba5fded --- /dev/null +++ b/docs/diagrams/architecture.puml @@ -0,0 +1,45 @@ +@startuml +title RollaDie\n Architecture Diagram + +skinparam componentStyle rectangle + +actor User + +package "Main Component" { + [RollaDie] +} + +package "Functionalities Component" { + [UI] + [Parser] +} + +package "Game Component" { + [Game] + [Menu] + +} + + +package "Storage Component" { + [Storage] +} + +package "Exception Component" { + [RolladieException] +} + + +User --> RollaDie : start game +RollaDie --> Menu : display main menu +RollaDie --> Parser : read user input +RollaDie --> Game : start game loop +RollaDie --> Storage : load game +RollaDie --> UI : print messages + +Game --> Parser : process commands +Game --> UI : update UI +Game --> Storage : save game +Game --> RolladieException : handle errors + +@enduml diff --git a/docs/diagrams/attackSequence.puml b/docs/diagrams/attackSequence.puml new file mode 100644 index 0000000000..d744f9ebfb --- /dev/null +++ b/docs/diagrams/attackSequence.puml @@ -0,0 +1,25 @@ +@startuml + +actor Player +participant Game +participant Battle +participant UI +actor Enemy +activate Player +activate Game +activate Battle +Game -> Battle : run() +loop while Player.isAlive() && Enemy.isAlive() + Battle -> Player : chooseAbility() + Player -> Player : showUserMenu() + Player -> Battle : p1Ability + + Battle -> Enemy : chooseAbility() + Enemy -> Enemy : chooseAIAction() + Enemy -> Battle : p2Ability + Battle -> Player : applyDamage() + Battle -> Enemy : applyDamage() +end +@enduml + + diff --git a/docs/diagrams/battle-sequence.puml b/docs/diagrams/battle-sequence.puml new file mode 100644 index 0000000000..8bd616abf7 --- /dev/null +++ b/docs/diagrams/battle-sequence.puml @@ -0,0 +1,40 @@ +@startuml + +!include Style.puml + +hide footbox +skinparam sequenceReferenceBackgroundColor #f7807c + +participant ":Battle" as battle gameC +participant ":UI" as ui uiC +participant ":Player" as player playerC +participant ":Dice" as dice storageC +participant ":HP" as hp eventC + + +-> battle : run() +activate battle +battle -> battle : new enemy Player +battle -> battle : start battle loop +loop players.isAlive +battle -> ui : show Player status +battle -> player : choose player ability +battle -> player : choose AI ability +battle -> player : roll dice +battle -> ui : animate dice +ui -> dice : run() +battle -> player : compute and apply damage +battle -> ui : narrator commentary +battle -> ui : animate hp +ui -> hp : run() +end + +alt player.isAlive +battle -> player : prepare next wave +<- battle : return +else player.isNotAlive +battle -> print : GameOver +<- battle : return +end + +@enduml diff --git a/docs/diagrams/buySequence.puml b/docs/diagrams/buySequence.puml new file mode 100644 index 0000000000..0d11fb1f3b --- /dev/null +++ b/docs/diagrams/buySequence.puml @@ -0,0 +1,36 @@ +@startuml +actor Player as UI +participant Shop +participant ShopUI +participant Player +participant Equipment +participant Narrator +participant UI as FunctionsUI + +UI -> Shop : handleShopInput(1) +activate Shop + +Shop -> ShopUI : printBuyInstructions() +Shop -> UI : readIntegerInput() +activate UI +UI --> Shop : buyInput (int) +deactivate UI + +Shop -> Shop : handleBuyInput(buyInput) +activate Shop + +Shop -> Equipment : (select from equipments[buyInput - 1]) +Shop --> Equipment : equipment + +Shop -> Player : buyEquipment(equipment) +Player --> Shop : hasBought (boolean) + +alt hasBought == true + Shop -> Narrator : commentOnShopBuy(player, equipment) +else not enough gold + Shop -> UI : printErrorMessage("Not enough gold!") +end + +deactivate Shop + +@enduml diff --git a/docs/diagrams/exceptionClass.puml b/docs/diagrams/exceptionClass.puml new file mode 100644 index 0000000000..f295089abb --- /dev/null +++ b/docs/diagrams/exceptionClass.puml @@ -0,0 +1,16 @@ +@startuml +hide circle + + +class Exception { +} + +class RolladieException { + +RolladieException(String message) +} + +RolladieException --|> Exception +@enduml + + +@enduml \ No newline at end of file diff --git a/docs/diagrams/flee.puml b/docs/diagrams/flee.puml new file mode 100644 index 0000000000..13785aba0a --- /dev/null +++ b/docs/diagrams/flee.puml @@ -0,0 +1,19 @@ +@startuml +participant Battle order 4 +participant BattleLogic order 5 +participant Playerturn order 6 +participant Turn order 7 +activate Battle +Battle -> BattleLogic : startBattle() +activate BattleLogic +loop +Battle -> BattleLogic : battleSequence +Playerturn -> Playerturn : getCurrAction(input String) +Playerturn -> Turn : setHasSurrenderred(boolean) +BattleLogic -> Turn : checkBattleEnd(turn) +Turn -> BattleLogic +end +BattleLogic -> BattleLogic : setHasWon(false) +BattleLogic -> BattleLogic : setHasBattleEnded(true) +BattleLogic -> Battle : setHasWon(boolean) +@enduml diff --git a/docs/diagrams/loadSequence.puml b/docs/diagrams/loadSequence.puml new file mode 100644 index 0000000000..07d4cc9685 --- /dev/null +++ b/docs/diagrams/loadSequence.puml @@ -0,0 +1,49 @@ +@startuml +hide footbox +skinparam sequenceReferenceBackgroundColor #FFFFFF +actor User + +participant ":Rolladie" as Rolladie +participant ":Storage" as Storage +participant "{abstract} \n Equipment" as Equipment +participant ":Game" as Game +participant ":Player" as Player + +User -> Rolladie: Load Game +Rolladie -> Storage: new Storage() +activate Storage +Storage --> Rolladie: +deactivate Storage +Rolladie -> Storage: .loadGame(...) +activate Storage +ref over Storage + Get int wave data +end ref +Storage -> Storage: Parse Player Data +activate Storage + Storage -> Storage: Parse Player Equipments + activate Storage + loop 3 times + ref over Storage, Equipment + Get equipment from subclasses database + and add to list of equipments + end ref + end loop + Storage --> Storage: equipments: List + deactivate Storage + Storage -> Player: new Player(equipments, ...) + activate Player + Player --> Storage: :Player + deactivate Player + Storage --> Storage: +deactivate Storage + +Storage -> Game: new Game(player: Player, wave: int) +activate Game +Game --> Storage: :Game +deactivate Game +Storage --> Rolladie: +destroy Storage +Rolladie -> Game: game.run() +activate Game +@enduml \ No newline at end of file diff --git a/docs/diagrams/lootSequence.puml b/docs/diagrams/lootSequence.puml new file mode 100644 index 0000000000..3c7d245e00 --- /dev/null +++ b/docs/diagrams/lootSequence.puml @@ -0,0 +1,26 @@ +@startuml + +actor Player +participant Game +participant Battle +participant Loot +participant LootUI +activate Player +activate Game +activate Battle +activate Loot +activate LootUI +Game -> Battle : run() +Battle -> Battle : startGameLoop() +Battle -> Game : hasWonCurrBattle() +deactivate Battle +Game -> Loot : setHasWon(boolean) +Game -> Loot : run() +alt Player wins the battle + Loot -> Player : addGold() + Loot -> LootUI : printLoot(loot) +else Player loses + Loot -> LootUI : printNoLoot() +end +deactivate Loot +@enduml diff --git a/docs/diagrams/rollDiceSequence.puml b/docs/diagrams/rollDiceSequence.puml new file mode 100644 index 0000000000..5eb5633fdd --- /dev/null +++ b/docs/diagrams/rollDiceSequence.puml @@ -0,0 +1,36 @@ +@startuml +'https://plantuml.com/sequence-diagram + +actor Player +participant "RollDice" as RD +participant "Random Generator" as RNG +participant "UI" + +Player -> RD : rollDice() +activate RD +RD -> RNG : generate first_dice +RNG --> RD : return random value +RD -> RNG : generate second_dice +RNG --> RD : return random value +RD -> UI : printDiceImage(first_dice) +RD -> UI : printDiceImage(second_dice) +RD --> Player : return first_dice + second_dice +deactivate RD + +Player -> RD : diceOutcome(diceValue) +activate RD +alt Invalid Dice Value (<2 or >12) + RD -> Player : throw RolladieException +else 2 ≤ diceValue < 5 + RD -> UI : print "Oops, 0 bonus points" + RD --> Player : return MISS +else 5 ≤ diceValue < 10 + RD -> UI : print "10 bonus points!" + RD --> Player : return HIT +else 10 ≤ diceValue ≤ 12 + RD -> UI : print "20 bonus points!" + RD --> Player : return CRUCIAL_HIT +end +deactivate RD + +@enduml \ No newline at end of file diff --git a/docs/diagrams/saveSequence.puml b/docs/diagrams/saveSequence.puml new file mode 100644 index 0000000000..2fbac7ccdc --- /dev/null +++ b/docs/diagrams/saveSequence.puml @@ -0,0 +1,68 @@ +@startuml +hide footbox +skinparam sequenceReferenceBackgroundColor #FFFFFF +actor User + +participant ":Game" as Game +participant ":Storage" as Storage +participant ":Player" as Player +participant "{abstract} \n Equipment" as Equipment + +loop until game is over + User->Game : game.run() + activate Game + opt is Battle event + Game->Game:saveGame() + activate Game + Game->Storage: new Storage() + activate Storage + Storage --> Game: + deactivate Storage + Game->Storage: .saveGame(...) + activate Storage + + ref over Storage + Write wave data into file + end ref + Storage -> Player: player.toText(): String + activate Player + loop equipments + Player -> Equipment: equipment.toText(): String + activate Equipment + Equipment -> Player: Encoded equipment data + deactivate Equipment + end equipments + Player --> Storage: Encoded player data + deactivate Player + ref over Storage + Write player data into file + end ref + + Storage --> Game + destroy Storage + Game --> Game + deactivate Game + end opt +end loop +@enduml + +@startuml +hide footbox +skinparam sequenceReferenceBackgroundColor #FFFFFF + +participant ":Storage" as Storage +participant ":FileWriter" as FileWriter +participant ":File" as File + +activate Storage +group sd Write data into file + Storage -> FileWriter: fw.write(data) + activate FileWriter + FileWriter -> File + activate File + File --> FileWriter + deactivate File + FileWriter --> Storage + deactivate FileWriter +end group +@enduml diff --git a/docs/diagrams/sellSequence.puml b/docs/diagrams/sellSequence.puml new file mode 100644 index 0000000000..73da488449 --- /dev/null +++ b/docs/diagrams/sellSequence.puml @@ -0,0 +1,43 @@ +@startuml +actor Player as UI +participant Shop +participant ShopUI +participant Player +participant Equipment +participant Narrator +participant UI as FunctionsUI + +UI -> Shop : handleShopInput(2) +activate Shop + +Shop -> ShopUI : printSellInstructions() +Shop -> UI : readIntegerInput() +activate UI +UI --> Shop : sellInput (int) +deactivate UI + +Shop -> Shop : handleSellInput(sellInput) +activate Shop + +Shop -> Player : getEquipment(sellInput) +Player --> Shop : equipment +activate Equipment + +Shop -> Equipment : getId() +Equipment --> Shop : id + +alt equipment exists (id != -1) + Shop -> Player : sellEquipment(sellInput) + Player --> Shop : void + + Shop -> Narrator : commentOnShopSell(player, equipment) +else no equipment + Shop -> UI : printErrorMessage("No Equipment at this slot!") +end + +deactivate Shop + +@enduml + + + diff --git a/docs/diagrams/shopClass.puml b/docs/diagrams/shopClass.puml new file mode 100644 index 0000000000..1797db1cb9 --- /dev/null +++ b/docs/diagrams/shopClass.puml @@ -0,0 +1,65 @@ +@startuml + +' Interfaces and Base Classes +class Event { + - player: Player + + Event(player: Player) + + run(): void +} + +' Main Class +class Shop { + - equipments: Equipment[] + - isDone: boolean + + Shop(player: Player, equipments: Equipment[]) + + run(): void + + handleShopInput(input: int): void + + handleBuyInput(buyInput: int): void + + handleSellInput(sellInput: int): void + + startShopping(): void +} + +' Other Classes +class Player { + + buyEquipment(equipment: Equipment): boolean + + sellEquipment(index: int): void + + getEquipment(index: int): Equipment +} + +class Equipment { + + getId(): int +} + +class UI { + + printErrorMessage(message: String): void + + readIntegerInput(): int +} + +class ShopUI { + + printShopMenu(player: Player): void + + printShopCollection(equipments: Equipment[]): void + + printBuyInstructions(): void + + printSellInstructions(): void +} + +class Narrator { + + commentOnShopEntry(): void + + commentOnShopExit(): void + + commentOnShopBuy(player: Player, equipment: Equipment): void + + commentOnShopSell(player: Player, equipment: Equipment): void +} + +class RolladieException +class InterruptedException + +' Relationships +Shop --|> Event +Shop ..> Player +Shop ..> Equipment +Shop ..> UI +Shop ..> ShopUI +Shop ..> Narrator +Shop ..> RolladieException +Shop ..> InterruptedException + +@enduml \ No newline at end of file diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png new file mode 100644 index 0000000000..e90bcae000 Binary files /dev/null and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/game-loop.png b/docs/images/game-loop.png new file mode 100644 index 0000000000..c13dc13f81 Binary files /dev/null and b/docs/images/game-loop.png differ diff --git a/docs/images/main.png b/docs/images/main.png new file mode 100644 index 0000000000..6dd9e20f0d Binary files /dev/null and b/docs/images/main.png differ diff --git a/docs/images/rolladie_battle.png b/docs/images/rolladie_battle.png new file mode 100644 index 0000000000..ff94288913 Binary files /dev/null and b/docs/images/rolladie_battle.png differ diff --git a/docs/images/rolladie_battle1.png b/docs/images/rolladie_battle1.png new file mode 100644 index 0000000000..6be3464aba Binary files /dev/null and b/docs/images/rolladie_battle1.png differ diff --git a/docs/images/rolladie_battle2.png b/docs/images/rolladie_battle2.png new file mode 100644 index 0000000000..a299528e82 Binary files /dev/null and b/docs/images/rolladie_battle2.png differ diff --git a/docs/images/rolladie_battle3.png b/docs/images/rolladie_battle3.png new file mode 100644 index 0000000000..4c560c748d Binary files /dev/null and b/docs/images/rolladie_battle3.png differ diff --git a/docs/images/rolladie_battle4.png b/docs/images/rolladie_battle4.png new file mode 100644 index 0000000000..0538fb93ce Binary files /dev/null and b/docs/images/rolladie_battle4.png differ diff --git a/docs/images/rolladie_battle5.png b/docs/images/rolladie_battle5.png new file mode 100644 index 0000000000..259b367e0c Binary files /dev/null and b/docs/images/rolladie_battle5.png differ diff --git a/docs/images/rolladie_battle6.png b/docs/images/rolladie_battle6.png new file mode 100644 index 0000000000..6fb9792f5e Binary files /dev/null and b/docs/images/rolladie_battle6.png differ diff --git a/docs/images/rolladie_buy_fail.png b/docs/images/rolladie_buy_fail.png new file mode 100644 index 0000000000..07ae217b8f Binary files /dev/null and b/docs/images/rolladie_buy_fail.png differ diff --git a/docs/images/rolladie_buy_instructions.png b/docs/images/rolladie_buy_instructions.png new file mode 100644 index 0000000000..0839fa205c Binary files /dev/null and b/docs/images/rolladie_buy_instructions.png differ diff --git a/docs/images/rolladie_buy_success.png b/docs/images/rolladie_buy_success.png new file mode 100644 index 0000000000..97ba334859 Binary files /dev/null and b/docs/images/rolladie_buy_success.png differ diff --git a/docs/images/rolladie_mainmenu.png b/docs/images/rolladie_mainmenu.png new file mode 100644 index 0000000000..ec8598fe76 Binary files /dev/null and b/docs/images/rolladie_mainmenu.png differ diff --git a/docs/images/rolladie_mainmenu_loadsave.png b/docs/images/rolladie_mainmenu_loadsave.png new file mode 100644 index 0000000000..c3779a132e Binary files /dev/null and b/docs/images/rolladie_mainmenu_loadsave.png differ diff --git a/docs/images/rolladie_sell_fail.png b/docs/images/rolladie_sell_fail.png new file mode 100644 index 0000000000..affe48a4a1 Binary files /dev/null and b/docs/images/rolladie_sell_fail.png differ diff --git a/docs/images/rolladie_sell_instructions.png b/docs/images/rolladie_sell_instructions.png new file mode 100644 index 0000000000..f210bd6476 Binary files /dev/null and b/docs/images/rolladie_sell_instructions.png differ diff --git a/docs/images/rolladie_sell_success.png b/docs/images/rolladie_sell_success.png new file mode 100644 index 0000000000..4b99eac043 Binary files /dev/null and b/docs/images/rolladie_sell_success.png differ diff --git a/docs/images/rolladie_shop_entry.png b/docs/images/rolladie_shop_entry.png new file mode 100644 index 0000000000..dab39fda06 Binary files /dev/null and b/docs/images/rolladie_shop_entry.png differ diff --git a/docs/images/rolladie_shop_exit.png b/docs/images/rolladie_shop_exit.png new file mode 100644 index 0000000000..82d37fb049 Binary files /dev/null and b/docs/images/rolladie_shop_exit.png differ diff --git a/docs/team/james17042002.md b/docs/team/james17042002.md new file mode 100644 index 0000000000..35cd3903b8 --- /dev/null +++ b/docs/team/james17042002.md @@ -0,0 +1,50 @@ +# James Koh - Project Portfolio Page + +## Overview +- RollaDie is a Dungeon & Dragons (DnD) text-based RPG, + optimized to play using Command Line Interface (CLI) and + has a simple text-ui display that reminisces games of the 1960s. +- This program is meant for CS2113 students as a stress reliever + and it aims to provide a fun and replayable experience! + +### Summary of Contributions +## Code contributed +Code contributed: [Code contributed](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=James17042002&breakdown=true) + +## Enhancements implemented + + +1. Empty Slot (equipment) + - Implemented an Empty Slot equipment to handle cases when player do not have a equipment type equipped + - Useful for the Sell Mechanic + +2. Player (equipmentList) + - Implemented equipmentList for the player to equip equipments. + - Implemented the relevant methods to get attack, get defense from the equipments. + - Implemented the relevant methods for the shop to call for the buy and sell mechanics. +3. Shop Event + - Did the UI for Shop (Shop UI) + - Implemented how Shops will be queued in the events queue. + - Implemented how Shops will offer stronger and stronger equipment by leveraging the respective equipment databases + - Implemented the Buy and Sell commands for shop + - Shop is crucial to the playability and strategy of the game, as it allows the player more flexible decison making as to how they decide to spend their gold. + - Helped to modify Junit Testing for Shop Event. + +4. Balancing Equipment Stats + - As Rolladie is a game, managing the stats of the player and enemies as the waves progress is important. + - Balanced the player and enemy such that the player is able to die if he is not careful while ensuring + that the game is not too difficult. + +## Contributions to UG +- Wrote about the Buy and Sell features in the UG. + +## Contributions to DG +- Authored overview section, implementation details section and diagram section for Buying and Selling. +- Created UML diagrams as listed below: + - Sequence Diagram (buy, sell) + +## Contributions to team-based tasks +* Maintain issue tracker +* Set up group meetings +* Set up milestones and linked it to relevant issues +* Reviewed PRs submitted by team members and contributed to discussions \ No newline at end of file diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/tiangsoonyong.md b/docs/team/tiangsoonyong.md new file mode 100644 index 0000000000..76ebabf093 --- /dev/null +++ b/docs/team/tiangsoonyong.md @@ -0,0 +1,33 @@ +# Soon Yong - Project Portfolio Page + +## Overview +RollaDie is a Dungeon & Dragons (DnD) text-based RPG, optimized to play using Command Line Interface (CLI) and has a simple text-ui display that reminisces games of the 1960s. +This program is meant for CS2113 students as a stress reliever and it aims to provide a fun and replayable experience! + +### Summary of Contributions +#### Code contributed: +[My Code Dashboard](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=tiangsoonyong&breakdown=true&sort=groupTitle%20dsc&sortWithin=title&since=2025-02-21&timeframe=commit&mergegroup=&groupSelect=groupByRepos&checkedFileTypes=docs~functional-code~test-code~other&tabOpen=true&tabType=authorship&tabAuthor=TiangSoonYong&tabRepo=AY2425S2-CS2113-T13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=falseNothing) + +#### Enhancements implemented: +My main contribution to the Rolladie game was the +`Storage` class: Handles the translation between game data and local text save file. It mainly features: +- Ability to `save` current progress +- Ability to `load` save file + +In order to achieve **scalability** of the game in future development, I implemented abstract method `toText()` for `Player` and `Equipment` classes +This helped streamline the process for saving game data of new equipments and player type. Additionally, I ensure rigorous testing was done within the `Storage` class to achieve maximum coverage. + +I also contributed to the initial version of `Game` class: Manages all game logic, particularly event generation and event queuing, to ensure a sequential flow of events for the user to enjoy. +Lastly, I implemented the `Flee` and `Exit` function within the Battle class as well. + +#### Contributions to the UG: +- Mainly contributed to the initial version of the UG **before v1.0** particularly the initial class diagram and skeleton of the page + +#### Contributions to the DG: +- Created the UMLs for the `Storage` class, including the class diagram and the two sequence diagram for loading and saving of game. +- Peer checking + +#### Contributions to team-based tasks +- Exploratory testing +- Code Integration +- Code code code diff --git a/docs/team/vincesum.md b/docs/team/vincesum.md new file mode 100644 index 0000000000..2fa798b593 --- /dev/null +++ b/docs/team/vincesum.md @@ -0,0 +1,47 @@ +# Vincent - Project Portfolio Page + +## Overview +- RollaDie is a Dungeon & Dragons (DnD) text-based RPG, +optimized to play using Command Line Interface (CLI) and +has a simple text-ui display that reminisces games of the 1960s. +- This program is meant for CS2113 students as a stress reliever +and it aims to provide a fun and replayable experience! + +### Summary of Contributions +## Code contributed +Code contributed: [Code contributed](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=vincesum&breakdown=true) + +## Enhancements implemented +1. Loot Event + - Implemented the Loot class which is a subclass of Event to grant player gold after defeating enemy. + - Implemented testing logic for determining if player won the battle. + - Only grants player loot if player wins the battle. + - Junit Testing for Loot Event. + +2. Equipment Database + - Implemented the Equipments with the Armor, Boots, and Weapons as well as their associated databases. + - Equipment is crucial for player to upgrade his stats to progress. + - Equipments can be bought in Shop. + +3. Shop Event + - Junit Testing for Shop Event. + +4. Balancing + - As Rolladie is a game, managing the stats of the player and enemies as the waves progress is important. + - Balanced the player and enemy such that the player is able to die if he is not careful while ensuring + that the game is not too difficult. + +## Contributions to UG +- Wrote about the Loot feature in the UG. +- Explained the battle sequence and the total number of rounds as well as the goal of the player. + +## Contributions to DG +- Authored overview section, implementation details section and diagram section for attack, heal, loot and UI. +- Created UML diagrams as listed below: + - Sequence Diagram (attack, heal, loot, UI) + +## Contributions to team-based tasks +* Maintain issue tracker +* Set up group meetings +* Set up milestones and linked it to relevant issues +* Reviewed PRs submitted by team members \ No newline at end of file diff --git a/docs/team/yyingg-243.md b/docs/team/yyingg-243.md new file mode 100644 index 0000000000..d93f85fd20 --- /dev/null +++ b/docs/team/yyingg-243.md @@ -0,0 +1,50 @@ +# Lee Ying Ying - Project Portfolio Page + +# Overview +- RollaDie is a Dungeon & Dragons (DnD) text-based RPG, +optimized to play using Command Line Interface (CLI) and +has a simple text-ui display that reminisces games of the 1960s. +- This program is meant for CS2113 students as a stress reliever +and it aims to provide a fun and replayable experience! + +# Summary of Contributions +Code contributed: [Code contributed](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=yyingg-243&breakdown=true) + +## Enhancements implemented +1. Roll dice + - Implemented the roll dice feature using Java's random library to simulate dice roll. + - Dice outcome is important in determining bonus points in game. + - Rolling dice is crucial to the turn-based mechanics by introducing randomness into the game. + +2. Damage calculation logic + - Refactored the attack logic to separate damage calculation into its own method. + - This helps in improving OOP design and improve readability and testability of the code. + +3. Defend logic + - Integrated roll dice logic for defend action + - Correctly sets bonus points and defending status base on dice outcome and player commands. + +4. Improved Code reliability and security + - Added Junit test cases for classes such as Battle and Player to ensure correctness of logic. + - Integrated assertions to validate game or character state to prevent invalid actions. + - Solve check style errors. + + +## Contributions to UG +- Wrote the base template for user guide, draft out feature component,eplanation and include example usage for features. +- Complete component: table content (adding links), introduction +(introduce Rolladie to the user),quick start (include explanation on running jar file), +command summary (listed out possible commands in RollaDie) + +## Contributions to DG +- Wrote the base template for developer guide. +- Completed acknowledgement, overview, setting up and getting started, product scope (target user, value proposition) +and user stories component. +- Draft out component details and wrote implementation details for attack and defend feature. (revised by members) +- Created UML diagrams(revised by members): exception class diagram, +sequence diagram: attack, heal feature, architecture diagram + +## Contributions to team-based tasks +* Maintain issue tracker +* Set up milestones and linked it to relevant issues +* Reviewed PRs submitted by team members and engaged in group discussion \ No newline at end of file diff --git a/docs/team/zavsky.md b/docs/team/zavsky.md new file mode 100644 index 0000000000..ce22f35e02 --- /dev/null +++ b/docs/team/zavsky.md @@ -0,0 +1,22 @@ +# Irwin Teo - Project Portfolio Page + +## Overview + +Rolladie is a tiny text-based adventure game aimed at maximising replayability and strategic depth for immersive gameplay through each and every turn. It pulls you into delving head-first into the secrets of its world and to emerge victorious in the fight against the horde of enemies charging your way! + +### Summary of Contributions + +#### Code contributed: +My contributions can be found in the following [link](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=zavsky&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2025-02-21&tabOpen=true&tabType=authorship&tabAuthor=zavsky&tabRepo=AY2425S2-CS2113-T13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~other&authorshipIsBinaryFileTypeChecked=false&authorshipIsIgnoredFilesChecked=false). + +Enhancements implemented: +- Improved the replayability of the game through increasing dynamism. Implemented Power stats to prevent players from abusing/spamming strong skills. At the same time, the Power stat will also increase total damage output when stacked. This presents players the dilemma of choice and adds strategic depth to the game. +- Implemented special effects for each Ability to the Abilities class (player skills), to increase the number of ways a battle outcome can be reached. Each ability can introduce extra rules to the damage calculation, to change the outcome dynamically. +- Simplified the way player characters are created in order to vary the enemies that the players can face +- All terminal animations like the ones in TypewriterEffect, DiceBattleAnimation, HpBar packages, to increase the appeal of the terminal-based application for players + +Contributions to User Guide: +Written the parts including Starting a new game, Loading from a save, Making your character, Choosing your battle ability, Battle sequence, command summary and FAQ + +Contributions to Developer Guide: +Helped draw the overall architecture diagram, edited the Main component, Functions component, and Game component. Created the sequence diagram for Load and save segment diff --git a/docs/test_structure.txt b/docs/test_structure.txt new file mode 100644 index 0000000000..9411aa54c4 --- /dev/null +++ b/docs/test_structure.txt @@ -0,0 +1,26 @@ +Folder PATH listing for volume Local Disk +Volume serial number is A43C-0BC4 +C:. +| test_structure.txt +| ++---data +| \---StorageTest +| ValidData.txt +| +\---java + +---game + | | GameTest.java + | | RolladieTest.java + | | RollDiceTest.java + | | + | +---Battle + | | BattleTest.java + | | + | +---Characters + | | CharacterTest.java + | | + | \---functionalities + | UITest.java + | + \---seedu + \---duke diff --git a/docs/uml_image/RollaDieArchitecture_Diagram.png b/docs/uml_image/RollaDieArchitecture_Diagram.png new file mode 100644 index 0000000000..3e07204890 Binary files /dev/null and b/docs/uml_image/RollaDieArchitecture_Diagram.png differ diff --git a/docs/uml_image/StorageClassDiagram.png b/docs/uml_image/StorageClassDiagram.png new file mode 100644 index 0000000000..317d110853 Binary files /dev/null and b/docs/uml_image/StorageClassDiagram.png differ diff --git a/docs/uml_image/attackSequence.png b/docs/uml_image/attackSequence.png new file mode 100644 index 0000000000..563f4c12bd Binary files /dev/null and b/docs/uml_image/attackSequence.png differ diff --git a/docs/uml_image/buySequenceDiagram.png b/docs/uml_image/buySequenceDiagram.png new file mode 100644 index 0000000000..f34253fa4e Binary files /dev/null and b/docs/uml_image/buySequenceDiagram.png differ diff --git a/docs/uml_image/exceptionClassDiagram.png b/docs/uml_image/exceptionClassDiagram.png new file mode 100644 index 0000000000..ad00cb99d3 Binary files /dev/null and b/docs/uml_image/exceptionClassDiagram.png differ diff --git a/docs/uml_image/fleeSequence.png b/docs/uml_image/fleeSequence.png new file mode 100644 index 0000000000..61b27cdf4f Binary files /dev/null and b/docs/uml_image/fleeSequence.png differ diff --git a/docs/uml_image/gameStartMenu.png b/docs/uml_image/gameStartMenu.png new file mode 100644 index 0000000000..d9e74d9b3e Binary files /dev/null and b/docs/uml_image/gameStartMenu.png differ diff --git a/docs/uml_image/loadSequenceDiagram.png b/docs/uml_image/loadSequenceDiagram.png new file mode 100644 index 0000000000..a425a043f5 Binary files /dev/null and b/docs/uml_image/loadSequenceDiagram.png differ diff --git a/docs/uml_image/lootSequenceDiagram.png b/docs/uml_image/lootSequenceDiagram.png new file mode 100644 index 0000000000..56d4b38aa3 Binary files /dev/null and b/docs/uml_image/lootSequenceDiagram.png differ diff --git a/docs/uml_image/saveLoadSequenceDiagram.png b/docs/uml_image/saveLoadSequenceDiagram.png new file mode 100644 index 0000000000..8bed63aa51 Binary files /dev/null and b/docs/uml_image/saveLoadSequenceDiagram.png differ diff --git a/docs/uml_image/saveSequenceDiagram.png b/docs/uml_image/saveSequenceDiagram.png new file mode 100644 index 0000000000..26a6ea98f4 Binary files /dev/null and b/docs/uml_image/saveSequenceDiagram.png differ diff --git a/docs/uml_image/sellSequenceDiagram.png b/docs/uml_image/sellSequenceDiagram.png new file mode 100644 index 0000000000..03b30328eb Binary files /dev/null and b/docs/uml_image/sellSequenceDiagram.png differ diff --git a/src/main/java/Rolladie.java b/src/main/java/Rolladie.java new file mode 100644 index 0000000000..e0ba0e22b4 --- /dev/null +++ b/src/main/java/Rolladie.java @@ -0,0 +1,56 @@ +/* + * This source file was generated by the Gradle 'init' task + */ + +import functions.Storage; +import game.Game; +import exceptions.RolladieException; + +import functions.ui.UI; + +public class Rolladie { + + public static void main(String[] args) { + mainMenu(); + } + + /** + * Starts the game menu and shows options for new game or loading from save + */ + public static void mainMenu() { + UI.printWelcomeMessage(); + + UI.printOptions(); + String userInput = UI.readInput(); + Game game; + while(!userInput.equals("3")) { + try { + switch (userInput) { + case "1": + game = new Game(); + break; + case "2": + int saveSlot = UI.promptSaveFile(); + try { + game = new Storage().loadGame(saveSlot); + UI.showContinueScreen(game); + } catch (RolladieException e) { + UI.printErrorMessage(e.getMessage()); + game = new Game(); + } + break; + default: + throw new RolladieException("Invalid option"); + } + game.run(); + UI.printWelcomeMessage(); + UI.printOptions(); + } catch (RolladieException e) { + UI.printErrorMessage(e.getMessage()); + } finally { + userInput = UI.readInput(); + } + } + } +} + diff --git a/src/main/java/equipments/EmptySlot.java b/src/main/java/equipments/EmptySlot.java new file mode 100644 index 0000000000..e6f1d685b9 --- /dev/null +++ b/src/main/java/equipments/EmptySlot.java @@ -0,0 +1,23 @@ +//@@author James17042002 +package equipments; + +public class EmptySlot extends Equipment { + public EmptySlot() { + super("empty", 0,0,0, 0); + } + + @Override + public String getEquipmentType() { + return "empty"; + } + + @Override + public String toText() { + return ""; + } + + @Override + public int getId() { + return -1; + } +} diff --git a/src/main/java/equipments/Equipment.java b/src/main/java/equipments/Equipment.java new file mode 100644 index 0000000000..782ec969ee --- /dev/null +++ b/src/main/java/equipments/Equipment.java @@ -0,0 +1,67 @@ +package equipments; + +/** + * Represents Armor that Player can augment + */ +public abstract class Equipment { + + public String name; + public int defense; + public int attack; + public int health; + public int value; + + public Equipment(String name, int defense, int attack, int health, int value) { + this.name = name; + this.defense = defense; + this.attack = attack; + this.health = health; + this.value = value; + } + + public abstract int getId(); + + public String getName() { + return this.name; + } + + public int getAttack() { + return this.attack; + } + + public int getDefense() { + return this.defense; + } + + public int getHealth() { + return this.health; + } + + public int getValue() { + return this.value; + } + + @Override + public boolean equals(Object obj) { + assert obj != null:"object cannot be null"; + + // First check if the obj is of type Equipment + if (this == obj) { + return true; // If both references are the same, they're equal + } + + if (obj instanceof Equipment) { + // Cast obj to Equipment to access Equipment-specific methods + Equipment other = (Equipment) obj; + + // Now you can safely call getName() and other methods + return this.getName().equals(other.getName()); + } + + return false; // If obj is not an instance of Equipment, return false + } + + public abstract String getEquipmentType(); + + public abstract String toText(); +} diff --git a/src/main/java/equipments/armors/Armor.java b/src/main/java/equipments/armors/Armor.java new file mode 100644 index 0000000000..09850e1a63 --- /dev/null +++ b/src/main/java/equipments/armors/Armor.java @@ -0,0 +1,38 @@ +//@@author vincesum +package equipments.armors; + +import equipments.Equipment; + +/** + * Represents Armor that Player can augment + */ +public class Armor extends Equipment { + public static final String EQUIPMENT_TYPE = "Armor"; + + public Armor(String name, int defense, int attack, int health, int value) { + super(name, defense, attack, health, value); + } + + public Armor(String name, int defense) { + super(name, defense, 0, 0, 0); + } + + @Override + public String getEquipmentType() { + return EQUIPMENT_TYPE; + } + + @Override + public String toText() { + return EQUIPMENT_TYPE + " " + ArmorDatabase.getIndexByName(this.name); + } + + @Override + public int getId() { + return 0; + } + + public String toString() { + return EQUIPMENT_TYPE +" : " + name + " (" + defense + " def)"; + } +} diff --git a/src/main/java/equipments/armors/ArmorDatabase.java b/src/main/java/equipments/armors/ArmorDatabase.java new file mode 100644 index 0000000000..31a71924dc --- /dev/null +++ b/src/main/java/equipments/armors/ArmorDatabase.java @@ -0,0 +1,60 @@ +//@@author vincesum +package equipments.armors; + +import exceptions.RolladieException; + +import java.util.ArrayList; + +public class ArmorDatabase { + /** + * A static list containing predefined armor objects. + */ + private static final ArrayList armorList = new ArrayList<>(); + + // Static block to initialize predefined armors + static { + armorList.add(new Armor("Leather Armor", 8, 0, 0, 10)); + armorList.add(new Armor("Chainmail Armor", 15, 0, 0, 20)); + armorList.add(new Armor("Iron Armor", 20, 0, 0, 25)); + armorList.add(new Armor("Diamond Armor", 30, 0, 0, 45)); + armorList.add(new Armor("Dragon Scale Armor", 40, 0, 0, 60)); + } + + public static ArrayList getAllArmor() { + return armorList; + } + + /** + * Find armor based on armorName + * @param name Name of the armor being queried + * @return Corresponding armor + */ + public static Armor getArmorByName(String name) throws RolladieException { + for (Armor armor : armorList) { + if (armor.getName().equalsIgnoreCase(name)) { + return armor; + } + } + throw new RolladieException("Armor not found!"); + } + + public static int getIndexByName(String name) { + for (int i = 0; i < armorList.size(); i++) { + if (armorList.get(i).getName().equalsIgnoreCase(name)) { + return i; + } + } + return -1; + } + + public static int getNumberOfArmorTypes() { + return armorList.size(); + } + + public static Armor getArmorByIndex(int index) { + if (index < 0) { + return new Tshirt(); + } + return armorList.get(index); + } +} diff --git a/src/main/java/equipments/armors/Tshirt.java b/src/main/java/equipments/armors/Tshirt.java new file mode 100644 index 0000000000..8183aea2d1 --- /dev/null +++ b/src/main/java/equipments/armors/Tshirt.java @@ -0,0 +1,7 @@ +package equipments.armors; + +public class Tshirt extends Armor { + public Tshirt() { + super("Tshirt", 3); + } +} diff --git a/src/main/java/equipments/boots/Boots.java b/src/main/java/equipments/boots/Boots.java new file mode 100644 index 0000000000..07889ea37e --- /dev/null +++ b/src/main/java/equipments/boots/Boots.java @@ -0,0 +1,38 @@ +//@@author vincesum +package equipments.boots; + +import equipments.Equipment; + +/** + * Represents Weapon that Player can equip + */ +public class Boots extends Equipment { + public static final String EQUIPMENT_TYPE = "Boots"; + + public Boots(String name, int defense, int attack, int health, int value) { + super(name, defense, attack, health, value); + } + + public Boots(String name, int defense, int attack) { + super(name, defense, attack, 0, 0); + } + + @Override + public String getEquipmentType() { + return EQUIPMENT_TYPE; + } + + @Override + public int getId() { + return 1; + } + + @Override + public String toText() { + return EQUIPMENT_TYPE + " " + BootsDatabase.getIndexByName(this.name); + } + + public String toString() { + return EQUIPMENT_TYPE + " : " + name + " (" + defense + " def) (" + attack + "atk)"; + } +} diff --git a/src/main/java/equipments/boots/BootsDatabase.java b/src/main/java/equipments/boots/BootsDatabase.java new file mode 100644 index 0000000000..66bd8e00e3 --- /dev/null +++ b/src/main/java/equipments/boots/BootsDatabase.java @@ -0,0 +1,58 @@ +//@@author vincesum +package equipments.boots; + +import exceptions.RolladieException; + +import java.util.ArrayList; + +public class BootsDatabase { + private static final ArrayList bootsList = new ArrayList<>(); + + // Static block to initialize predefined boots + static { + bootsList.add(new Boots("Leather Soles", 5, 5, 0, 10)); + bootsList.add(new Boots("Chainmail Boots", 9, 9, 0, 15)); + bootsList.add(new Boots("Iron Boots", 12, 16, 0, 25)); + bootsList.add(new Boots("Diamond Boots", 26, 22, 0, 40)); + bootsList.add(new Boots("Dragon Scale Strides", 40, 40, 0, 50)); + } + + // Get all boots + public static ArrayList getAllBoots() { + return bootsList; + } + + /** + * Find boots based on bootsName + * @param name Name of the boots being queried + * @return Corresponding boots + */ + public static Boots getBootsByName(String name) throws RolladieException { + for (Boots boots : bootsList) { + if (boots.getName().equalsIgnoreCase(name)) { + return boots; + } + } + throw new RolladieException("Boots not found!"); + } + + public static int getIndexByName(String name) { + for (int i = 0; i < bootsList.size(); i++) { + if (bootsList.get(i).getName().equalsIgnoreCase(name)) { + return i; + } + } + return -1; + } + + public static int getNumberOfBootsTypes() { + return bootsList.size(); + } + + public static Boots getBootsByIndex(int index) { + if (index < 0) { + return new Slippers(); + } + return bootsList.get(index); + } +} diff --git a/src/main/java/equipments/boots/Slippers.java b/src/main/java/equipments/boots/Slippers.java new file mode 100644 index 0000000000..478bc32e46 --- /dev/null +++ b/src/main/java/equipments/boots/Slippers.java @@ -0,0 +1,7 @@ +package equipments.boots; + +public class Slippers extends Boots{ + public Slippers() { + super("Slippers", 0, 1 ); + } +} diff --git a/src/main/java/equipments/weapons/Stick.java b/src/main/java/equipments/weapons/Stick.java new file mode 100644 index 0000000000..41dc179a81 --- /dev/null +++ b/src/main/java/equipments/weapons/Stick.java @@ -0,0 +1,7 @@ +package equipments.weapons; + +public class Stick extends Weapon { + public Stick() { + super("Stick", 2); + } +} diff --git a/src/main/java/equipments/weapons/Weapon.java b/src/main/java/equipments/weapons/Weapon.java new file mode 100644 index 0000000000..11dec58e6b --- /dev/null +++ b/src/main/java/equipments/weapons/Weapon.java @@ -0,0 +1,39 @@ +//@@author vincesum +package equipments.weapons; + +import equipments.Equipment; + +/** + * Represents Weapon that Player can equip + */ +public class Weapon extends Equipment { + public static final String EQUIPMENT_TYPE = "Weapon"; + + + public Weapon(String name, int defense, int attack, int health, int value) { + super(name, defense, attack, health, value); + } + + public Weapon(String name, int attack) { + super(name, 0, attack, 0, 0); + } + + @Override + public String getEquipmentType() { + return EQUIPMENT_TYPE; + } + + @Override + public int getId() { + return 2; + } + + @Override + public String toText() { + return EQUIPMENT_TYPE + " " + WeaponDatabase.getIndexByName(this.name); + } + + public String toString() { + return EQUIPMENT_TYPE + " : " + name + " (" + attack + " atk)"; + } +} diff --git a/src/main/java/equipments/weapons/WeaponDatabase.java b/src/main/java/equipments/weapons/WeaponDatabase.java new file mode 100644 index 0000000000..7411c53e67 --- /dev/null +++ b/src/main/java/equipments/weapons/WeaponDatabase.java @@ -0,0 +1,73 @@ +//@@author vincesum +package equipments.weapons; + +import exceptions.RolladieException; +import java.util.ArrayList; + +/** + * Database class that maintains a list of Weapon objects. + * This ensures easy access without the need for repeated instantiations. + */ +public class WeaponDatabase { + /** + * A static list containing predefined weapon objects. + */ + private static final ArrayList weaponList = new ArrayList<>(); + + // Static block to initialize predefined weapons + static { + weaponList.add(new Weapon("Wooden Sword", 0, 10, 0, 5)); + weaponList.add(new Weapon("Iron Sword", 0, 20, 0, 10)); + weaponList.add(new Weapon("Kunai", 0, 30, 0, 20)); + weaponList.add(new Weapon("Executioner's Axe", 0, 40, 0, 30)); + weaponList.add(new Weapon("Dragon Tooth Sword", 0, 60, 0, 50)); + } + + + public static ArrayList getAllWeapon() { + return weaponList; + } + + // public static int getIndexByName(String name) throws RolladieException { + // for (int i = 0; i < weaponList.size(); i++) { + // if (weaponList.get(i).getName().equalsIgnoreCase(name)) { + // return i; + // } + // } + // throw new RolladieException("Armor not found!"); + // } + + /** + * Find Weapon based on weaponName + * @param name Name of the weapon being queried + * @return Corresponding Weapon + */ + public static Weapon getWeaponByName(String name) throws RolladieException { + for (Weapon weapon : weaponList) { + if (weapon.getName().equalsIgnoreCase(name)) { + return weapon; + } + } + throw new RolladieException("Weapon not found!"); + } + + public static int getIndexByName(String name) { + for (int i = 0; i < weaponList.size(); i++) { + if (weaponList.get(i).getName().equalsIgnoreCase(name)) { + return i; + } + } + return -1; + } + + public static int getNumberOfWeaponTypes() { + return weaponList.size(); + } + + public static Weapon getWeaponByIndex(int index) { + if (index < 0) { + return new Stick(); + } + return weaponList.get(index); + } +} diff --git a/src/main/java/events/Battle.java b/src/main/java/events/Battle.java new file mode 100644 index 0000000000..e8aaa5c7b3 --- /dev/null +++ b/src/main/java/events/Battle.java @@ -0,0 +1,203 @@ +package events; + +import players.abilities.Ability; +import players.abilities.AbilityType; +import players.abilities.Crush; +import players.abilities.PowerStrike; +import players.abilities.Whirlwind; +import players.Player; +import equipments.armors.Armor; +import equipments.Equipment; +import equipments.weapons.Weapon; +import equipments.EmptySlot; +import functions.DiceBattleAnimation; +import functions.TypewriterEffect; +import exceptions.RolladieException; +import functions.ui.Narrator; +import functions.ui.BattleDisplay; +import functions.ui.HpBar; + + +import java.util.List; +import java.util.ArrayList; + +import static functions.ui.Narrator.END_DELAY; + +public class Battle extends Event { + private int wave; + private Player enemy; + + public Battle(Player player, int wave) { + super(player); + this.wave = wave; + this.enemy = generateNewEnemy(wave); + } + + @Override + public void run() { + try { + + startGameLoop(this.player, this.wave); + } catch (InterruptedException | RolladieException e) { + System.out.println(e.getMessage()); + } + } + + + /** + * Main game loop logic + * + * @param player player character + * @param wave the number of enemies encountered so far + * @throws InterruptedException + */ + public void startGameLoop(Player player, int wave) throws InterruptedException, RolladieException { + assert player != null: "player cannot be null"; + assert player.isAlive(): "player must be alive"; + assert wave > 0: "Number of enemy encountered must be at least 1"; + + System.out.println("🌊 Encounter " + wave + " begins!"); + + if (!this.enemy.isAlive()) { + this.enemy = generateNewEnemy(wave); // todo make tougher per wave + } + + startBattle(player, enemy); + + if (!player.isAlive()) { + TypewriterEffect.print("💀 You fell at encounter " + wave, END_DELAY); + return; + } + + // Heal partially, recharge power + System.out.println("🍃 You survived! Regaining strength..."); + player.resetAllCooldowns(); + player.hp = Math.min(player.maxHp, player.hp + 10); + player.power = Math.min(player.maxPower, player.power + 20); + + if (wave == 3 && !player.hasAbility("Whirlwind")) { + player.abilities.add(new Whirlwind()); + TypewriterEffect.print("🔥 You’ve learned Whirlwind!", END_DELAY); + } + + if (wave == 5) { + player.obtainEquipment(new Weapon("Flame Blade", 5)); + TypewriterEffect.print("🗡️ You obtained the Flame Blade!", 1000); + } + } + + + + /** + * Creates a new enemy when the previous one is defeated, increasing difficulty as wave progresses + */ + public static Player generateNewEnemy(int wave) { + assert wave > 0: "Number of enemy encountered must be at least 1"; + + Weapon claws = new Weapon("Claws", 1 + wave / 2); + Armor hide = new Armor("Hide", 1 + wave / 2); + List equipmentList = new ArrayList(List.of(hide, new EmptySlot(), claws)); + Player enemy = new Player("Enemy " + wave, 30 + wave * 20, (3 + wave) / 2, 2, equipmentList, false); + + enemy.abilities.add(new PowerStrike()); + if (wave >= 3){ + enemy.abilities.add(new Crush()); + } + + return enemy; + } + + /** + * Begins a loop battle scenario with an opponent. Exits when either one is killed + * + * @param player1 + * @param player2 + * @throws InterruptedException + */ + private void startBattle(Player player1, Player player2) throws InterruptedException, RolladieException { + assert player1 != null: "player1 cannot be null"; + assert player2 != null: "player2 cannot be null"; + + int round = 1; + + while (player1.isAlive() && player2.isAlive()) { + System.out.println("\n================ ROUND " + round + " ================\n"); + + BattleDisplay.showPlayerStatus(player1); + BattleDisplay.showPlayerStatus(player2); + + // Choose Abilities + Ability p1Ability = player1.chooseAbility(); + if(p1Ability == null) { + this.isExit = true; + return; + } + if(p1Ability.type.equals(AbilityType.FLEE)) { + if(tryToFlee(player1, player2)) { + System.out.println("[Narrator] You escaped...for now!"); + hasWon = false; + return; + } + System.out.println("[Narrator] Fate was not on your side!"); + } + Ability p2Ability = player2.chooseAbility(); + System.out.println(); + + System.out.println("[Narrator] Dice roll determines the fate of this round!"); + Thread.sleep(0); + + // Dice Roll + Animation + String diceDisplay = DiceBattleAnimation.animateBattle(player1.getDiceRolls(), player2.getDiceRolls()); + + // Store HP before damage + int prevHp1 = player1.hp; + int prevHp2 = player2.hp; + + // Damage + int p1Damage = player1.computeDamageTo(player2); + int p2Damage = 0; + if (p1Damage < player2.hp) { + p2Damage = player2.computeDamageTo(player1); + } + + diceDisplay = player2.applyDamage(p1Damage, player1, diceDisplay); + Narrator.commentOnMomentum(player1, player2, p1Damage, player2.hp); + + diceDisplay = player1.applyDamage(p2Damage, player2, diceDisplay); + Narrator.commentOnMomentum(player2, player1, p2Damage, player1.hp); + + // Show result messages + // System.out.printf("\n%s dealt %d damage to %s\n", player1.name, p1Damage, player2.name); + // System.out.printf("%s dealt %d damage to %s\n", player2.name, p2Damage, player1.name); + + // Animate HP bars *beneath* dice + HpBar.animate(player1, player2, prevHp1, prevHp2, diceDisplay); + + Narrator.commentOnHealth(player1); + Narrator.commentOnHealth(player2); + + round++; + Thread.sleep(0); + + if (round == 5 && !player1.hasAbility("Whirlwind")) { + player1.abilities.add(new Whirlwind()); + TypewriterEffect.print("[Narrator] 🔥 " + player1.name + + " has unlocked a new ability: Whirlwind!", 1000); + } + } + if (player1.isAlive()) { + hasWon = true; + } + + TypewriterEffect.print("\n🏁 " + (player1.isAlive() ? player1.name : player2.name) + " wins the battle!", 1000); + } + + private boolean tryToFlee(Player player1, Player player2) throws InterruptedException { + int[] temp = player1.diceRolls; + player1.diceRolls = new int[player2.diceRolls.length]; + DiceBattleAnimation.animateBattle(player1.getDiceRolls(), player2.getDiceRolls()); + boolean canFlee = player1.totalRoll() > player2.totalRoll(); + player1.diceRolls = temp; + return canFlee; + } +} diff --git a/src/main/java/events/Event.java b/src/main/java/events/Event.java new file mode 100644 index 0000000000..ee302dfed0 --- /dev/null +++ b/src/main/java/events/Event.java @@ -0,0 +1,24 @@ +package events; + +import exceptions.RolladieException; +import players.Player; + +public abstract class Event { + public boolean isExit; + protected Player player; + protected boolean hasWon; + + public Event(Player player) { + this.player = player; + hasWon = false; + isExit = false; + } + + public boolean getHasWon() { + return hasWon; + } + public void setHasWon(boolean hasWon) { + this.hasWon = hasWon; + } + public abstract void run() throws RolladieException, InterruptedException; +} diff --git a/src/main/java/events/Loot.java b/src/main/java/events/Loot.java new file mode 100644 index 0000000000..34c1263da9 --- /dev/null +++ b/src/main/java/events/Loot.java @@ -0,0 +1,57 @@ +package events; + +import functions.ui.Narrator; +import players.Player; +import exceptions.RolladieException; +import functions.ui.LootUI; + +import java.util.Random; + +public class Loot extends Event { + private int baseLoot; + + public Loot(Player player, int loot) { + super(player); + baseLoot = loot; + } + + + /** + * Runs the loot event, which generates loot from 30 to 70 gold and returns it to player. + * + * @throws RolladieException + */ + @Override + public void run() throws RolladieException { + if (hasWon) { + int loot = generateRandomLoot(); + player.earnGold(loot); + LootUI.printLoot(loot); + Narrator.commentOnLootEntry(); + } else { + LootUI.printNoLoot(); + Narrator.commentOnLootDefeat(); + } + LootUI.halt(); + } + + /** + * Runs a simulation of the loot event for testing purposes. + * Does not have LootUI.printLoot(loot) as it causes scanner issues with testing. + * @throws RolladieException + */ + public void simulateRun() throws RolladieException { + if (hasWon) { + int loot = generateRandomLoot(); + player.earnGold(loot); + LootUI.printLoot(loot); + } else { + LootUI.printNoLoot(); + } + } + private int generateRandomLoot() throws RolladieException { + Random rand = new Random(); + int bonusLoot = rand.nextInt(10); + return baseLoot + bonusLoot; + } +} diff --git a/src/main/java/events/Shop.java b/src/main/java/events/Shop.java new file mode 100644 index 0000000000..267df68130 --- /dev/null +++ b/src/main/java/events/Shop.java @@ -0,0 +1,97 @@ +//@@James17042002 + +package events; + +import players.Player; +import equipments.Equipment; +import exceptions.RolladieException; +import functions.ui.UI; +import functions.ui.ShopUI; +import functions.ui.Narrator; + +import static functions.ui.UI.readIntegerInput; + + +public class Shop extends Event { + public Equipment[] equipments; + private boolean isDone; + + public Shop(Player player, Equipment[] equipments) { + super(player); + this.equipments = equipments; + isDone = false; + } + + @Override + public void run() throws RolladieException, InterruptedException { + Narrator.commentOnShopEntry(); + startShopping(); + Narrator.commentOnShopExit(); + } + + + public void handleShopInput(int input) throws RolladieException, InterruptedException { + switch (input) { + case 1: //buy + ShopUI.printBuyInstructions(); + int buyInput = readIntegerInput(); + if (buyInput > equipments.length || buyInput < 0) { + UI.printErrorMessage("Buy index out of range!"); + break; + } + handleBuyInput(buyInput); + break; + case 2: //sell + ShopUI.printSellInstructions(); + int sellInput = readIntegerInput(); + if (sellInput >= 3 || sellInput < 0) { + UI.printErrorMessage("Sell index out of range!"); + break; + } + handleSellInput(sellInput); + break; + case 3: //exit + isDone = true; + break; + default: + UI.printErrorMessage("You can only use \"buy\", \"sell\" or \"leave\" bro"); + } + } + + private void handleBuyInput(int buyInput) throws RolladieException, InterruptedException { + if (buyInput > equipments.length || buyInput < 1) { + UI.printErrorMessage("Buy index out of range!"); + } + Equipment equipment = equipments[buyInput - 1]; + boolean hasBought = player.buyEquipment(equipment); + if (hasBought) { + Narrator.commentOnShopBuy(player, equipment); + } else { + UI.printErrorMessage("Not enough gold!"); + } + } + + private void handleSellInput(int sellInput) throws RolladieException, InterruptedException { + if (sellInput >= 3 || sellInput < 0) { + UI.printErrorMessage("Buy index out of range!"); + } + Equipment equipment = player.getEquipment(sellInput); + + if (equipment.getId() != -1) { + player.sellEquipment(sellInput); + Narrator.commentOnShopSell(player, equipment); + } else { + UI.printErrorMessage("Equipment Type not Equipped!"); + } + } + + + private void startShopping() throws RolladieException, InterruptedException { + while (!isDone) { + ShopUI.printShopCollection(equipments); + ShopUI.printShopMenu(player); + int input = readIntegerInput(); + handleShopInput(input); + } + } +} diff --git a/src/main/java/exceptions/RolladieException.java b/src/main/java/exceptions/RolladieException.java new file mode 100644 index 0000000000..0a077afec9 --- /dev/null +++ b/src/main/java/exceptions/RolladieException.java @@ -0,0 +1,7 @@ +package exceptions; + +public class RolladieException extends Exception { + public RolladieException(String message) { + super(message); + } +} diff --git a/src/main/java/functions/DiceBattleAnimation.java b/src/main/java/functions/DiceBattleAnimation.java new file mode 100644 index 0000000000..35fba88060 --- /dev/null +++ b/src/main/java/functions/DiceBattleAnimation.java @@ -0,0 +1,212 @@ +package functions; + +import java.util.Random; + +import functions.ui.UI; + +public class DiceBattleAnimation { + private static volatile boolean skipAnimation = false; + + private static final String[][] DICE_ART = { + { + " _______ ", + "| |", + "| O |", + "| |", + " ------- " + }, + { + " _______ ", + "| O |", + "| |", + "| O |", + " ------- " + }, + { + " _______ ", + "| O |", + "| O |", + "| O |", + " ------- " + }, + { + " _______ ", + "| O O |", + "| |", + "| O O |", + " ------- " + }, + { + " _______ ", + "| O O |", + "| O |", + "| O O |", + " ------- " + }, + { + " _______ ", + "| O O |", + "| O O |", + "| O O |", + " ------- " + } + }; + + public static void main(String[] args) throws InterruptedException { + int[] player1Faces = {3, 6}; + int[] player2Faces = {1, 5, 2}; + animateBattle(player1Faces, player2Faces); + } + + public static void animateBattleWithInterrupt(int[] player1Rolls, int[] player2Rolls) throws InterruptedException { + skipAnimation = false; + Thread inputThread = new Thread(() -> { + try { + // System.in.read(); + UI.nextLine(); + skipAnimation = true; + } catch (Exception e) { + e.printStackTrace(); + } + }); + + inputThread.setDaemon(true); + inputThread.start(); + + animateBattle(player1Rolls, player2Rolls); + } + + /** + * Prints a dice rolling animation to the terminal + * + * @param player1Rolls individual dice results for Player 1 + * @param player2Rolls individual dice results for Player 2 + * @return final dice settled ascii image representation + * @throws InterruptedException + */ + public static String animateBattle(int[] player1Rolls, int[] player2Rolls) throws InterruptedException { + Random rand = new Random(); + int frames = 16; + + for (int frame = 0; frame < frames; frame++) { + if (skipAnimation) { + break; + } + int[] p1Rolls = randomRolls(player1Rolls.length, rand); + int[] p2Rolls = randomRolls(player2Rolls.length, rand); + + int[] p1Offsets = generateOffsets(player1Rolls.length, frame); + int[] p2Offsets = generateOffsets(player2Rolls.length, frame + 3); // staggered for variety + + TerminalClear.clearAndWrite("🎲 Dice Rolling...\nYour Hero ......... This Enemy\n"); + + // System.out.println("🎲 Dice Rolling..."); + printBattleBoards(p1Rolls, p2Rolls, p1Offsets, p2Offsets); + + Thread.sleep(90); + } + + // Final result frame + String finalDiceRender = printBattleBoards(player1Rolls, player2Rolls, new int[player1Rolls.length], + new int[player2Rolls.length]); + + TerminalClear.clearAndWrite("🎲 Final Rolls:\nYour Hero ......... This Enemy\n" + finalDiceRender); + + return finalDiceRender + "\n"; + } + + private static int[] randomRolls(int size, Random rand) { + int[] rolls = new int[size]; + for (int i = 0; i < size; i++) { + rolls[i] = rand.nextInt(6) + 1; + } + return rolls; + } + + private static int[] generateOffsets(int count, int frame) { + int[] offsets = new int[count]; + for (int i = 0; i < count; i++) { + offsets[i] = (int) (Math.sin((frame + i) * 0.5) * 2); + } + return offsets; + } + + private static String printBattleBoards(int[] p1Faces, int[] p2Faces, int[] p1Offsets, int[] p2Offsets) { + int dieHeight = 5; + + int p1TotalHeight = getMaxOffset(p1Offsets, dieHeight) + p1Faces.length * (dieHeight + 1); + int p2TotalHeight = getMaxOffset(p2Offsets, dieHeight) + p2Faces.length * (dieHeight + 1); + int maxHeight = Math.max(p1TotalHeight, p2TotalHeight); + + StringBuilder[] lines = new StringBuilder[maxHeight]; + for (int i = 0; i < maxHeight; i++) { + lines[i] = new StringBuilder(); + } + + // Draw Player 1 (left side) + int currentP1Line = 0; + for (int i = 0; i < p1Faces.length; i++) { + int offset = p1Offsets[i]; + String[] art = DICE_ART[p1Faces[i] - 1]; + + for (int j = 0; j < dieHeight; j++) { + int lineIndex = currentP1Line + offset + j; + if (lineIndex >= 0 && lineIndex < maxHeight) { + lines[lineIndex].append(art[j]); + } + } + + currentP1Line += dieHeight + 1; + } + + // Add spacing between players + for (StringBuilder line : lines) { + while (line.length() < 12) { + line.append(" "); + } + line.append(" | "); + } + + // Draw Player 2 (right side) + int currentP2Line = 0; + for (int i = 0; i < p2Faces.length; i++) { + int offset = p2Offsets[i]; + String[] art = DICE_ART[p2Faces[i] - 1]; + + for (int j = 0; j < dieHeight; j++) { + int lineIndex = currentP2Line + offset + j; + if (lineIndex >= 0 && lineIndex < maxHeight) { + lines[lineIndex].append(art[j]); + } + } + + currentP2Line += dieHeight + 1; + } + + StringBuilder combined = new StringBuilder(); + + for (StringBuilder line : lines) { + if (line.toString().trim().length() > 0) { + System.out.println(line); + combined.append(line).append("\n"); + } + } + + return combined.toString(); + } + + private static int getMaxOffset(int[] offsets, int dieHeight) { + int max = 0; + for (int offset : offsets) { + if (offset + dieHeight > max) { + max = offset + dieHeight; + } + } + return max; + } + + public static void clearConsole() { + System.out.print("\033[H\033[2J"); + System.out.flush(); + } +} diff --git a/src/main/java/functions/Pair.java b/src/main/java/functions/Pair.java new file mode 100644 index 0000000000..883bbd3af8 --- /dev/null +++ b/src/main/java/functions/Pair.java @@ -0,0 +1,11 @@ +package functions; + +public class Pair { + public final A first; + public final B second; + + public Pair(A a, B b) { + this.first = a; + this.second = b; + } +} diff --git a/src/main/java/functions/Storage.java b/src/main/java/functions/Storage.java new file mode 100644 index 0000000000..3f25969d2a --- /dev/null +++ b/src/main/java/functions/Storage.java @@ -0,0 +1,175 @@ +package functions; + +import java.io.IOException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +import equipments.Equipment; +import equipments.armors.Armor; +import equipments.armors.ArmorDatabase; +import equipments.boots.Boots; +import equipments.boots.BootsDatabase; +import equipments.weapons.Weapon; +import equipments.weapons.WeaponDatabase; +import exceptions.RolladieException; +import functions.ui.UI; +import game.Game; +import players.Player; + +/** + * Translates the game data from and into text save file + */ +public class Storage { + public static final String SAVE_DELIMITER = " | "; + private static final String FILE_DIRECTORY = "data/"; + private static final String FILE_NAME = "savefile"; + private static final String FILE_TYPE = ".txt"; + private static final String LOAD_DELIMITER = " \\| "; + + private final String fileDirectory; + private final String fileName; + private final String fileType; + + /** + * Default constructor of Storage + */ + public Storage() { + this.fileDirectory = FILE_DIRECTORY; + this.fileName = FILE_NAME; + this.fileType = FILE_TYPE; + } + + /** + * Customized constructor of Storage + */ + public Storage(String fileDirectory, String fileName) { + this.fileDirectory = fileDirectory; + this.fileName = fileName; + this.fileType = FILE_TYPE; + } + + /** + * Saves the attributes of the game into a text file + * defined by fileName + saveSLOT + fileType in fileDirectory + * + * @param saveSlot + * @param player + * @param wave + * @throws RolladieException + */ + public void saveGame(int saveSlot, int wave, Player player) throws RolladieException { + File dir = new File(this.fileName); + if (!dir.exists()) { + dir.mkdirs(); + } + String path = this.fileName + saveSlot + this.fileType; + File file = new File(this.fileDirectory, path); + try { + if (!file.exists()) { + file.createNewFile(); + } + FileWriter fw = new FileWriter(file); + + String waveText = Integer.toString(wave); + fw.write(waveText + System.lineSeparator()); + + String playerText = player.toText(); + fw.write(playerText + System.lineSeparator()); + + fw.close(); + UI.printMessage("✅ Game saved to save slot " + saveSlot); + } catch (IOException e) { + throw new RolladieException("❌ Save failed: " + e.getMessage()); + } + } + + /** + * Returns Game object after decoding text from savefile into game parameters + * + * @return Game + */ + public Game loadGame(int saveSlot) throws RolladieException { + String filename = this.fileName + saveSlot + this.fileType; + File f = new File(this.fileDirectory + filename); + try { + Scanner s = new Scanner(f); + int wave = Integer.parseInt(s.nextLine().trim()); + + String[] playerData = s.nextLine().split(LOAD_DELIMITER); + Player player = parsePlayerFromText(wave, playerData); + + UI.printMessage("✅ Game loaded from save slot " + saveSlot); + return new Game(player, wave); + + } catch (FileNotFoundException e) { + throw new RolladieException("savefile.txt not found!"); + } catch (RolladieException e) { + UI.printErrorMessage("❌ Load failed: " + e.getMessage() + "\nStarting new game instead"); + } + return new Game(); + } + + /** + * Returns player object defined by playerData + * Decodes player from text within savefile + * + * @param wave current wave information + * @param playerData player data saved + * @return Creates a new player with the data saved + * @throws RolladieException If there is an error while parsing equipment or if input data is invalid. + */ + private Player parsePlayerFromText(int wave, String[] playerData) throws RolladieException { + String name = playerData[0]; + int hp = Integer.parseInt(playerData[1]); + int maxHp = Integer.parseInt(playerData[2]); + int baseAttack = Integer.parseInt(playerData[3]); + int numDice = Integer.parseInt(playerData[4]); + List equipmentList = parseEquipmentListFromText(Arrays.copyOfRange(playerData, 5, 8)); + int gold = Integer.parseInt(playerData[8]); + int power = Integer.parseInt(playerData[9]); + int maxPower = Integer.parseInt(playerData[10]); + + return new Player(wave, name, hp, maxHp, baseAttack, numDice, equipmentList, gold, power, maxPower); + } + + + /** + * Returns list of equipment defined by equipmentsData + * Intermediate operation for parsing player + * + * @param equipmentsData An array of strings, where each string represents an equipment item. + * @return A list containing the player's equipped armor, boots, and weapon + * @throws RolladieException If the equipment type is invalid or an index is out of range. + */ + private List parseEquipmentListFromText(String[] equipmentsData) throws RolladieException { + int defaultIndex = -1; + Equipment armor = ArmorDatabase.getArmorByIndex(defaultIndex); + Equipment boots = BootsDatabase.getBootsByIndex(defaultIndex); + Equipment weapon = WeaponDatabase.getWeaponByIndex(defaultIndex); + + for (int i = 0; i < equipmentsData.length; i++) { + String[] equipmentText = equipmentsData[i].split(" "); + String equipmentType = equipmentText[0]; + int equipmentIndex = Integer.parseInt(equipmentText[1]); + switch (equipmentType) { + case Armor.EQUIPMENT_TYPE: + armor = ArmorDatabase.getArmorByIndex(equipmentIndex); + break; + case Boots.EQUIPMENT_TYPE: + boots = BootsDatabase.getBootsByIndex(equipmentIndex); + break; + case Weapon.EQUIPMENT_TYPE: + weapon = WeaponDatabase.getWeaponByIndex(equipmentIndex); + break; + default: + throw new RolladieException("Invalid equipment type"); + } + } + return List.of(armor, boots, weapon); + } +} diff --git a/src/main/java/functions/TerminalClear.java b/src/main/java/functions/TerminalClear.java new file mode 100644 index 0000000000..1dd7f3a3fa --- /dev/null +++ b/src/main/java/functions/TerminalClear.java @@ -0,0 +1,11 @@ +package functions; + +public class TerminalClear { + /** + * Buffer for reducing flickering on Windows terminals + */ + public static void clearAndWrite(String content) { + System.out.print("\033[H\033[2J" + content); + System.out.flush(); + } +} diff --git a/src/main/java/functions/TypewriterEffect.java b/src/main/java/functions/TypewriterEffect.java new file mode 100644 index 0000000000..6e9c19a4d8 --- /dev/null +++ b/src/main/java/functions/TypewriterEffect.java @@ -0,0 +1,105 @@ +package functions; + +import java.io.IOException; + +import functions.ui.UI; + +public class TypewriterEffect { + private static volatile boolean skipAnimation = false; + + public static void main(String[] args) { + String paragraph = "This is an example of a typewriter effect, which pauses slightly longer on punctuation. " + + "It creates a cinematic effect... but you can press any key to skip it."; + + Thread inputThread = new Thread(() -> { + try { + System.in.read(); // Wait for any key press + skipAnimation = true; + } catch (Exception e) { + e.printStackTrace(); + } + }); + + inputThread.setDaemon(true); + inputThread.start(); + + print(paragraph); + } + + /** + * (Do not use) + * Prints to console with Typewriter effect, and allows for skipping with any button press + * + * @param text text to print with effect + */ + public static void printWithInterrupt(String text) { + skipAnimation = false; + Thread inputThread = new Thread(() -> { + try { + // System.in.read(); + UI.nextLine(); + skipAnimation = true; + } catch (Exception e) { + e.printStackTrace(); + } + }); + + inputThread.setDaemon(true); + inputThread.start(); + + print(text); + flushInputBuffer(); + } + + public static void printWithInterrupt(String text, int delay) throws InterruptedException { + printWithInterrupt(text); + Thread.sleep(delay); + } + + /** + * Prints to console with Typewriter effect, pausing longer at commas and full stops + * + * @param text text to print with effect + */ + public static void print(String text) { + for (int i = 0; i < text.length(); i++) { + if (skipAnimation) { + System.out.print(text.substring(i)); // Print the rest instantly + break; + } + + char ch = text.charAt(i); + System.out.print(ch); + System.out.flush(); + + try { + if (ch == '.' || ch == ',') { + Thread.sleep(200); + } else if (ch == ' ') { + Thread.sleep(30); + } else { + Thread.sleep(20); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + System.out.println(); // Move to next line after printing + } + + public static void print(String text, int endDelay) throws InterruptedException { + print(text); + Thread.sleep(endDelay); + } + + public static void flushInputBuffer() { + try { + while (System.in.available() > 0) { + System.in.read(); // discard input bytes + } + } catch (IOException e) { + // Ignore + } + } +} diff --git a/src/main/java/functions/ui/AnsiColor.java b/src/main/java/functions/ui/AnsiColor.java new file mode 100644 index 0000000000..fcf6ce894d --- /dev/null +++ b/src/main/java/functions/ui/AnsiColor.java @@ -0,0 +1,8 @@ +package functions.ui; + +public class AnsiColor { + public static final String RESET = "\u001B[0m"; + public static final String RED = "\u001B[31m"; + public static final String GREEN = "\u001B[32m"; + public static final String YELLOW = "\u001B[33m"; // orange-ish +} diff --git a/src/main/java/functions/ui/BattleDisplay.java b/src/main/java/functions/ui/BattleDisplay.java new file mode 100644 index 0000000000..548d084f27 --- /dev/null +++ b/src/main/java/functions/ui/BattleDisplay.java @@ -0,0 +1,44 @@ +package functions.ui; + +import players.Player; +import exceptions.RolladieException; + +public class BattleDisplay { + /** + * Prints to console the Player status, including name, hp, power, weapon stats, + * armor stats and abilities + * + * @param p Player to query status on + */ + public static void showPlayerStatus(Player p) throws RolladieException { + /* + System.out.println("🧍 " + p.name + (p.isHuman ? "" : " (AI)")); + System.out.println("HP : " + p.hp + "/" + p.maxHp + " ❤️"); + System.out.println("Power : " + drawPowerBar(p.power, p.maxPower)); + System.out.println(p.equipmentList.toString()); + System.out.println("Abilities:"); + + for (int i = 0; i < p.abilities.size(); i++) { + Ability a = p.abilities.get(i); + String status = a.isCDReady() ? "✅ ready" : "⏳ " + a.currentCoolDown + " turn(s)"; + System.out.printf(" %d. %s %s (%s)\n", i + 1, a.icon, a.name, status); + } + System.out.println(); + + */ + + System.out.println(p.toString()); + } + + public static String drawPowerBar(int power, int maxPower) { + int segments = 20; + int filled = (int)((power / (double)maxPower) * segments); + StringBuilder sb = new StringBuilder("["); + for (int i = 0; i < segments; i++) { + sb.append(i < filled ? "|" : " "); + } + sb.append("] "); + sb.append(power).append("/").append(maxPower); + return sb.toString(); + } +} diff --git a/src/main/java/functions/ui/GameOverUI.java b/src/main/java/functions/ui/GameOverUI.java new file mode 100644 index 0000000000..040f1f1da6 --- /dev/null +++ b/src/main/java/functions/ui/GameOverUI.java @@ -0,0 +1,34 @@ +package functions.ui; + +/** + * GameOverUI class to print the end screens for the player. + */ +public class GameOverUI { + public static final String GAMEOVER = + " _____ ____ \n" + + " / ____| / __ \\ \n" + + " | | __ __ _ _ __ ___ ___ | | | |_ _____ _ __ \n" + + " | | |_ |/ _` | '_ ` _ \\ / _ \\ | | | \\ \\ / / _ \\ '__|\n" + + " | |__| | (_| | | | | | | __/ | |__| |\\ V / __/ | \n" + + " \\_____|\\__,_|_| |_| |_|\\___| \\____/ \\_/ \\___|_| \n" + + " "; + public static final String VICTORY = + "▓██ ██▓ ▒█████ █ ██ █ █░ ▒█████ ███▄ █\n" + + "▒██ ██▒▒██▒ ██▒ ██ ▓██▒ ▓█░ █ ░█░▒██▒ ██▒ ██ ▀█ █\n" + + "▒██ ██░▒██░ ██▒▓██ ▒██░ ▒█░ █ ░█ ▒██░ ██▒▓██ ▀█ ██▒\n" + + "░ ▐██▓░▒██ ██░▓▓█ ░██░ ░█░ █ ░█ ▒██ ██░▓██▒ ▐▌██▒\n" + + "░ ██▒▓░░ ████▓▒░▒▒█████▓ ░░██▒██▓ ░ ████▓▒░▒██░ ▓██░\n" + + "██▒▒▒ ░ ▒░▒░▒░ ░▒▓▒ ▒ ▒ ░ ▓░▒ ▒ ░ ▒░▒░▒░ ░ ▒░ ▒ ▒ \n" + + "▓██ ░▒░ ░ ▒ ▒░ ░░▒░ ░ ░ ▒ ░ ░ ░ ▒ ▒░ ░ ░░ ░ ▒░\n" + + "▒ ▒ ░░ ░ ░ ░ ▒ ░░░ ░ ░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ \n" + + "░ ░ ░ ░ ░ ░ ░ ░ ░ \n" + + "░ ░ \n" + + " YOU WIN! \n"; + + public static void printGameOver() { + System.out.println(GAMEOVER); + } + public static void printVictory() { + System.out.println(VICTORY); + } +} diff --git a/src/main/java/functions/ui/HpBar.java b/src/main/java/functions/ui/HpBar.java new file mode 100644 index 0000000000..e8c8e71e14 --- /dev/null +++ b/src/main/java/functions/ui/HpBar.java @@ -0,0 +1,77 @@ +package functions.ui; + +import players.Player; +import functions.DiceBattleAnimation; + +public class HpBar { + /** + * Prints to console an animation depicting the HP dropping after each round of battle + * + * @param p1 player + * @param p2 computer-controlled Player + * @param prevHp1 player's previous HP + * @param prevHp2 computer's previous HP + * @param diceDisplay String to keep static on-screen before printing HP bars + * @throws InterruptedException + */ + public static void animate(Player p1, Player p2, int prevHp1, int prevHp2, String diceDisplay) + throws InterruptedException { + int target1 = p1.hp; + int target2 = p2.hp; + + int current1 = prevHp1; + int current2 = prevHp2; + + int max = Math.max(Math.abs(target1 - current1), Math.abs(target2 - current2)); + + for (int i = 0; i <= max; i++) { + if (current1 != target1){ + current1 += Integer.compare(target1, current1); + } + + if (current2 != target2){ + current2 += Integer.compare(target2, current2); + } + + String bar1 = buildBar(current1, p1.maxHp); + String bar2 = buildBar(current2, p2.maxHp); + + DiceBattleAnimation.clearConsole(); + System.out.print("🎲 Final Rolls:\n" + diceDisplay); + System.out.println("❤️ HP Status:"); + System.out.printf("%-10s : ", p1.name); + System.out.println(bar1 + " " + current1 + "/" + p1.maxHp); + System.out.printf("%-10s : ", p2.name); + System.out.println(bar2 + " " + current2 + "/" + p2.maxHp); + + //Thread.sleep(Math.min(70 + 2 * i, 300)); + Thread.sleep(40); + } + } + + private static String buildBar(int hp, int maxHp) { + int totalBlocks = 30; + int filledBlocks = (int) ((hp / (double) maxHp) * totalBlocks); + String color = getColor(hp, maxHp); + + String bar = "[" + color; + for (int i = 0; i < totalBlocks; i++) { + bar += (i < filledBlocks) ? "|" : " "; + } + bar += AnsiColor.RESET + "]"; + return bar; + } + + private static String getColor(int hp, int maxHp) { + double percent = (double) hp / maxHp; + if (percent >= 0.7) { + return AnsiColor.GREEN; + + } else if (percent >= 0.3){ + return AnsiColor.YELLOW; + } else{ + return AnsiColor.RED; + + } + } +} diff --git a/src/main/java/functions/ui/LootUI.java b/src/main/java/functions/ui/LootUI.java new file mode 100644 index 0000000000..64afec6095 --- /dev/null +++ b/src/main/java/functions/ui/LootUI.java @@ -0,0 +1,43 @@ +package functions.ui; + +public class LootUI { + private static final String CHEST = + " _.--.\n" + + " _.-'_:-'||\n" + + " _.-'_.-::::'||\n" + + " _.-:'_.-::::::' ||\n" + + " .'`-.-:::::::' ||\n" + + " /.'`;|:::::::' ||_\n" + + " || ||::::::' _.;._'-._\n" + + " || ||:::::' _.-!oo @.!-._'-.\n" + + " \'. ||:::::.-!()oo @!()@.-'_.|\n" + + " '.'-;|:.-'.&$@.& ()$%-'o.'\\U||\n" + + " `>'-.!@%()@'@_%-'_.-o _.|'||\n" + + " ||-._'-.@.-'_.-' _.-o |'||\n" + + " ||=[ '-._.-\\U/.-' o |'||\n" + + " || '-.]=|| |'| o |'||\n" + + " || || |'| _| ';\n" + + " || || |'| _.-'_.-'\n" + + " |'-._ || |'|_.-'_.-'\n" + + " '-._'-.|| |' `_.-'\n" + + " '-.||_/.-'"; + + + /** + * Prints the loot that the player gets. + * @param gold the amount of gold player gets. + */ + public static void printLoot(int gold) { + System.out.println(CHEST); + System.out.println("You got " + gold + " gold!"); + } + + public static void printNoLoot() { + System.out.println("As you did not win the battle, you did not get anything..."); + } + + public static void halt() { + System.out.println("\nPress Enter to continue..."); + UI.nextLine(); + } +} diff --git a/src/main/java/functions/ui/Narrator.java b/src/main/java/functions/ui/Narrator.java new file mode 100644 index 0000000000..964f0f9e3b --- /dev/null +++ b/src/main/java/functions/ui/Narrator.java @@ -0,0 +1,135 @@ +package functions.ui; + +import players.Player; +import functions.TypewriterEffect; +import equipments.Equipment; + +public class Narrator { + + public static final int END_DELAY = 800; + + public static void commentOnHealth(Player p) throws InterruptedException { + double percent = p.hp / (double) p.maxHp; + + if (p.hp <= 0) { + TypewriterEffect.print("[Narrator] 💀 " + p.name + " has fallen in battle!", END_DELAY); + } else if (percent <= 0.2) { + TypewriterEffect.print("[Narrator] ⚠️ " + p.name + " is barely clinging to life!", END_DELAY); + } else if (percent <= 0.4) { + TypewriterEffect.print("[Narrator] " + p.name + " is severely wounded!", END_DELAY); + } else if (percent >= 0.9) { + TypewriterEffect.print("[Narrator] " + p.name + " is looking nearly untouched.", END_DELAY); + } else if (percent >= 0.7) { + TypewriterEffect.print("[Narrator] " + p.name + " is holding up well.", END_DELAY); + } else { + // Between 40% and 70% + TypewriterEffect.print("[Narrator] " + p.name + " is still standing strong.", END_DELAY); + } + } + + public static void commentOnMomentum(Player attacker, Player defender, int damageDealt, int previousDefenderHp) + throws InterruptedException { + double hpPercentBefore = previousDefenderHp / (double) defender.maxHp; + double hpPercentAfter = defender.hp / (double) defender.maxHp; + + if (damageDealt >= 30) { + TypewriterEffect.print("[Narrator] 💥 Massive impact! " + + attacker.name + " lands a crushing hit!", END_DELAY); + } else if (damageDealt >= 15) { + TypewriterEffect.print("[Narrator] 😲 " + attacker.name + + " delivers a powerful strike!", END_DELAY); + } else { + TypewriterEffect.print("[Narrator] 😲 " + attacker.name + + " delivers a mediocre blow.", END_DELAY); + } + // Comeback detection + double attackerHp = attacker.hp / (double) attacker.maxHp; + double defenderHp = defender.hp / (double) defender.maxHp; + if (attackerHp < 0.3 && defenderHp > 0.7 && damageDealt > 30) { + TypewriterEffect.print("[Narrator] 🤯 Unbelievable! " + + attacker.name + " fights back against the odds!", END_DELAY); + } + + // Sudden swing (drop below critical) + if (hpPercentBefore > 0.4 && hpPercentAfter <= 0.2) { + TypewriterEffect.print("[Narrator] ⚠️ " + + defender.name + " is in critical condition after that blow!", END_DELAY); + } + + // Domination + if ((attacker.hp - defender.hp) > 20) { + TypewriterEffect.print("[Narrator] " + attacker.name + + " is completely dominating the arena!", END_DELAY); + } + + // Clutch survival + if (defender.hp <= 3 && defender.hp > 0) { + TypewriterEffect.print("[Narrator] 😬 " + defender.name + + " barely survives with a sliver of health!", END_DELAY); + } + } + + public static void commentOnLootEntry() { + TypewriterEffect.print("[Narrator] As you won, here's your loot after the battle!"); + } + + public static void commentOnLootDefeat() { + TypewriterEffect.print("[Narrator] As you did not win, you don't deserve loot!"); + } + + public static void commentOnShopEntry() { + TypewriterEffect.print("[Narrator] You enter an Equipment Shop! Upgrade your Stats here!"); + } + + public static void commentOnShopBuy(Player p, Equipment equipment) throws InterruptedException { + TypewriterEffect.print("[Narrator] You buy " + equipment + " for " + equipment.getValue() + " gold."); + System.out.println(p); + } + + public static void commentOnShopSell(Player p, Equipment equipment) throws InterruptedException { + TypewriterEffect.print("[Narrator] You sell " + equipment + " for " + equipment.getValue()/2 + " gold."); + System.out.println(p); + } + + public static void commentOnShopExit() { + TypewriterEffect.print("[Narrator] You leave with your new gear, feeling stronger than ever!"); + } + + public static void newGameSequence() throws InterruptedException { + TypewriterEffect.print("The tavern was thick with the stench of ale and unwashed ambition." + + " You sat in the corner, nursing a drink, when the murmurs reached your ears.\n" + // + "\n" + // + "\"They say the Vault of Dusk holds treasures beyond imagining—gold that glows like embers," + + " relics that whisper forgotten spells, and the Crown of the Fallen King," + + " said to grant power over life itself.\"\n" + // + "\n" + // + "The speaker, a grizzled mercenary with a scar across his nose, scoffed." + + " \"Aye, and it’s also cursed. Dozens have tried. Dozens have died." + + " The vault doesn’t give up its secrets to just anyone.\"\n" + // + "\n" + // + "The other patrons laughed, but you didn’t join them." + + " Your fingers tightened around your tankard.\n" + // + "\n" + // + "Foolhardy? Maybe.\n" + // + "\n" + // + "Determined? Absolutely.\n" + // + "\n" + // + "You’d spent years scraping by on petty jobs—guarding merchants," + + " hunting bandits, fetching trinkets for nobles who barely glanced at you." + + " But this? This was your chance.\n" + // + "\n" + // + "The Vault of Dusk wasn’t just a tomb of gold—it was a crucible. Only the boldest," + + " the cleverest, or the luckiest would emerge alive. And you? You planned on being all three.\n" + // + "\n" + // + "As you stood and tossed a coin onto the table, the mercenary raised an eyebrow." + + " \"You’re not seriously thinking of going, are you?\"\n" + // + "\n" + // + "You grinned. \"Someone’s got to claim that crown. Might as well be me.\"\n" + // + "\n" + // + "The laughter died. The room fell silent.\n" + // + "\n" + // + "And with that, you stepped out into the night," + + " the weight of your destiny—or your doom—settling on your shoulders.", 2000); + } +} + diff --git a/src/main/java/functions/ui/ShopUI.java b/src/main/java/functions/ui/ShopUI.java new file mode 100644 index 0000000000..06a9754b19 --- /dev/null +++ b/src/main/java/functions/ui/ShopUI.java @@ -0,0 +1,34 @@ +package functions.ui; + + +import players.Player; +import equipments.Equipment; +import functions.TypewriterEffect; + + +public class ShopUI { + public static void printShopCollection(Equipment[] equipments) { + TypewriterEffect.print("[Shopkeeper] Here is my shop collection!"); + for (int i = 1; i <= equipments.length; i++) { + System.out.println(i + ": " + equipments[i - 1] + "(" + equipments[i - 1].getValue() + " gold)"); + } + } + + public static void printShopMenu(Player player) { + System.out.println(player.name + " has " + player.gold + " gold — choose an action:"); + System.out.println("1. Buy"); + System.out.println("2. Sell"); + System.out.println("3. Exit the Shop"); + } + + public static void printBuyInstructions() { + System.out.println("Input the corresponding index of the equipment you want to buy."); + } + + public static void printSellInstructions() { + System.out.println("Input the corresponding index of the equipment you want to sell."); + System.out.println("0: Armor"); + System.out.println("1: Boots"); + System.out.println("2: Weapon"); + } +} diff --git a/src/main/java/functions/ui/UI.java b/src/main/java/functions/ui/UI.java new file mode 100644 index 0000000000..594647d705 --- /dev/null +++ b/src/main/java/functions/ui/UI.java @@ -0,0 +1,136 @@ +package functions.ui; + +import players.Player; + +import game.Game; + +import java.io.InputStream; +import java.util.Scanner; + +/** + * The UI class is responsible for handling the user interface in the text-based RPG game. + * It displays messages related to the game, player, and enemy interactions, and renders the game's LOGO. + */ +public class UI { + + /** + * A string containing the ASCII art LOGO of the game. + */ + public static final String LOGO = + " ____ ___ _ _ _ ____ ___ _____ \n" + + "| _ \\ / _ \\ | | | | / \\ | _ \\ |_ _| | ____| \n" + + "| |_) | | | | | | | | | / _ \\ | | | | | | | _| \n" + + "| _ < | |_| | | |___ | |___ / /_\\ \\ | |_| | | | | |___ \n" + + "|_| \\_\\ \\___/ |_____| |_____| /_/ \\_\\|____/ |___| |_____| "; + + + /** + * A separator string used to format the output in the UI. + */ + public static final String LINE_SEPARATOR = "====================================================================="; + + public static Scanner scanner = new Scanner(System.in); + + public static void main(String[] args) { + System.out.println(LOGO); + } + + public static void nextLine() { + scanner.nextLine(); + } + + public static void resetScanner(InputStream in) { + scanner = new Scanner(in); + } + + public static String storageWave() { + return scanner.nextLine().trim(); + } + + public static String[] storagePlayerData(String loadDelimiter) { + return scanner.nextLine().split(loadDelimiter); + } + + public static String readInput() { + String inputLine = scanner.nextLine().toLowerCase(); + return inputLine; + } + + public static int readIntegerInput() throws InterruptedException { + String input = scanner.nextLine().trim(); + int intInput = -1; + try { + intInput = Integer.parseInt(input); + } catch (NumberFormatException e) { + System.out.println("Invalid input. Try again."); + Thread.sleep(1000); + } + return intInput; + } + + /** + * Prints a message to the console. + * + * @param message The message to be printed. + */ + public static void printErrorMessage(String message) { + System.out.println(message); + } + + public static void printMessage(String message) { + System.out.println(message); + } + + + /** + * Prints a welcome message to the player, including the game LOGO and a description. + */ + public static void printWelcomeMessage() { + System.out.println("Welcome to"); + System.out.println(LINE_SEPARATOR); + System.out.println(LOGO); + System.out.println(LINE_SEPARATOR); + System.out.println("A text-based RPG game where your fate is determined by the roll of a die!!"); + } + + public static void printExitMessage() { + System.out.println("Narrator: Leaving so soon? I expected more from you!"); + } + + public static void printDeathMessage() { + System.out.println("Narrator: Game over, you've died! L"); + } + + public static void printWinMessage() { + System.out.println("Narrator: YOU ARE BIG BOI"); + } + + public static int promptSaveFile() { + System.out.print("Choose save slot to load (1–3): "); + int saveSlot = Integer.parseInt(readInput()); + while(saveSlot < 1 || saveSlot > 3) { + System.out.print("Out of range!"); + System.out.print("Choose save slot to load (1–3): "); + saveSlot = Integer.parseInt(readInput()); + } + return saveSlot; + } + + public static void printOptions() { + System.out.println("1. Start New Game"); + System.out.println("2. Load Game"); + System.out.println("3. Exit"); + System.out.print("Select an option: "); + } + + public static void showContinueScreen(Game game) { + Player player = game.getPlayer(); + int wave = game.getWave(); + System.out.println(player.toString()); + + System.out.println("🌊 Current Wave: " + wave); + System.out.println("\nPress Enter to continue..."); + scanner.nextLine(); + } + +} diff --git a/src/main/java/game/Game.java b/src/main/java/game/Game.java new file mode 100644 index 0000000000..effadf1a6b --- /dev/null +++ b/src/main/java/game/Game.java @@ -0,0 +1,168 @@ +package game; + +import events.Loot; +import events.Shop; +import exceptions.RolladieException; +import functions.Storage; +import functions.ui.UI; +import players.Player; +import equipments.armors.ArmorDatabase; +import equipments.boots.BootsDatabase; +import equipments.Equipment; +import equipments.weapons.WeaponDatabase; +import events.Battle; +import events.Event; + +import java.util.Queue; +import java.util.LinkedList; + +/** + * Manages all game logic specifically: Event Generation and Sequence + */ +public class Game { + private static final int MAX_NUMBER_OF_WAVES = 8; + private Queue eventsQueue = new LinkedList<>(); + private Player player; + private Event currentEvent; + private int wave; + private int turnsWithoutShop = 0; + private boolean hasWonCurrBattle = false; + + /** + * Constructor to instantiate a new game + * Creates a new player based on a Hero preset + * Generates the event queue + * Polls the first event from the queue to be the current event + */ + public Game() { + this.player = Player.createNewPlayer(); + this.wave = 1; + this.eventsQueue = generateEventQueue(this.wave); + this.currentEvent = nextEvent(); + } + + /** + * Overloaded constructor used to generate defined game + * Main usage is within the Storage class to load game from save file + * + * @param player + * @param wave + */ + public Game(Player player, int wave) { + this.player = player; + this.wave = wave; + this.eventsQueue = generateEventQueue(wave); + this.currentEvent = nextEvent(); + } + + public int getWave() { + return this.wave; + } + + public Player getPlayer() { + return this.player; + } + + + /** + * Runs the current game until the event sequence is completed + * Ends the game prematurely if the player died within the event + */ + public void run() { + while (this.currentEvent != null && this.player.isAlive()) { + try { + //If current battle is won, sets loot event to give rewards + currentEvent.setHasWon(hasWonCurrBattle); + //Saves game on loot or shop screen after a battle. + if (this.currentEvent instanceof Battle) { + saveGame(); + } + this.currentEvent.run(); + if (this.currentEvent.isExit) { + UI.printExitMessage(); + return; + } + if (!player.isAlive()) { + break; + } + //Checks if current battle is won + if (this.currentEvent instanceof Battle) { + this.wave++; + hasWonCurrBattle = currentEvent.getHasWon(); + } + this.currentEvent = nextEvent(); + } catch (RolladieException | InterruptedException e) { + UI.printErrorMessage(e.getMessage()); + } + } + GameOver gameOver = new GameOver(hasWonCurrBattle); + gameOver.run(); + } + + + /** + * Returns a filled queue of events + * Used during the construction of a new game + * + * @param start + * @return eventsQueue + */ + private Queue generateEventQueue(int start) { + Queue eventsQueue = new LinkedList<>(); + int i; + for (i = start; i <= MAX_NUMBER_OF_WAVES; i++) { + eventsQueue.add(generateBattle(i)); + eventsQueue.add(generateLoot((i + 1) * 10)); + if (i % 2 == 0) { + eventsQueue.add(generateShopEvent(i)); + } + } + eventsQueue.add(generateBattle(i + 1)); + return eventsQueue; + } + + /** + * Returns a battle to insert into the event queue. + * + * @return Event + */ + private Battle generateBattle(int wave) { + Battle newBattle = new Battle(this.player, wave); + return newBattle; + } + + private Event generateLoot(int loot) { + return new Loot(this.player, loot); + } + + public Event generateShopEvent(int wave) { + Equipment[] equipmentsForSale = { + ArmorDatabase.getArmorByIndex((wave / 2) - 1), + BootsDatabase.getBootsByIndex((wave / 2) - 1), + WeaponDatabase.getWeaponByIndex((wave / 2) - 1), + }; + return new Shop(this.player, equipmentsForSale); + } + + /** + * returns the next event inside the event queue + * + * @return Event + */ + private Event nextEvent() { + return this.eventsQueue.poll(); + } + + /** + * Calls the Storage class to save the current game status + */ + public void saveGame() throws RolladieException { + UI.printMessage("💾 Save game? (y/n): "); + String saveInput = UI.readInput(); + if (saveInput.equalsIgnoreCase("y")) { + int saveSlot = UI.promptSaveFile(); + new Storage().saveGame(saveSlot, wave, this.player); + } + } + +} diff --git a/src/main/java/game/GameOver.java b/src/main/java/game/GameOver.java new file mode 100644 index 0000000000..d6bc20fe42 --- /dev/null +++ b/src/main/java/game/GameOver.java @@ -0,0 +1,22 @@ +package game; + +import functions.ui.GameOverUI; +import functions.ui.UI; + +public class GameOver { + private boolean hasWon; + public GameOver(boolean hasWon) { + this.hasWon = hasWon; + } + public void run() { + if (hasWon) { + GameOverUI.printVictory(); + System.out.println("You have won the game! Enter \"enter\" to go back to main menu."); + UI.nextLine();; + } else { + GameOverUI.printGameOver(); + System.out.println("You have lost the game! Enter \"enter\" to go back to main menu."); + UI.nextLine();; + } + } +} diff --git a/src/main/java/players/Player.java b/src/main/java/players/Player.java new file mode 100644 index 0000000000..472b1539de --- /dev/null +++ b/src/main/java/players/Player.java @@ -0,0 +1,578 @@ +package players; + +import static functions.Storage.SAVE_DELIMITER; +import static functions.ui.BattleDisplay.drawPowerBar; + +import java.io.IOException; +import java.io.InputStream; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Scanner; +import java.util.stream.Collectors; + +import equipments.boots.Slippers; +import equipments.weapons.Stick; + +import equipments.Equipment; +import equipments.armors.Tshirt; +import equipments.EmptySlot; +import functions.TypewriterEffect; + +import functions.ui.UI; +import exceptions.RolladieException; +import players.abilities.Ability; +import players.abilities.BasicAttack; +import players.abilities.Flee; +import players.abilities.Heal; +import players.abilities.PowerStrike; +import players.abilities.Whirlwind; + + +/** + * Represents player and non-player characters in the game + */ +public class Player { + private static Scanner scanner = new Scanner(System.in); + + public String name; + public int hp; + public int maxHp; + public int baseAttack; + public int[] diceRolls; + public List equipmentList; + public Ability lastAbilityUsed; + public boolean isHuman; + public List abilities = new ArrayList<>(); + public int gold; + + public int power = 50; + public int maxPower = 100; + + + /** + * Creates a Player object either controlled by a human player or computer + * + * @param name Name of the character + * @param maxHp Maximum hitpoints the character can take + * @param baseAttack Base damage amount + * @param numDice Number of dice to roll during battle encounters + * @param equipmentList A list to encapsulate all the equipment equipped by a character, + * @param isHuman True if creating player-controlled character, false otherwise + */ + public Player(String name, int maxHp, int baseAttack, int numDice, List equipmentList, boolean isHuman) { + this.name = name; + this.hp = this.maxHp = maxHp; + this.baseAttack = baseAttack; + this.diceRolls = new int[numDice]; + this.equipmentList = equipmentList; + this.isHuman = isHuman; + this.gold = 0; + } + + public Player(String name, int maxHp, int baseAttack, int numDice, + List equipmentList, boolean isHuman, int gold) { + this.name = name; + this.hp = this.maxHp = maxHp; + this.baseAttack = baseAttack; + this.diceRolls = new int[numDice]; + this.equipmentList = equipmentList; + this.isHuman = isHuman; + this.gold = gold; + } + + public Player(String name, int maxHp, int baseAttack) { + this.name = name; + this.hp = this.maxHp = maxHp; + this.baseAttack = baseAttack; + this.diceRolls = new int[2]; + this.equipmentList = new ArrayList<>(List.of(new EmptySlot(), new EmptySlot(), new EmptySlot())); + this.isHuman = true; + this.gold = 0; + } + + /** + * Overloaded constructor to load player from save file + */ + + public Player(int wave, String name, int hp, int maxHp, int baseAttack, int numDice, + List equipmentList, int gold, int power, int maxPower) { + this.abilities.add(new Flee()); + this.abilities.add(new BasicAttack()); + this.abilities.add(new PowerStrike()); + this.abilities.add(new Heal()); + if (wave > 2){ + this.abilities.add(new Whirlwind()); + } + this.name = name; + this.hp = hp; + this.maxHp = maxHp; + this.baseAttack = baseAttack; + this.diceRolls = new int[numDice]; + this.equipmentList = equipmentList; + this.gold = gold; + this.power = power; + this.maxPower = maxPower; + this.isHuman = true; + } + + + public static void resetScanner(InputStream in) { + scanner = new Scanner(in); + } + + public int getGold() { + return gold; + } + + public void spendGold(int amount) throws RolladieException { + if (gold - amount < 0) { + throw new RolladieException("not enough gold"); + } + gold -= amount; + } + + public void earnGold(int amount) { + gold += amount; + } + + public int getPlayerAttack() { + return equipmentList.stream() + .filter(e -> e.getId() != -1) + .mapToInt(Equipment::getAttack) + .sum(); + } + + public int getPlayerDefense() { + return equipmentList.stream() + .filter(e -> e.getId() != -1) + .mapToInt(Equipment::getDefense) + .sum(); + } + + public Equipment getEquipment(int equipmentType) { + Equipment currSlot = equipmentList.get(equipmentType); + return currSlot; + } + + public void obtainEquipment(Equipment equipment) throws RolladieException { + Equipment currSlot = equipmentList.get(equipment.getId()); + if (currSlot.getId() != -1) { + throw new RolladieException(currSlot.getEquipmentType() + " is already equipped!"); + } + equipmentList.set(equipment.getId(), equipment); + } + + public void removeEquipment(int equipmentType) throws RolladieException { + Equipment equipment = equipmentList.get(equipmentType); + if (equipment.getId() == -1) { + throw new RolladieException("No equipment at this slot!"); + } + equipmentList.set(equipmentType, new EmptySlot()); + } + + public boolean buyEquipment(Equipment equipment) throws RolladieException { + if (this.gold >= equipment.getValue()) { + if (equipmentList.get(equipment.getId()).getId() != -1) { + removeEquipment(equipment.getId()); + } + obtainEquipment(equipment); + spendGold(equipment.getValue()); + return true; + } else { + return false; + } + } + + public void sellEquipment(int equipmentType) throws RolladieException { + earnGold(getEquipment(equipmentType).getValue() / 2); + removeEquipment(equipmentType); + } + + /** + * Creates a new human player when starting from new game + * + * @return Player character + */ + public static Player createNewPlayer() { + UI.printMessage("Enter your hero's name: "); + String name = scanner.nextLine(); + + // todo: choose character class to vary these starting stats + List equipmentList = new ArrayList(List.of(new Tshirt(), new Slippers(), new Stick())); + Player player = new Player(name, 100, 5, 3, equipmentList, true); + player.abilities.add(new Flee()); + player.abilities.add(new BasicAttack()); + player.abilities.add(new PowerStrike()); + player.abilities.add(new Heal()); + + return player; + } + + private void rollDice() { + Random rand = new Random(); + for (int i = 0; i < diceRolls.length; i++) { + diceRolls[i] = rand.nextInt(6) + 1; + } + } + + /** + * Get hp value of the player + * + * @return An integer represent hp value of the player. + */ + public int getHp() { + return hp; + } + + /** + * Reroll the dice and get results + * + * @return Integer array of size equivalent to number of dice Player has + */ + public int[] getDiceRolls() { + rollDice(); + return diceRolls; + } + + /** + * Adds up the current dice roll results that the Player possesses + * + * @return sum of dice rolls + */ + public int totalRoll() { + int sum = 0; + for (int roll : diceRolls) { + sum += roll; + } + return sum; + } + + + // todo: print the compute damage process + + /** + * Calculates the damage dealt to an opponent, computed as follows: + *

+ * [(dice roll result) + (num of dice) * (weapon bonus)] * + * [(power) / (max power) * 0.5 * (weapon damage multiplier)] - (opponent armor defense) + * + * @param opponent Player that the current Player is battling + * @return damage dealt to an opponent + * @throws InterruptedException + */ + public int computeDamageTo(Player opponent) throws InterruptedException { + assert opponent.isAlive() : "Opponent must be alive to receive damage"; + int base = totalRoll() + (diceRolls.length * getPlayerAttack()); + + // if (powerStrikeActive) base *= 1.5; + double powerMultiplier = 1.0 + (power / (double) maxPower) * 0.5; // up to +50% + int rawDamage = (int) (base * powerMultiplier * lastAbilityUsed.damageMult); + int damage = Math.max(0, rawDamage - opponent.getPlayerDefense()); + + return damage; + } + + /** + * Updates the hitpoints of current Player based on damage dealt by opponent + * + * @param damage Value of damage dealt + * @param opponent Player object representing opponent + * @param text String to concatenate result to + * @return String for UI + * @throws InterruptedException + */ + public String applyDamage(int damage, Player opponent, String text) throws InterruptedException { + assert damage > 0 : "damage value must be non-negative"; + + this.hp = Math.max(0, this.hp - damage); + + String textToPrint; + + if (damage > 10) { + textToPrint = "[Narrator] It's a devastating blow from " + opponent.name + " doing " + damage + " damage!"; + } else if (damage == 0) { + textToPrint = "[Narrator] But the attack glances harmlessly off " + name + "'s armor!"; + } else { + textToPrint = "[Narrator] " + opponent.name + " hits " + name + " for " + damage + " damage."; + } + + TypewriterEffect.print(textToPrint, 1000); + System.out.print("\007"); + + updateAbilityCooldown(); + updatePower(); + applyAbilityAdditionalFeatures(lastAbilityUsed); + + return text + textToPrint + "\n"; + } + + /** + * Recovers Player hitpoints + * + * @param amount value of hitpoints to recover + */ + public void heal(int amount) { + assert amount >= 0 : "amount to heal must be non-negative"; + + this.hp = Math.min(maxHp, this.hp + amount); + } + + /** + * Gives control to player or computer to make battle decisions + * + * @return an Ability object + * @throws InterruptedException + */ + public Ability chooseAbility() throws InterruptedException { + Ability chosenAbility = null; + + if (isHuman) { + chosenAbility = showUserMenu(); + if (chosenAbility == null) { + return null; + } + } else { + chosenAbility = chooseAIAction(); + } + + chosenAbility.startCooldown(); + power = Math.max(0, power - chosenAbility.powerCost); + TypewriterEffect.print("[Narrator] " + name + " uses " + chosenAbility.name, 800); + Thread.sleep(0); + return chosenAbility; + } + + public boolean isAlive() { + return this.hp > 0; + } + + /** + * Prints Player options for each specified round of battle + * + * @return an Ability object + * @throws InterruptedException + */ + private Ability showUserMenu() throws InterruptedException { + System.out.println("\n" + name + " — choose an action:"); + for (int i = 0; i < abilities.size(); i++) { + Ability a = abilities.get(i); + String status = a.isReady(power) ? "" : "(cooldown or insufficient power)"; + System.out.printf("%d. %s %s (%s) %s\n", i + 1, a.icon, a.name, a.tags, status); + } + System.out.println("Type exit to return to Main Menu"); + + while (true) { + // Ensure Scanner is valid and handle exceptions + if (!scanner.hasNextLine()) { + System.out.println("[DEBUG] Scanner has no next line. Recovering..."); + scanner = new Scanner(System.in); + } + + try { + if (System.in.available() > 0) { + scanner.nextLine(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + String input = scanner.nextLine().trim(); + if (input.equals("exit")) { + return null; + } + int intInput = -1; + try { + intInput = Integer.parseInt(input); + } catch (NumberFormatException e) { + System.out.println("Invalid input. Try again."); + Thread.sleep(1000); + } + + if (intInput == -1){ + continue; + } + + if (intInput <= 0 || intInput > abilities.size()) { + continue; + } + + // todo update the typed string for unique ability types + Ability chosenAbility = abilities.get(intInput - 1); + if (chosenAbility.isReady(power)) { + + lastAbilityUsed = chosenAbility; + return chosenAbility; + } else { + System.out.println("On cooldown. Try another ability"); + Thread.sleep(1000); + } + } + } + + + // todo: fix the ai + private Ability chooseAIAction() { + List readyAbilities = abilities.stream() + .filter(a -> a.isReady(power)) + .collect(Collectors.toList()); + + // Prioritize HEALING if low HP + if (hp < maxHp * 0.4) { + for (Ability a : readyAbilities) { + if (a.name.equalsIgnoreCase("Heal")) { + lastAbilityUsed = a; + return a; + } + } + } + + // If power is high, use a powerful skill if available + if (power >= 50) { + for (Ability a : readyAbilities) { + if (a.powerCost >= 30 && !a.name.equalsIgnoreCase("Heal")) { + lastAbilityUsed = a; + return a; + } + } + } + + // Fallback to any offensive move + for (Ability a : readyAbilities) { + if (!a.name.equalsIgnoreCase("Heal")) { + lastAbilityUsed = a; + return a; + } + } + + // Last resort: heal if possible + for (Ability a : readyAbilities) { + lastAbilityUsed = a; + return a; + } + + // No abilities ready → do normal attack + lastAbilityUsed = new BasicAttack(); + return lastAbilityUsed; + } + + /** + * Decrements all Ability cooldowns by 1 + */ + private void updateAbilityCooldown() { + for (Ability a : abilities) { + a.tickCooldown(); + } + } + + /** + * Checks if the Player has learnt a specific Ability + * + * @param name String variable containing Ability name + * @return true if Ability present, false otherwise + */ + public boolean hasAbility(String name) { + assert name != null : "ability to be searched cannot be null"; + + for (Ability a : abilities) { + if (a.name.equalsIgnoreCase(name)){ + return true; + } + } + return false; + } + + /** + * Apply special effects of Abilities + * + * @param ability + */ + public void applyAbilityAdditionalFeatures(Ability ability) { + ability.additionalFeatures(this); + } + + /** + * Increment the Power value after each battle round + */ + public void updatePower() { + updatePower(10); + } + + /** + * Increment the Power value by a variable amount after each battle round + */ + public void updatePower(int powerVal) { + assert powerVal >= 0 : "power value must be non-negative"; + + power = Math.min(maxPower, power + powerVal); + } + + /** + * Reset all Ability cooldowns such that all become usable + */ + public void resetAllCooldowns() { + for (Ability a : abilities) { + a.resetCooldown(); + } + } + + + public String toString() { + StringBuilder sb = new StringBuilder(); + + // Add name and player type (Human or AI) + sb.append("🧍 ").append(name).append(isHuman ? "" : " (AI)").append("\n"); + // Add HP information + sb.append("HP : ").append(hp).append("/").append(maxHp).append(" ❤️\n"); + + // Add Power bar + sb.append("Power : ").append(drawPowerBar(power, maxPower)).append("\n"); + // Add equipment list + for (Equipment equipment : equipmentList) { + if (equipment.getId() != -1) { + sb.append(equipment.toString()).append("\n"); + } else { + sb.append("Empty slot\n"); + } + } + if(isHuman){ + sb.append("Gold: ").append(gold).append("\n"); + } + + // Add abilities + sb.append("Abilities:\n"); + for (int i = 0; i < abilities.size(); i++) { + Ability a = abilities.get(i); + String status = a.isCDReady() ? "✅ ready" : "⏳ " + a.currentCoolDown + " turn(s)"; + sb.append(String.format(" %d. %s %s %s\n", i + 1, a.icon, a.name, status)); + } + + + // Return the final string representation + return sb.toString(); + } + + /** + * Returns encoded string of player data to be saved + * + * @return encoded text + */ + + public String toText() { + String equipmentsText = ""; + for (Equipment equipment : equipmentList) { + equipmentsText += equipment.toText() + SAVE_DELIMITER; + } + + return this.name + SAVE_DELIMITER + + this.hp + SAVE_DELIMITER + + this.maxHp + SAVE_DELIMITER + + this.baseAttack + SAVE_DELIMITER + + this.diceRolls.length + SAVE_DELIMITER + + equipmentsText + + this.gold + SAVE_DELIMITER + + this.power + SAVE_DELIMITER + + this.maxPower; + } +} diff --git a/src/main/java/players/abilities/Ability.java b/src/main/java/players/abilities/Ability.java new file mode 100644 index 0000000000..d4ee893c1b --- /dev/null +++ b/src/main/java/players/abilities/Ability.java @@ -0,0 +1,95 @@ +package players.abilities; + +import players.Player; + +/** + * Represents Player-class abilities used during a battle encounter + */ +public abstract class Ability { + + public AbilityType type; + public String name; + public String tags; + public String icon; + public int coolDown; + public int currentCoolDown; + public double damageMult; + public int powerCost; + + /** + * Create an Ability that Player object can use during battle + * + * @param type AbilityType enum + * @param name Name of the Ability + * @param tags Extra information relating to Ability to aid users' choice. Example: 'Heal +5HP' + * @param icon Emoji icon to represent the attack type + * @param coolDown Number of turns that the Ability will become unusable after casting + * @param damageMult Damage multiplier + * @param powerCost Amount of `Power` required to cast the ability + */ + public Ability(AbilityType type, String name, String tags, String icon, int coolDown, + double damageMult, int powerCost) { + this.type = type; + this.name = name; + this.tags = tags; + this.icon = icon; + this.coolDown = coolDown; + this.currentCoolDown = 0; + this.damageMult = damageMult; + this.powerCost = powerCost; + } + + /** + * Checks if the action is ready to be performed based on both cooldown status and power requirement. + * + * @param currentPower the current available power to check against the Ability's power cost + * @return true if the cooldown is complete and there's sufficient power, false otherwise + */ + public boolean isReady(int currentPower) { + assert currentPower >= 0: "power must not be negative"; + + return currentCoolDown == 0 && currentPower >= powerCost; + } + + /** + * Checks if the cooldown period has completed. + * + * @return true if the cooldown is complete (currentCooldown == 0), false otherwise + */ + public boolean isCDReady() { + return currentCoolDown == 0; + } + + /** + * Sets the cooldown timer + */ + public void startCooldown() { + this.currentCoolDown = coolDown; + } + + /** + * Resets the cooldown timer + */ + public void resetCooldown() { + this.currentCoolDown = 0; + } + + /** + * Decrements the cooldown time + */ + public void tickCooldown() { + if (currentCoolDown > 0) { + currentCoolDown--; + } + } + + /** + * Contains special effects of the Ability + * @param player the Player which the ability will affect + */ + public void additionalFeatures(Player player) {} + + public String toText() { + return this.name; + } +} diff --git a/src/main/java/players/abilities/AbilityType.java b/src/main/java/players/abilities/AbilityType.java new file mode 100644 index 0000000000..75693be2b0 --- /dev/null +++ b/src/main/java/players/abilities/AbilityType.java @@ -0,0 +1,13 @@ +package players.abilities; + +/** + * Stores all ability types available in the game + */ +public enum AbilityType { + BASIC_ATTACK, + POWER_STRIKE, + HEAL, + WHIRLWIND, + CRUSH, + FLEE +} diff --git a/src/main/java/players/abilities/BasicAttack.java b/src/main/java/players/abilities/BasicAttack.java new file mode 100644 index 0000000000..38f5b00ce2 --- /dev/null +++ b/src/main/java/players/abilities/BasicAttack.java @@ -0,0 +1,15 @@ +package players.abilities; + +import players.Player; + +public class BasicAttack extends Ability { + public BasicAttack() { + super(AbilityType.BASIC_ATTACK, "Basic Attack", "", + "🛡️", 0, 1, 0); + + } + + public void additionalFeatures(Player player) { + player.updatePower(10); + } +} diff --git a/src/main/java/players/abilities/Crush.java b/src/main/java/players/abilities/Crush.java new file mode 100644 index 0000000000..3bc05c9491 --- /dev/null +++ b/src/main/java/players/abilities/Crush.java @@ -0,0 +1,13 @@ +package players.abilities; + +import players.Player; + +public class Crush extends Ability { + public Crush() { + super(AbilityType.CRUSH, "Crush", "Flatten", "🔨", 3, 1.5, 40); + } + + public void additionalFeatures(Player player) { + // todo cause some debuff on opponent + } +} diff --git a/src/main/java/players/abilities/Flee.java b/src/main/java/players/abilities/Flee.java new file mode 100644 index 0000000000..81f09100eb --- /dev/null +++ b/src/main/java/players/abilities/Flee.java @@ -0,0 +1,15 @@ +package players.abilities; + +import players.Player; + +public class Flee extends Ability{ + + public Flee() { + super(AbilityType.FLEE, "Flee", "", + "", 0, 0, 0); + } + + public void additionalFeatures(Player player) { + + } +} diff --git a/src/main/java/players/abilities/Heal.java b/src/main/java/players/abilities/Heal.java new file mode 100644 index 0000000000..05f6931aa2 --- /dev/null +++ b/src/main/java/players/abilities/Heal.java @@ -0,0 +1,14 @@ +package players.abilities; + +import players.Player; + +public class Heal extends Ability { + public Heal() { + super(AbilityType.HEAL, "Heal", "+20 HP", "🛡️", 2, 0, 0); + } + + public void additionalFeatures(Player player) { + player.heal(20); + player.updatePower(10); + } +} diff --git a/src/main/java/players/abilities/PowerStrike.java b/src/main/java/players/abilities/PowerStrike.java new file mode 100644 index 0000000000..b5e570dc68 --- /dev/null +++ b/src/main/java/players/abilities/PowerStrike.java @@ -0,0 +1,13 @@ +package players.abilities; + +import players.Player; + +public class PowerStrike extends Ability { + public PowerStrike() { + super(AbilityType.POWER_STRIKE, "Power Strike", "Double damage", "💥", 3, 2, 30); + } + + public void additionalFeatures(Player player) { + + } +} diff --git a/src/main/java/players/abilities/Whirlwind.java b/src/main/java/players/abilities/Whirlwind.java new file mode 100644 index 0000000000..edfd667cc2 --- /dev/null +++ b/src/main/java/players/abilities/Whirlwind.java @@ -0,0 +1,13 @@ +package players.abilities; + +import players.Player; + +public class Whirlwind extends Ability { + public Whirlwind() { + super(AbilityType.WHIRLWIND, "Whirlwind", "Massive damage", "🌪️", 4, 3, 40); + } + + public void additionalFeatures(Player player) { + + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/test/data/StorageTest/testFile100.txt b/src/test/data/StorageTest/testFile100.txt new file mode 100644 index 0000000000..9eff3bcc94 --- /dev/null +++ b/src/test/data/StorageTest/testFile100.txt @@ -0,0 +1,2 @@ +100 +test | -1332747283 | -226506637 | -137090109 | 2 | Armor -1 | Boots -1 | Weapon -1 | 1196634750 | 613899042 | -2076512344 diff --git a/src/test/data/StorageTest/testFile101.txt b/src/test/data/StorageTest/testFile101.txt new file mode 100644 index 0000000000..ec7ea270c5 --- /dev/null +++ b/src/test/data/StorageTest/testFile101.txt @@ -0,0 +1,2 @@ +101 +test | -981103099 | -1566925937 | 1876208781 | 2 | Armor -1 | Boots -1 | Invalid -1 | 199037842 | -558825004 | 486876973 diff --git a/src/test/java/LootTest.java b/src/test/java/LootTest.java new file mode 100644 index 0000000000..c42091ad79 --- /dev/null +++ b/src/test/java/LootTest.java @@ -0,0 +1,25 @@ +import static org.junit.jupiter.api.Assertions.assertTrue; + +import players.Player; +import org.junit.jupiter.api.Test; +import events.Loot; + +public class LootTest { + + @Test + public void lootGranted_updatesPlayerGold() { + Player player = new Player("Tom", 100, 10); + + // Proceed with the loot logic + Loot loot = new Loot(player, 10); + loot.setHasWon(true); + try { + loot.simulateRun(); + } catch (Exception e) { + e.printStackTrace(); + } + + // Validate that the player has received some gold (>= 10) + assertTrue(player.getGold() >= 10); + } +} diff --git a/src/test/java/ShopTest.java b/src/test/java/ShopTest.java new file mode 100644 index 0000000000..e97a6bbc6b --- /dev/null +++ b/src/test/java/ShopTest.java @@ -0,0 +1,79 @@ +import players.Player; +import equipments.Equipment; +import equipments.EmptySlot; +import equipments.armors.ArmorDatabase; +import equipments.boots.BootsDatabase; +import equipments.weapons.WeaponDatabase; +import events.Shop; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ShopTest { + + // Store the original System.in + private final InputStream originalSystemIn = System.in; + + // Restore the original System.in after each test + @AfterEach + public void restoreSystemInStream() { + + System.setIn(originalSystemIn); + } + + @Test + public void testShop_purchaseEquipment_correctlyUpdatesPlayerEquipments() { + Player player = new Player("Tom", 100, 10); + Equipment[] equipmentsForSale = { + ArmorDatabase.getArmorByIndex(0), + BootsDatabase.getBootsByIndex(0), + WeaponDatabase.getWeaponByIndex(0), + }; + Shop shop = new Shop(player, equipmentsForSale); + + // Simulate input for selecting item to purchase + String simulatedInput = "1\n1\n"; // "1" for buying, "1" for selecting first equipment + ByteArrayInputStream inputStream = new ByteArrayInputStream(simulatedInput.getBytes()); + System.setIn(inputStream); + + player.earnGold(100); // Ensure player has enough gold + try { + shop.run(); // Execute the shop logic + Equipment equipment = ArmorDatabase.getArmorByIndex(0); + assertEquals(equipment, player.getEquipment(0)); // Check if player got the correct equipment + } catch (Exception e) { + System.out.println(e.getMessage()); + } + restoreSystemInStream(); + } + + @Test + public void testShop_sellEquipment_correctlyUpdatesPlayerEquipments() { + Player player = new Player("Tom", 100, 10); + String simulatedInput = "2\n0\n"; // "2" to sell, "0" to select armor to sell + ByteArrayInputStream inputStream = new ByteArrayInputStream(simulatedInput.getBytes()); + System.setIn(inputStream); + + Equipment[] equipmentsForSale = { + ArmorDatabase.getArmorByIndex(0), + BootsDatabase.getBootsByIndex(0), + WeaponDatabase.getWeaponByIndex(0), + }; + Shop shop = new Shop(player, equipmentsForSale); + + try { + player.obtainEquipment(ArmorDatabase.getArmorByIndex(0)); // Equip the player with armor + assertEquals(ArmorDatabase.getArmorByIndex(0), player.getEquipment(0)); // Ensure player has armor + shop.run(); // Execute shop logic (should sell the armor) + assertEquals(new EmptySlot(), player.getEquipment(0)); + // Check if the equipment was correctly sold, which is up to your implementation + } catch (Exception e) { + System.out.println(e.getMessage()); + } + restoreSystemInStream(); + } +} diff --git a/src/test/java/equipments/EquipmentTest.java b/src/test/java/equipments/EquipmentTest.java new file mode 100644 index 0000000000..a03fd84de2 --- /dev/null +++ b/src/test/java/equipments/EquipmentTest.java @@ -0,0 +1,101 @@ +package equipments; + + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class EquipmentTest { + + private Equipment sword; + private Equipment shield; + + @BeforeEach + public void setUp() { + sword = new equipments.EquipmentTest.TestEquipment("Iron Sword", + 30, 50, 20, 100); + shield = new equipments.EquipmentTest.TestEquipment("Steel Shield", + 50, 0, 40, 150); + } + + @Test + public void getName_validInput_correctNameReturned() { + assertEquals("Iron Sword", sword.getName()); + assertEquals("Steel Shield", shield.getName()); + } + + @Test + public void getAttack_validInput_correctValueReturned() { + assertEquals(50, sword.getAttack()); + assertEquals(0, shield.getAttack()); + } + + @Test + public void getDefense_validInput_correctValueReturned() { + assertEquals(30, sword.getDefense()); + assertEquals(50, shield.getDefense()); + } + + @Test + public void getHealth_validInput_correctValueReturned() { + assertEquals(20, sword.getHealth()); + assertEquals(40, shield.getHealth()); + } + + @Test + public void getValue_validInput_correctValueReturned() { + assertEquals(100, sword.getValue()); + assertEquals(150, shield.getValue()); + } + + @Test + public void equals_validObject() { + + Equipment anotherSword = new equipments.EquipmentTest.TestEquipment("Iron Sword", + 30, 50, 20, 100); + Equipment differentShield = new equipments.EquipmentTest.TestEquipment("Golden Shield", + 60, 0, 50, 200); + + assertTrue(sword.equals(anotherSword)); + assertFalse(sword.equals(differentShield)); + assertFalse(sword.equals(new Object())); + } + + @Test + public void equals_objectIsNull_assertionThrown() { + + + try{ + sword.equals(null); + }catch(AssertionError e){ + assertEquals("object cannot be null", e.getMessage()); + + } + } + + /* Concrete subclass to allow testing of the abstract class Equipment */ + private static class TestEquipment extends Equipment { + public TestEquipment(String name, int defense, int attack, int health, int value) { + super(name, defense, attack, health, value); + } + + @Override + public String getEquipmentType() { + return "test"; + } + + @Override + public int getId() { + return 0; + } + + @Override + public String toText() { + // TODO Auto-generated method stub + throw new UnsupportedOperationException("Unimplemented method 'toText'"); + } + } +} diff --git a/src/test/java/equipments/weapons/WeaponDatabaseTest.java b/src/test/java/equipments/weapons/WeaponDatabaseTest.java new file mode 100644 index 0000000000..9ec756b4c8 --- /dev/null +++ b/src/test/java/equipments/weapons/WeaponDatabaseTest.java @@ -0,0 +1,53 @@ +package equipments.weapons; + +import exceptions.RolladieException; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class WeaponDatabaseTest { + + @Test + public void getAllWeapon_weaponListFixed_fourWeapon() { + + assertEquals(5, WeaponDatabase.getAllWeapon().size()); + } + + @Test + public void getWeaponByName_validName_weaponIsInList() throws RolladieException { + + Weapon weapon = WeaponDatabase.getWeaponByName("Iron Sword"); + assertNotNull(weapon, "Weapon should not be null."); + assertEquals("Iron Sword", weapon.getName()); + } + + @Test + public void getWeaponByName_invalidInput_exceptionThrown() { + + RolladieException ex = assertThrows(RolladieException.class, + () -> WeaponDatabase.getWeaponByName("Nonexistent Sword")); + assertEquals("Weapon not found!", ex.getMessage()); + } + + @Test + public void getNumberOfWeaponTypes_weaponListFixed_fourTypes() { + + assertEquals(5, WeaponDatabase.getNumberOfWeaponTypes()); + } + + @Test + public void getWeaponByIndex_validInput_correctWeaponReturned() { + + Weapon weapon = WeaponDatabase.getWeaponByIndex(0); + assertNotNull(weapon); + assertEquals("Wooden Sword", weapon.getName()); + } + + @Test + public void getWeaponByIndex_invalidInput_exceptionThrown() { + + assertThrows(IndexOutOfBoundsException.class, () -> WeaponDatabase.getWeaponByIndex(10)); + } +} diff --git a/src/test/java/events/BattleTest.java b/src/test/java/events/BattleTest.java new file mode 100644 index 0000000000..b2d81e058d --- /dev/null +++ b/src/test/java/events/BattleTest.java @@ -0,0 +1,51 @@ +package events; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import equipments.Equipment; +import equipments.armors.Armor; +import equipments.weapons.Weapon; +import org.junit.jupiter.api.Test; +import players.Player; +import java.util.List; + + +class BattleTest { + + private List equipmentList; + + @Test + public void startGameLoop_noEnemy_assertionThrown() throws InterruptedException, exceptions.RolladieException { + + equipmentList = List.of(new Weapon("Sword", 2), + new Armor("Leather", 1)); + Player player = new Player("Hero", 100, 5, 2, equipmentList, true); + + try { + int numberOfEnemy = 0; + Battle battle = new Battle(player, numberOfEnemy); + battle.startGameLoop(player, numberOfEnemy); + + } catch (AssertionError e) { + assertEquals("Number of enemy encountered must be at least 1", e.getMessage()); + } + } + + @Test + public void generateNewEnemy_waveEqualsFive_enemyHasCrush() { + int wave = 5; + Player enemy = Battle.generateNewEnemy(wave); + assertTrue(enemy.hasAbility("crush")); + } + + @Test + public void generateNewEnemy_waveEqualsZero_assertionThrown() { + int wave = 0; + try { + Battle.generateNewEnemy(wave); + } catch (AssertionError e) { + assertEquals("Number of enemy encountered must be at least 1", e.getMessage()); + } + } +} diff --git a/src/test/java/functions/StorageTest.java b/src/test/java/functions/StorageTest.java new file mode 100644 index 0000000000..26f15e9a20 --- /dev/null +++ b/src/test/java/functions/StorageTest.java @@ -0,0 +1,83 @@ +package functions; + +import equipments.Equipment; +import equipments.armors.Tshirt; +import equipments.boots.Slippers; +import equipments.weapons.Stick; +import exceptions.RolladieException; +import game.Game; +import players.Player; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class StorageTest { + Player testPlayer; + List testEquipments = new ArrayList(List.of(new Tshirt(), new Slippers(), new Stick())); + String testFileDirectory = "src/test/data/StorageTest/"; + String testFileName = "testFile"; + Storage testStorage = new Storage(testFileDirectory, testFileName); + + @Test + public void saveAndLoad_equals() throws RolladieException { + testPlayer = generateTestPlayer(); + int saveSlot = 100; + int testWave = saveSlot; + testStorage.saveGame(saveSlot, testWave, testPlayer); + Game loadedGame = testStorage.loadGame(saveSlot); + Player loadedPlayer = loadedGame.getPlayer(); + int loadedWave = loadedGame.getWave(); + assertEquals(loadedWave, testWave); + assertEquals(loadedPlayer.name, testPlayer.name); + assertEquals(loadedPlayer.hp, testPlayer.hp); + assertEquals(loadedPlayer.maxHp, testPlayer.maxHp); + assertEquals(loadedPlayer.baseAttack, testPlayer.baseAttack); + assertEquals(loadedPlayer.diceRolls.length, testPlayer.diceRolls.length); + assertEquals(loadedPlayer.equipmentList, testPlayer.equipmentList); + assertEquals(loadedPlayer.gold, testPlayer.gold); + assertEquals(loadedPlayer.power, testPlayer.power); + assertEquals(loadedPlayer.maxPower, testPlayer.maxPower); + assertEquals(loadedPlayer.abilities.size(), testPlayer.abilities.size()); + } + + @Test + public void invalidEquipment_startNewGame() { + int saveSlot = 101; + assertThrows(NoSuchElementException.class, () -> testStorage.loadGame(saveSlot)); + } + + @Test + public void invalidSaveSlot_throwException() { + int saveSlot = 102; + assertThrows(RolladieException.class, () -> testStorage.loadGame(saveSlot)); + } + + @Test + public void invalidFilePath_throwException() { + String testFileDirectory = "INVALID"; + testStorage = new Storage(testFileDirectory, testFileName); + testPlayer = generateTestPlayer(); + assertThrows(RolladieException.class, () -> testStorage.saveGame(0,0, testPlayer)); + } + + private Player generateTestPlayer() { + int testWave = 100; + Random random = new Random(); + String name = "test"; + int hp = random.nextInt(); + int maxHp = random.nextInt(); + int baseAttack = random.nextInt(); + int numDice = 2; + int gold = random.nextInt(); + int power = random.nextInt(); + int maxPower = random.nextInt(); + return new Player(testWave, name, hp, maxHp, baseAttack, numDice, testEquipments, gold, power, maxPower); + } +} diff --git a/src/test/java/functions/ui/UITest.java b/src/test/java/functions/ui/UITest.java new file mode 100644 index 0000000000..349fee8786 --- /dev/null +++ b/src/test/java/functions/ui/UITest.java @@ -0,0 +1,151 @@ +package functions.ui; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; + +/** + * A class contains Junit test cases for UI class + */ +public class UITest { + + private final InputStream originalIn = System.in; + private final PrintStream originalOut = System.out; + private ByteArrayInputStream testIn; + private ByteArrayOutputStream testOut; + + @BeforeEach + public void setUp() { + testOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(testOut)); + } + + @AfterEach + public void restoreStreams() { + System.setIn(originalIn); + System.setOut(originalOut); + } + + @Test + public void readInput_validInput_returnsLowercase() { + String simulatedInput = "HELLO\n"; + testIn = new ByteArrayInputStream(simulatedInput.getBytes()); + System.setIn(testIn); + + UI.resetScanner(System.in); + String result = UI.readInput(); + assertEquals("hello", result); + } + + @Test + public void readInput_emptyInput_returnsEmptyString() { + testIn = new ByteArrayInputStream("\n".getBytes()); + System.setIn(testIn); + + UI.resetScanner(System.in); + String result = UI.readInput(); + assertEquals("", result); + } + + @Test + public void printErrorMessage_printsCorrectly() { + UI.printErrorMessage("Error!"); + assertTrue(testOut.toString().contains("Error!")); + } + + @Test + public void printErrorMessage_printsNothingOnNull() { + UI.printErrorMessage(null); + assertTrue(testOut.toString().contains("null")); + } + + @Test + public void printMessage_printsCorrectly() { + UI.printMessage("Hello world"); + assertTrue(testOut.toString().contains("Hello world")); + } + + @Test + public void printMessage_emptyMessage_printsNewLine() { + UI.printMessage(""); + assertTrue(testOut.toString().contains("\n")); + } + + @Test + public void printWelcomeMessage_outputsLogoAndIntro() { + UI.printWelcomeMessage(); + String output = testOut.toString(); + assertTrue(output.contains("Welcome to")); + assertTrue(output.contains("RPG")); + } + + @Test + public void printExitMessage_displaysExitLine() { + UI.printExitMessage(); + assertTrue(testOut.toString().contains("Leaving so soon")); + } + + @Test + public void printDeathMessage_displaysDeathMessage() { + UI.printDeathMessage(); + assertTrue(testOut.toString().contains("you've died")); + } + + @Test + public void promptSaveFile_validInput_returnsInput() { + String simulatedInput = "2\n\n"; + testIn = new ByteArrayInputStream(simulatedInput.getBytes()); + System.setIn(testIn); + UI.resetScanner(System.in); + int result = UI.promptSaveFile(); + assertEquals(2, result); + } + + @Test + public void promptSaveFile_emptyInput_returnsEmpty() { + String simulatedInput = "\n\n\n\n"; + ByteArrayInputStream inputStream = new ByteArrayInputStream(simulatedInput.getBytes()); + System.setIn(inputStream); + UI.resetScanner(System.in); + + assertThrows(NumberFormatException.class, () -> { + UI.promptSaveFile(); + }); + } + + @Test + public void printOptions_displaysAllOptions() { + UI.printOptions(); + String output = testOut.toString(); + assertTrue(output.contains("1. Start New Game")); + assertTrue(output.contains("2. Load Game")); + assertTrue(output.contains("3. Exit")); + } + + /*@Test + public void showContinueScreen_displaysPlayerAndWave() { + String simulatedInput = "TestHero\ncontinue\n"; + ByteArrayInputStream testIn = new ByteArrayInputStream(simulatedInput.getBytes()); + System.setIn(testIn); + UI.resetScanner(testIn); // reset Scanner BEFORE using input + + Game game = new Game(); // now createNewPlayer() can read from input + + ByteArrayOutputStream testOut = new ByteArrayOutputStream(); + System.setOut(new PrintStream(testOut)); + + UI.showContinueScreen(game); + + String output = testOut.toString(); + assertTrue(output.contains("🌊 Current Wave: 1"), "Output: \n" + output); + }*/ + +} diff --git a/src/test/java/players/PlayerTest.java b/src/test/java/players/PlayerTest.java new file mode 100644 index 0000000000..d3433f6513 --- /dev/null +++ b/src/test/java/players/PlayerTest.java @@ -0,0 +1,172 @@ +package players; + +import equipments.Equipment; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import players.abilities.Ability; +import players.abilities.BasicAttack; +import players.abilities.PowerStrike; +import equipments.armors.Armor; +import equipments.weapons.Weapon; +import java.io.ByteArrayInputStream; +import java.util.List; + +public class PlayerTest { + + private Player player; + private Player opponent; + private List equipmentList; + private List opponentList; + + @BeforeEach + public void setUp(){ + equipmentList = List.of(new Weapon("Sword", 2), + new Armor("Leather", 1)); + player = new Player("Hero", 100, 5, 2, + equipmentList, true); + + opponentList = List.of(new Weapon("Claws", 1), new Armor("Shield", 4)); + opponent = new Player("Enemy", 100, 3, 4, + opponentList, false); + } + + @Test + public void createNewPlayer_validInput_playerIsAlive(){ + String simulatedInput = "TestHero\n"; + ByteArrayInputStream testInput = new ByteArrayInputStream(simulatedInput.getBytes()); + Player.resetScanner(testInput); + Player player = Player.createNewPlayer(); + assertTrue(player.isAlive()); + } + + @Test + public void totalRoll_checkDiceSum_sumIsCorrect(){ + + player.diceRolls = new int[]{5,10,12}; + int total = player.totalRoll(); + + assertEquals(27, total); + } + + + @Test + void totalRoll_negativeRolls_sumIsCorrectlyCalculated() { + player.diceRolls = new int[]{-1, 5, 3}; + int total = player.totalRoll(); + + assertEquals(7, total); + } + + @Test + public void computeDamageTo_testDamageCalculation_damageEqualsNine() throws InterruptedException{ + + player.maxPower = 100; + player.power = 50; + player.lastAbilityUsed = new BasicAttack(); + + player.diceRolls = new int[]{4, 3}; + + + int damage = player.computeDamageTo(opponent); + assertEquals(9,damage); + } + + @Test + public void computeDamageTo_opponentIsNotAlive_assertionThrown() throws InterruptedException{ + try{ + opponent = new Player("Enemy", 0, 5, 2, opponentList, true); + + player.lastAbilityUsed = new BasicAttack(); + player.lastAbilityUsed.damageMult = 1; + player.computeDamageTo(opponent); + }catch(AssertionError e){ + assertEquals("Opponent must be alive to receive damage", e.getMessage()); + + } + } + + @Test + public void applyDamage_largeDamage_devastatingBlow() throws InterruptedException { + + opponent.lastAbilityUsed = new BasicAttack(); + String result = opponent.applyDamage(15, player, "Battle: "); + + assertEquals(85, opponent.getHp()); + assertTrue(result.contains("devastating blow")); + } + + @Test + public void applyDamage_playerDamageIsNegative_assertionThrown()throws InterruptedException { + + int playerDamage = -10; + + try{ + player.applyDamage(playerDamage,opponent,"Testing"); + + }catch(AssertionError e){ + assertEquals("damage value must be non-negative",e.getMessage()); + } + } + + @Test + public void heal_healAmountIsNegative_assertionThrown() { + try{ + player.heal(-100); + }catch(AssertionError e){ + assertEquals("amount to heal must be non-negative", e.getMessage()); + } + } + + @Test + public void heal_healAmountEqualsFIve_hpUpdatedCorrectly() { + player.heal(5); + assertEquals(100, player.getHp()); + } + + @Test + public void updatePower_updateAmountIsFive_powerValueUpdatedCorrectly(){ + player.updatePower(5); + assertEquals(55, player.power); + } + + + @Test + public void updatePower_updateAmountIsNegative_assertionThrown() { + try{ + player.updatePower(-100); + }catch(AssertionError e){ + assertEquals("power value must be non-negative", e.getMessage()); + } + } + + @Test + public void resetAllCooldowns_nullAbilityInList_gracefulHandling() { + player.abilities.add(null); + player.abilities.add(new BasicAttack()); + + assertThrows(NullPointerException.class, player::resetAllCooldowns); + } + + @Test + public void resetAllCooldowns_setsAllCooldownsToZero() { + Ability ability1 = new BasicAttack(); + Ability ability2 = new PowerStrike(); + + ability1.currentCoolDown = 100; + ability2.currentCoolDown = -100; + + player.abilities.add(ability1); + player.abilities.add(ability2); + + player.resetAllCooldowns(); + + assertEquals(0, ability1.currentCoolDown); + assertEquals(0, ability2.currentCoolDown); + } + +} + diff --git a/src/test/java/players/abilities/AbilityTest.java b/src/test/java/players/abilities/AbilityTest.java new file mode 100644 index 0000000000..1a0444dc43 --- /dev/null +++ b/src/test/java/players/abilities/AbilityTest.java @@ -0,0 +1,88 @@ +package players.abilities; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import players.Player; + +/** + * A class contains Junit test for ability class + */ +public class AbilityTest { + Ability ability; + + /* Create a subclass for testing */ + static class TestAbility extends Ability { + public TestAbility() { + super(AbilityType.BASIC_ATTACK, "Test", "", "🔥", 2, 1.5, 10); + } + + @Override + public void additionalFeatures(Player player) {} + } + + @BeforeEach + public void setUp() { + ability = new players.abilities.AbilityTest.TestAbility(); + } + + @Test + public void isReady_currentPowerIsGreater_shouldReturnTrue() { + assertTrue(ability.isReady(15)); + } + + @Test + public void isReady_whenNotEnoughPower_shouldReturnFalse() { + assertFalse(ability.isReady(5)); + } + + @Test + public void isReady_negativeValue_assertionThrown() { + try{ + ability.isReady(-10); + }catch(AssertionError e){ + assertEquals("power must not be negative", e.getMessage()); + } + } + + @Test + public void isReady_whenOnCooldown_shouldReturnFalse() { + ability.startCooldown(); + assertFalse(ability.isReady(15)); + } + + @Test + public void isCDReady_whenCooldownZero_shouldReturnTrue() { + assertTrue(ability.isCDReady()); + } + + @Test + public void startCooldown_setCurrentCooldownToDefinedCoolDown_returnTrue() { + ability.startCooldown(); + assertEquals(2, ability.currentCoolDown); + } + + @Test + public void resetCooldown_setCurrentCooldownToZero_coolDownEqualsZero() { + ability.startCooldown(); + ability.resetCooldown(); + assertEquals(0, ability.currentCoolDown); + } + + @Test + public void tickCooldown_coolDownEqualsTwo_shouldDecreaseCooldown() { + ability.startCooldown(); + ability.tickCooldown(); + assertEquals(1, ability.currentCoolDown); + } + + @Test + public void tickCooldown_coolDownIsZero_shouldNotGoBelowZero() { + ability.resetCooldown(); + ability.tickCooldown(); + assertEquals(0, ability.currentCoolDown); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -}