ADDED EXT_LICENSE Index: EXT_LICENSE ================================================================== --- /dev/null +++ EXT_LICENSE @@ -0,0 +1,401 @@ +============================================================ +License for src/main/resources/fonts/Strait-Regular.ttf +============================================================ + +Copyright (c) 2012, Eduardo Tunni (http://www.tipo.net.ar), with Reserved Font Name 'Strait' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +============================================================ +License for src/main/resources/fonts/HomemadeApple-Regular.ttf +============================================================ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +============================================================ +License for src/main/resources/fonts/IBMPlexMono-Medium.ttf +============================================================ + +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. Index: README.md ================================================================== --- README.md +++ README.md @@ -1,56 +1,57 @@ # Mountain Group 105 -You are a group of four weary travelers. -You have been walking for so long that you no longer have any memory of where you came from or who you are. -You cannot recall anything but the endless action of putting one foot in front of the other, traversing this empty landscape. -Suddenly, you look up, startled out of your reverie. -An imposing mountain looms before you. -Your party steps forward, drawn to it for some inexplicable reason. -You crane your neck to see if you can make out the peak, but the morning mist impedes your view. -You all know you cannot go back to wherever you came from. -You have to keep going. -You have to climb the mountain. -Maybe whatever waits for you up there will remind you of who you are… and what you’re searching for. - -## Quickstart Guide - -This section contains instructions on how to download and run pre-compiled Jar distribution. - -First, this project requires [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html), so make sure that is installed. - -Next, navigate to the [latest GitHub release](https://github.com/CSC207-2022F-UofT/course-project-group-105/releases/latest), then download and unzip the attached `course-project-group-105.zip` file. - -Finally, navigate to the `bin` folder and run either `course-project-group-105.bat` (on Windows) or `course-project-group-105`. - -Note: because JavaFX has native components, it is somewhat hit-or-miss whether the pre-compiled Jar will on a specific platform/operating system combination. It is only known to consistently work on Linux+amd64 - -*If it does not work, try the instructions in the next section* - -## Build (and Run) from Source +![Screenshots of the game](screenshots.webp) + +Mountain Group 105 is a rogue-like video game originally created as part of a [software design course](https://artsci.calendar.utoronto.ca/course/csc207h1), with the following premise: + +> You are a group of four weary travellers. +> You have been walking for so long that you no longer have any memory of where you came from or who you are. +> You cannot recall anything but the endless action of putting one foot in front of the other, traversing this empty landscape. +> Suddenly, you look up, startled out of your reverie. +> An imposing mountain looms before you. +> Your party steps forward, drawn to it for some inexplicable reason. +> You crane your neck to see if you can make out the peak, but the morning mist impedes your view. +> You all know you cannot go back to wherever you came from. +> You have to keep going. +> You have to climb the mountain. +> Maybe whatever waits for you up there will remind you of who you are… and what you’re searching for. + +## Running Instructions ### IntelliJ IDEA (IDE) Instructions -First download the source code by going to `File > New > Project from Version Control...`, set `Version control:` to `Git`, and the `URL:` to `https://github.com/CSC207-2022F-UofT/course-project-group-105.git`. -Then press `Clone`. +First download the latest source code from [here](https://vcs.pta.gg/mg105/download) and extract it. + +Next, open the project in IntelliJ by going to to `File > Open`, and navigating to the folder created in the last step. Next, if IntelliJ does not immediately recognize the fact this is a Gradle project (you can tell by the lack of a `Gradle` tab on any of the edges), navigate to the `build.gradle` file in the IDE. An icon with an elephant should appear, click it. There should now be a `Gradle` tab on one of the edges. -Finally, open the `Gradle` tab and navigate to `course-project-group-105 > Tasks > application` and double-click run. +Finally, open the `Gradle` tab and navigate to `course-project-group-105 > Tasks > application` and double-click `run`. ### Command-Line (CLI) Instructions First, this project requires [Java 17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html), so make sure that is installed. -Next clone the repository and switch to the directory which can be done by +Next download the source code and change into that directory which can either be done by + +```sh +fossil clone https://vcs.pta.gg/mg105 +cd mg105 +``` + +or by ```sh -git clone https://github.com/CSC207-2022F-UofT/course-project-group-105.git -cd course-project-group-105 +wget https://vcs.pta.gg/mg105/zip/trunk/download.zip +unzip download.zip +cd download ``` + +if you do not have [Fossil](https://fossil-scm.org) installed. Finally, this is a standard Gradle project, so to start the application simply run ```sh gradlew.bat run @@ -62,55 +63,71 @@ ./gradlew run ``` on any Unix-like operating system (or the built-in IntelliJ IDEA terminal). -## Highlights (and extra hints for the TA) - -- Functionality - - All twelve (12) of our original user stories are complete. - - In addition we've added the following extra functionality: - - Persistence: When a battle ends your characters' stats are automatically saved and are recalled on next game open. - Even if you lose the game your characters' stats get saved! - - Minimap: Press 'm' to open a minimap that will show a visual representation of the map you've discovered so far! - - Keyboard layout: - - `w`, `a`, `s`, `d`: movement keys, in the usual configuration. - - `e`: interact with an adjacent chest on the map. - - `f`: fight an adjacent enemy on the map. - - `m`: open the minimap (any key closes it). - - `i`: open/close the inventory. - - ``: open the walking menu. - - `k`: open the help text. - - `t`: start the tutorial. -- Code Organization - - Code is organized by layers, `com.mg105.user_interface`, `com.mg105.interface_adapters`, `com.mg105.data_control`, `com.mg105.use_cases`, `com.mg105.entities`. - - The `com.mg105.user_interface` package is the only package that knows anything about the graphics library, JavaFX. - - The `com.mg105.utils` package mostly keeps track of constants. -- Testing - - As of [96e8a0a3](https://github.com/CSC207-2022F-UofT/course-project-group-105/pull/101/commits/96e8a0a3081fbd400cdc11552415465772a5a1a1), (line) test coverage is as follows: - - `com.mg105.entities`: 90% - - `com.mg105.use_cases`: 90% - - `com.mg105.data_control`: 76% - - `com.mg105.interface_adapters`: 57% - - `com.mg105.user_interface`: 1% - - `com.mg105.utils`: 100% - - Total: 51% - - Some tests assume a **completely clean** environment, if some tests fail delete `move_data.csv` and `party_data.csv` and run them again. -- Documentation - - Current up-to-date Javadoc can be found [here](https://docs.mg105.com/). -- Extra GitHub Features Used - - GitHub actions to make sure sensitive files (`.idea/*`) are not accidentally modified in a PR ([link](https://github.com/CSC207-2022F-UofT/course-project-group-105/actions/workflows/sanity.yml)). - - GitHub releases for every merge into `main`, built by GitHub actions ([link](https://github.com/CSC207-2022F-UofT/course-project-group-105/releases)). - - GitHub pages that host up-to-date Javadoc of `main`, built automatically by GitHub actions ([link](https://docs.mg105.com/)). - -## Note for Apple Silicon Users (m1 and beyond chips) -Due to some dependency issues this game will NOT build properly for anyone using Apple Silicon (m1 or m2 chips). You -should still be able to make changes and run and create tests. +## Features + +As a complete game, Mountain Group 105 features + +- a randomly generated room-based map, +- the ability to change the character on the map, +- a progressively updating minimap that only shows currently discovered rooms, +- a turn-based battle sequence, +- an inventory system (with health potions and upgrade tokens), +- an item-based character upgrade system, +- an interactive controls display, + +and it has been balanced so that it plays decently well! + +*Exercise to the reader: match a feature to each of the four screenshots at the top of this README.* + +## Program Architecture + +Being part of a specific course, this project strictly adheres to [Robert Martin's Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html). + +The code is packaged by layers where + +- `com.mg105.entities` contain the enterprise business rules, +- `com.mg105.use_cases` contain the application business rules, +- `com.mg105.interface_adapters` contain the interface adapters, and +- `com.mg105.user_interface` and `com.mg105.data_control` both contain frameworks and drivers. + +Additionally the code makes use of a plethora of design patterns including + +- facade pattern in `com.mg105.use_cases.save.Saver` and `com.mg105.use_cases.set_up.data_system_creator.DataStorageSystemCreator`, +- simple factory pattern in `com.mg105.use_cases.inventory.ItemFactory` and `com.mg105.user_interface.StandardComponentFactory`, +- builder pattern in `com.mg105.use_cases.set_up.data_system_creator.DataStorageSystemCreator`, +- chain of responsibility pattern in `com.mg105.user_interface.InputListener`, `com.mg105.interface_adapters.InputInterpreter`, and `com.mg105.interface_adapters.InputControllable`, +- observer pattern in `com.mg105.use_cases.map.RoomUpdater`, +- mediator pattern in `com.mg105.entities.GameState`, and +- dependency injection in essentially all the classes. + +## Changes Since Initial Release + +As previously mentioned this was originally part of a course project. +The original state of the source code as of the submission date can be found [here](https://vcs.pta.gg/mg105/dir?ci=release-1) or at its original home on GitHub, [here](https://github.com/CSC207-2022F-UofT/course-project-group-105). + +Since the original release, the only changes have been a complete UI overhaul (the core functionality and overall architecture is essentially unchanged). +The marking scheme allocated no marks for how pretty it looked, and so no time was spent on that. +In particular the overhaul aims to improve + +1. lack of consistent game resolution, +1. unintuitive and inconsistent controls (some buttons, some keyboard), and +1. lack of any real graphics or use of colour. + +A full diff of these changes can be found [here](https://vcs.pta.gg/mg105/vdiff?from=release-1&to=trunk). + +## Note for Apple Silicon Users + +Mountain Group 105 has a hard dependency on JavaFX, which has been flaky on Apple Silicon chips. +The game may not be playable on your hardware. + +## Note for Tiling Window Manager Users + +Put your window manager into its floating mode before starting the game. +The game expects a constant, self-set resolution, which tiling window managers tend to violate. ## Copyright -Unless mentioned otherwise, code is licensed under the GNU Affero General Public License, Version 3.0. -See the [LICENSE](/LICENSE) file for more details. - -Here are the exceptions: - -- `imgs/background2.jpg` by Fred Seibert is licensed under [CC BY-NC-ND 2.0](https://www.creativecommons.org/licenses/by-nc-nd/2.0/). +Unless mentioned in the [EXT_LICENSE](/file?name=EXT_LICENSE&ci=trunk) file, code is licensed under the GNU Affero General Public License, Version 3.0. +See the [LICENSE](/file?name=LICENSE&ci=trunk) and [EXT_LICENSE](/file?name=EXT_LICENSE&ci=trunk) files for more details. DELETED images/closed_issue.png Index: images/closed_issue.png ================================================================== --- images/closed_issue.png +++ /dev/null cannot compute difference between binary files DELETED images/closed_project.png Index: images/closed_project.png ================================================================== --- images/closed_project.png +++ /dev/null cannot compute difference between binary files DELETED images/create_branch.png Index: images/create_branch.png ================================================================== --- images/create_branch.png +++ /dev/null cannot compute difference between binary files DELETED images/create_pr.png Index: images/create_pr.png ================================================================== --- images/create_pr.png +++ /dev/null cannot compute difference between binary files DELETED images/create_project.png Index: images/create_project.png ================================================================== --- images/create_project.png +++ /dev/null cannot compute difference between binary files DELETED images/link_branch.png Index: images/link_branch.png ================================================================== --- images/link_branch.png +++ /dev/null cannot compute difference between binary files DELETED images/link_project.png Index: images/link_project.png ================================================================== --- images/link_project.png +++ /dev/null cannot compute difference between binary files DELETED images/new_issue.png Index: images/new_issue.png ================================================================== --- images/new_issue.png +++ /dev/null cannot compute difference between binary files DELETED images/new_pr.png Index: images/new_pr.png ================================================================== --- images/new_pr.png +++ /dev/null cannot compute difference between binary files DELETED images/rename.png Index: images/rename.png ================================================================== --- images/rename.png +++ /dev/null cannot compute difference between binary files DELETED images/set_tags.png Index: images/set_tags.png ================================================================== --- images/set_tags.png +++ /dev/null cannot compute difference between binary files DELETED imgs/background2.jpg Index: imgs/background2.jpg ================================================================== --- imgs/background2.jpg +++ /dev/null cannot compute difference between binary files DELETED project_plan_dev.md Index: project_plan_dev.md ================================================================== --- project_plan_dev.md +++ /dev/null @@ -1,104 +0,0 @@ -# Project Planning and Development with Github - -In this course project, you are expected to use Github to manage your code. This document describes the workflow for using Github when you are developing the course project. Please read it carefully and follow the instructions. **Try to work through the steps with the help of your team first, but please ask for help if your team gets stuck on any of the steps or needs something clarified.** - -## Create a New Github Project -[Github Projects](https://docs.github.com/en/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects) (*Yes, the name of this product is called "Github Projects", do not confuse it with the course project*) is a lightweight project management tool that is integrated to Github. You can use it to track issues, pull requests, visualize tasks status, and track responsibilities. **TAs will mark you project implementation plan and track your progression using the Github project.** - -1. Navigate to **Projects** Page on [CSC207 organization page](https://github.com/orgs/CSC207-2022F-UofT/projects) - -2. Click **New project**, and click **Create** on the new page. -![](images/create_project.png) - -3. Click the title bar to rename the project to your team/project's name, and press **Enter** to save the change. - -![](images/rename.png) - -4. Link the project to your repository. Navigate to your repository and select **Projects** tab, then click **Add Project** and select the project you just created. - -![](images/link_project.png) - -5. The project will show up in the the list below. - -## Define Your Features for the Implementation Plan -As a part of the project planning, you are required to record all features formulated from your user stories, as **issues** in your Github repository. - -1. Navigate to your repository and select **Issues** tab, then click **New issue**. - -![](images/new_issue.png) - -1. Fill in the title as the name of the feature and provide a brief description of the feature. -**Please use a consistent naming convention for your issues.** For example, you can use the following format: `[Feature x] ` - -2. On the side bar, select the **Assignee**, **Labels** (Enhancement for your Features), and **Projects**(the one you just created) for the issue. Then click **Submit new issue**. - -![](images/set_tags.png) - -3. On the project page, you can see an item is automatically created. :warning: :warning: **Make sure you verify that each feature issue is successfully created in the project.** :warning: :warning: - - -## Feature Development -When you work on a feature, you are always required to create a **branch** for the feature and **merge** the branch back to the main branch with **pull requests** when the feature is completed. Note: the below should remind you of the "workflow" we covered in the first lab this term. Please review the details of that document in addition to the below, which provides additional details about how the process works on GitHub. - -1. To create a new branch, navigate to the issue you are assigned to, and click **Create branch** on the right side bar. - -![](images/create_branch.png) - -2. Select a name and click **Create branch** on the pop-up window. Use the provided command to check out the branch you just created on your local machine. - - *Alternatively, you can create a branch manually, and link it to the issue.* - -3. Verify that the branch is successfully linked to the issue. - - -![](images/link_branch.png) - - -## Merge Feature Branch to Main Branch -When you finish working on a feature, you are required to merge the feature branch back to the main branch with a **pull request**. - -1. After you make changes to the code and commit them to the feature branch, you will see a **Compare & pull request** button on the repository page. Click it to create a pull request. - - *Alternatively, you can create a pull request in the **Pull requests** tab.* - -![](images/create_pr.png) - -2. Give a meaningful title and description for the pull request, remember please make the name consistent. - - 2.1 First make sure that you are merging from the feature branch to the main branch (see blue box). - - 2.2 Make sure that you set the correct fields as issues (see red box). - -![](images/new_pr.png) - -3. Select reviewers for the pull request. You can select multiple reviewers. The reviewers will be notified and will review your code. You can also add comments to the pull request. - -4. After the reviewers approve the pull request, you can merge the pull request. :warning: :warning: **Pull requests -must be reviewed and approved by other team members before merging.** :warning: :warning: **Reviewing and approving pull requests will be a part of the evaluation.** - -5. After the pull request is merged, the linked issue will be automatically closed. You can verify that the issue is closed by navigating to the issues page and project page. - -![](images/closed_issue.png) - -![](images/closed_project.png) - -6. (Optional) Delete the feature branch after the pull request is merged. You can delete the branch by navigating to the **View all branches** page. - -7. (Optional) If the feature is not completed or you want to continue working on the feature, you can reopen the issue and create new pull requests. Remember to change the status of the issue to **In Progress**. - - -## More Project Management and Other Resoruces (Optional) - -- Use issues to keep track of bugs, tasks and other things that need to be done by selecting the appropriate labels. - -- Use milestones to group issues into a set of deliverables. To create milestones, navigate to the **Milestones** tab and click **New milestone**. - -- Use Projects to tracks issues and collaborate with your team. See the [sample project (Password Manager example)](https://github.com/orgs/CSC207-2022F-UofT/projects/2) for reference. - -- Github document for projects: https://docs.github.com/en/issues/planning-and-tracking-with-projects - -- Git operations: https://docs.github.com/en/get-started/using-git - -- Git cheat sheet: https://education.github.com/git-cheat-sheet-education.pdf - -- I MESSED UP GIT WHAT TO DO?!: https://dangitgit.com/ ADDED screenshots.webp Index: screenshots.webp ================================================================== --- /dev/null +++ screenshots.webp cannot compute difference between binary files Index: src/main/java/com/mg105/Application.java ================================================================== --- src/main/java/com/mg105/Application.java +++ src/main/java/com/mg105/Application.java @@ -13,15 +13,11 @@ import com.mg105.interface_adapters.inventory.InventoryPresenter; import com.mg105.interface_adapters.map.MapGeneratorInterpreter; import com.mg105.interface_adapters.map.MinimapInterpreter; import com.mg105.interface_adapters.map.RoomInterpreter; import com.mg105.interface_adapters.map.RoomInterpreterInterface; -import com.mg105.interface_adapters.tutorial.TutorialTextController; -import com.mg105.use_cases.ChestInteractor; -import com.mg105.use_cases.OpponentSetInteractor; -import com.mg105.use_cases.ReplayGenerator; -import com.mg105.use_cases.WalkVisInteractor; +import com.mg105.use_cases.*; import com.mg105.use_cases.battle.BattleInteractor; import com.mg105.use_cases.inventory.InventoryInteractor; import com.mg105.use_cases.map.*; import com.mg105.use_cases.save.PartySaver; import com.mg105.use_cases.save.Save; @@ -29,10 +25,11 @@ import com.mg105.use_cases.set_up.data_system_creator.CreateDataStorage; import com.mg105.use_cases.set_up.data_system_creator.DataStorageSystemCreator; import com.mg105.use_cases.set_up.state_setter.GameStateSetter; import com.mg105.use_cases.set_up.state_setter.PartyCreator; import com.mg105.user_interface.*; +import com.mg105.utils.UIConstants; import javafx.scene.input.KeyEvent; import javafx.stage.Stage; import java.awt.*; import java.util.HashMap; @@ -49,117 +46,111 @@ * * @param primaryStage the primary stage for this application. */ @Override public void start(Stage primaryStage) { - - // Set up the initial use cases - Inventory inventory = new Inventory(); - - GameState state = new GameState(inventory, new WalkingCharacter(new Point(1, 1))); - - // Setting up database + GameState state = new GameState(new Inventory(), new WalkingCharacter(new Point(1, 1))); CreateDataStorage[] databaseCreators = {new MoveDataCreator(), new PartyDataCreator()}; DataStorageSystemCreator databaseCreator = new DataStorageSystemCreator(databaseCreators); databaseCreator.create(); - // Setting the values from the database in game state PartyCreator[] partyCreator = {new PartyCreator(new PartyDataAccess(new MoveDataAccess()))}; GameStateSetter setter = new GameStateSetter(partyCreator); setter.setState(state); - Map drawableComponents = new HashMap<>(); - // We fill this map in later because of the ordering of parameters + Map drawableComponents = new HashMap<>(); // We fill this map in later because of the ordering of parameters SceneController sceneController = new SceneController( primaryStage, drawableComponents, Toggler.ToggleableComponent.MAP ); + + StandardComponentFactory componentFactory = new StandardComponentFactory(sceneController); + MapGenerator mapGenerator = new MapGenerator(state); MapGeneratorInterpreter mapGeneratorInterpreter = new MapGeneratorInterpreter(mapGenerator); MapGeneratorButton generateMapButton = new MapGeneratorButton(mapGeneratorInterpreter, sceneController); - MainMenu mainMenu = new MainMenu(generateMapButton); + MainMenu mainMenu = new MainMenu(sceneController, generateMapButton, componentFactory); RoomGetterInterface roomGetter = new RoomGetter(state); RoomInterpreterInterface roomInterpreter = new RoomInterpreter(roomGetter); MapDrawer mapDrawer = new MapDrawer(roomInterpreter); drawableComponents.put(Toggler.ToggleableComponent.MAIN_MENU, mainMenu); drawableComponents.put(Toggler.ToggleableComponent.MAP, mapDrawer); - // Minimap setup MinimapInterpreter minimapInterpreter = new MinimapInterpreter(roomGetter); MinimapDrawer minimapDrawer = new MinimapDrawer(minimapInterpreter); drawableComponents.put(Toggler.ToggleableComponent.MINIMAP, minimapDrawer); - // InventoryDisplay set up InventoryPresenter inventoryPresenter = new InventoryPresenter(); InventoryInteractor inventoryInteractor = new InventoryInteractor(state, inventoryPresenter); - InventoryDisplay inventoryDisplay = new InventoryDisplay(new InventoryController( - inventoryInteractor)); + InventoryDisplay inventoryDisplay = new InventoryDisplay( + new InventoryController(inventoryInteractor), + componentFactory, + new PartyGetter(new PartyStatGetter(state)) + ); inventoryPresenter.setView(inventoryDisplay); drawableComponents.put(Toggler.ToggleableComponent.INVENTORY, inventoryDisplay); - /////Tutorial scene//// - TutorialTextController textChanger = new TutorialTextController(false); - TutorialTextDisplay textDisplay = new TutorialTextDisplay(); - TutorialTextWindow tutorialDisplay = new TutorialTextWindow(textChanger, textDisplay); - drawableComponents.put(Toggler.ToggleableComponent.TUTORIAL, tutorialDisplay); - ////////////////////// - - //WalkingMenu scene// WalkVisInteractor walkVisInteractor = new WalkVisInteractor(state); WalkVisController walkVisController = new WalkVisController(walkVisInteractor); - WalkingMenu walkingMenu = new WalkingMenu(walkVisController); + WalkingMenu walkingMenu = new WalkingMenu(walkVisController, sceneController, componentFactory); drawableComponents.put(Toggler.ToggleableComponent.WALK_MENU, walkingMenu); - ///////////////////// - //LoseMenu scene// ReplayGenerator replayGenerator = new ReplayGenerator(state, minimapInterpreter); replayGenerator.replay(); ReplayGeneratorInterpreter replayGeneratorInterpreter = new ReplayGeneratorInterpreter(replayGenerator); ReplayGeneratorButton loseButton = new ReplayGeneratorButton(replayGeneratorInterpreter, sceneController, Toggler.ToggleableComponent.LOSE_MENU); - LoseMenu loseMenu = new LoseMenu(loseButton); + LoseMenu loseMenu = new LoseMenu(loseButton, componentFactory, mainMenu); drawableComponents.put(Toggler.ToggleableComponent.LOSE_MENU, loseMenu); - //////////////////// - - //BattleMenu scene// - //OpponentSet setup OpponentSetInteractor opponentInteractor = new OpponentSetInteractor(state); - // Creating Saver Save[] savers = {new PartySaver(state, new PartyDataAccess(new MoveDataAccess()))}; Saver saver = new Saver(savers); - //Battle setup BattleInteractor battleInteractor = new BattleInteractor(state, inventoryInteractor, saver); BattlePresenter battlePresenter = new BattlePresenter(battleInteractor, sceneController); - BattleMenu battleMenu = new BattleMenu(battlePresenter); + BattleMenu battleMenu = new BattleMenu(battlePresenter, componentFactory); drawableComponents.put(Toggler.ToggleableComponent.BATTLE, battleMenu); - ///////////////////// RoomUpdater roomUpdater = new RoomUpdater(); roomUpdater.addObserver(mapDrawer); roomUpdater.addObserver(minimapInterpreter); CharacterMoverInterface characterMover = new CharacterMover(state, roomUpdater); - /////WinGame Scene///// ReplayGeneratorButton winButton = new ReplayGeneratorButton(replayGeneratorInterpreter, sceneController, Toggler.ToggleableComponent.WIN_MENU); - WinMenu winMenu = new WinMenu(winButton); + WinMenu winMenu = new WinMenu(winButton, componentFactory, mainMenu); WinDisplay winDisplay = new WinDisplay(sceneController, roomGetter, replayGenerator); roomUpdater.addObserver(winDisplay); drawableComponents.put(Toggler.ToggleableComponent.WIN_MENU, winMenu); - ///////////////// + + KeymapDisplay keymapDisplay = new KeymapDisplay(componentFactory); + drawableComponents.put(Toggler.ToggleableComponent.CONTROLS, keymapDisplay); + + IntroDisplay introDisplay = new IntroDisplay(sceneController, keymapDisplay, componentFactory); + drawableComponents.put(Toggler.ToggleableComponent.INTRO, introDisplay); ChestInteractor chestInteractor = new ChestInteractor(state, inventoryInteractor, roomUpdater); - InputInterpreter inputInterpreter = new InputInterpreter(characterMover, sceneController, textChanger, chestInteractor, - opponentInteractor); + + Map inputPassthroughs = new HashMap<>(); + inputPassthroughs.put(Toggler.ToggleableComponent.MAIN_MENU, mainMenu); + inputPassthroughs.put(Toggler.ToggleableComponent.WIN_MENU, winMenu); + inputPassthroughs.put(Toggler.ToggleableComponent.LOSE_MENU, loseMenu); + inputPassthroughs.put(Toggler.ToggleableComponent.INTRO, introDisplay); + inputPassthroughs.put(Toggler.ToggleableComponent.BATTLE, battleMenu); + + InputInterpreter inputInterpreter = new InputInterpreter(characterMover, sceneController, chestInteractor, + opponentInteractor, inventoryDisplay, walkingMenu, sceneController, inputPassthroughs); InputListener inputListener = new InputListener(inputInterpreter); + sceneController.toggle(Toggler.ToggleableComponent.INTRO); + sceneController.toggle(Toggler.ToggleableComponent.MAIN_MENU); primaryStage.addEventFilter(KeyEvent.KEY_TYPED, inputListener); - sceneController.toggle(Toggler.ToggleableComponent.MAIN_MENU); + primaryStage.addEventFilter(KeyEvent.ANY, keymapDisplay); primaryStage.setResizable(false); + primaryStage.setMinWidth(UIConstants.CANVAS_SIZE); + primaryStage.setMinHeight(UIConstants.CANVAS_SIZE); primaryStage.show(); } - } DELETED src/main/java/com/mg105/entities/GiveTutorial.java Index: src/main/java/com/mg105/entities/GiveTutorial.java ================================================================== --- src/main/java/com/mg105/entities/GiveTutorial.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.mg105.entities; - - -import com.mg105.utils.TutorialTexts; - -/** - * Data for what actions player has completed, contains methods for mutating the data - */ -public class GiveTutorial { - - private boolean moved; - private boolean attacked; - private boolean usedItem; - - /** - * Constructor for GiveTutorial entity - * - * @param moved whether player has moved - * @param attacked whether player has attacked - * @param usedItem whether player has opened a chest - */ - public GiveTutorial(boolean moved, boolean attacked, boolean usedItem) { - this.moved = moved; - this.attacked = attacked; - this.usedItem = usedItem; - } - - /** - * Set moved, attacked, usedItem to true if they have been performed by player - *

- * action should be a valid action - * - * @param action the action that has been performed - */ - public void actionPerformedSetter(String action) { - if (action.equalsIgnoreCase(TutorialTexts.MOVED)) { - this.moved = true; - } else if (action.equalsIgnoreCase(TutorialTexts.ATTACKED)) { - this.attacked = true; - } else if (action.equalsIgnoreCase(TutorialTexts.USED_ITEM)) { - this.usedItem = true; - } - - } - - /** - * Get if player has moved, attacked, and usedItem. - *

- * action should be a valid action - * - * @param action the action that is checked - * @return whether the player has performed each action - */ - public boolean actionPerformedGetter(String action) { - if (action.equalsIgnoreCase(TutorialTexts.MOVED)) { - return this.moved; - } else if (action.equalsIgnoreCase(TutorialTexts.ATTACKED)) { - return this.attacked; - } else if (action.equalsIgnoreCase(TutorialTexts.USED_ITEM)) { - return this.usedItem; - } else { - return false; - } - } - -} ADDED src/main/java/com/mg105/interface_adapters/InputControllable.java Index: src/main/java/com/mg105/interface_adapters/InputControllable.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/interface_adapters/InputControllable.java @@ -0,0 +1,13 @@ +package com.mg105.interface_adapters; + +/** + * InputControllable represents something that can be directly controlled by a keyboard input. + */ +public interface InputControllable { + /** + * Send key to this component. + * + * @param key the key this component should process. + */ + void interpret(String key); +} Index: src/main/java/com/mg105/interface_adapters/InputInterpreter.java ================================================================== --- src/main/java/com/mg105/interface_adapters/InputInterpreter.java +++ src/main/java/com/mg105/interface_adapters/InputInterpreter.java @@ -1,121 +1,117 @@ package com.mg105.interface_adapters; -import com.mg105.interface_adapters.tutorial.TutorialTextController; import com.mg105.use_cases.ChestInteractor; import com.mg105.use_cases.OpponentSetInteractor; import com.mg105.use_cases.map.CharacterMoverInterface; -import com.mg105.utils.TutorialTexts; import org.jetbrains.annotations.NotNull; import java.awt.*; +import java.util.Map; /** * InputInterpreter takes in keyboard inputs and distributes them to their appropriate use cases. */ public class InputInterpreter { private final @NotNull CharacterMoverInterface mover; private final @NotNull Toggler toggler; private final @NotNull ChestInteractor chestInteractor; - private final @NotNull TutorialTextController textChanger; - private final @NotNull OpponentSetInteractor opponentInteractor; + private final @NotNull InputControllable inventory; + private final @NotNull InputControllable walkMenu; + private final @NotNull InputControllable alerter; + private final @NotNull Map passthroughs; /** * Create a new InputInterpreter that translates keyboard inputs to appropriate function invocations. * * @param mover the character mover. * @param toggler the toggler used to change the displayed interface. - * @param textChanger the text controller for tutorial * @param chestInteractor the ChestInteractor used to interact with chests. * @param opponentInteractor the interactor used to interact with opponents. + * @param inventory the inventory. + * @param walkMenu the walking menu. + * @param alerter the alerter. + * @param passthroughs components that just get input directly pushed to them. */ - public InputInterpreter(@NotNull CharacterMoverInterface mover, @NotNull Toggler toggler, - @NotNull TutorialTextController textChanger, @NotNull ChestInteractor chestInteractor, - @NotNull OpponentSetInteractor opponentInteractor) { + public InputInterpreter(@NotNull CharacterMoverInterface mover, + @NotNull Toggler toggler, + @NotNull ChestInteractor chestInteractor, + @NotNull OpponentSetInteractor opponentInteractor, + @NotNull InputControllable inventory, + @NotNull InputControllable walkMenu, + @NotNull InputControllable alerter, + @NotNull Map passthroughs) { this.mover = mover; this.toggler = toggler; - this.textChanger = textChanger; this.opponentInteractor = opponentInteractor; this.chestInteractor = chestInteractor; + this.inventory = inventory; + this.walkMenu = walkMenu; + this.alerter = alerter; + this.passthroughs = passthroughs; } /** * Interpret key being pressed as an action. * * @param key the key being pressed as a string. */ public void interpret(String key) { - switch (toggler.getCurrentComponent()) { + // There is one ALWAYS GLOBAL keyboard mapping... 'k' to show the keyboard mappings. + if (key.equals("k")) { + toggler.toggle(Toggler.ToggleableComponent.CONTROLS); + return; + } + + Toggler.ToggleableComponent currentComponent = toggler.getCurrentComponent(); + + switch (currentComponent) { case MAP -> { switch (key) { - case "w" -> { - mover.generateMapMoveBy(new Point(0, -1)); - textChanger.getTutorial().setActionPerformed(TutorialTexts.MOVED); - } - case "a" -> { - mover.generateMapMoveBy(new Point(-1, 0)); - textChanger.getTutorial().setActionPerformed(TutorialTexts.MOVED); - } - case "s" -> { - mover.generateMapMoveBy(new Point(0, 1)); - textChanger.getTutorial().setActionPerformed(TutorialTexts.MOVED); - } - case "d" -> { - mover.generateMapMoveBy(new Point(1, 0)); - textChanger.getTutorial().setActionPerformed(TutorialTexts.MOVED); - } - - case "k" -> { - toggler.toggle(Toggler.ToggleableComponent.TUTORIAL); - textChanger.setShowControls(true); - - } - case "t" -> { - toggler.toggle(Toggler.ToggleableComponent.TUTORIAL); - textChanger.setChangeText(); - } - case "e" -> { - chestInteractor.getChestItem(); - // tutorial only cares that you pick up item, not that you use it - textChanger.getTutorial().setActionPerformed(TutorialTexts.USED_ITEM); - } + case "w" -> mover.generateMapMoveBy(new Point(0, -1)); + case "a" -> mover.generateMapMoveBy(new Point(-1, 0)); + case "s" -> mover.generateMapMoveBy(new Point(0, 1)); + case "d" -> mover.generateMapMoveBy(new Point(1, 0)); + case "e" -> chestInteractor.getChestItem(); case "i" -> toggler.toggle(Toggler.ToggleableComponent.INVENTORY); - case " " -> - //There is a warning if curly brackets are used on this block. - // I don't know what is correct to do in this situation. - toggler.toggle(Toggler.ToggleableComponent.WALK_MENU); - + case "c" -> toggler.toggle(Toggler.ToggleableComponent.WALK_MENU); case "f" -> { if (opponentInteractor.setOpponentSet()) { toggler.toggle(Toggler.ToggleableComponent.BATTLE); - textChanger.getTutorial().setActionPerformed(TutorialTexts.ATTACKED); } } case "m" -> toggler.toggle(Toggler.ToggleableComponent.MINIMAP); } } - case TUTORIAL -> { - switch (key) { - case "w", "a", "s", "d" -> { - toggler.toggle(Toggler.ToggleableComponent.TUTORIAL); - textChanger.setChangeText(); - } - case "k" -> textChanger.setShowControls(true); - } - } - - case WALK_MENU -> { - if (key.equals(" ")) { - toggler.toggle(Toggler.ToggleableComponent.WALK_MENU); - } - } - case MINIMAP -> toggler.toggle(Toggler.ToggleableComponent.MINIMAP); + case WALK_MENU -> { + if (key.equals("c")) { + toggler.toggle(Toggler.ToggleableComponent.WALK_MENU); + } else { + walkMenu.interpret(key); + } + } + case MINIMAP -> { + if (key.equals("m")) { + toggler.toggle(Toggler.ToggleableComponent.MINIMAP); + } + } case INVENTORY -> { if (key.equals("i")) { toggler.toggle(Toggler.ToggleableComponent.INVENTORY); + } else { + inventory.interpret(key); + } + } + case ALERT -> { + if (key.equals(" ")) { + alerter.interpret(key); + } + } + default -> { + if (passthroughs.containsKey(currentComponent)) { + passthroughs.get(currentComponent).interpret(key); } } } - } } ADDED src/main/java/com/mg105/interface_adapters/PartyGetter.java Index: src/main/java/com/mg105/interface_adapters/PartyGetter.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/interface_adapters/PartyGetter.java @@ -0,0 +1,64 @@ +package com.mg105.interface_adapters; + +import com.mg105.use_cases.PartyStatGetter; +import org.jetbrains.annotations.NotNull; + +/** + * Get information about a party member. + */ +public class PartyGetter { + private final @NotNull PartyStatGetter getter; + + /** + * Create a new PartyGetter. + * + * @param getter the getter used to access information. + */ + public PartyGetter(@NotNull PartyStatGetter getter) { + this.getter = getter; + } + + /** + * Get if name is fainted. + * + * @param name the party member's name. + * + * @return if name is fainted. + */ + public boolean isFainted(@NotNull String name) { + return getter.isFainted(name); + } + + /** + * Get the HP of a party member. + * + * @param name the party member's name. + * + * @return the HP of a party member. + */ + public int getHp(@NotNull String name) { + return getter.getHp(name); + } + + /** + * Get the maximum HP of a party member. + * + * @param name the party member's name. + * + * @return the maximum HP of a party member. + */ + public int getMaxHp(@NotNull String name) { + return getter.getMaxHp(name); + } + + /** + * Get the Dmg of a party member. + * + * @param name the party member's name. + * + * @return the Dmg of a party member. + */ + public int getDmg(@NotNull String name) { + return getter.getDmg(name); + } +} Index: src/main/java/com/mg105/interface_adapters/Toggler.java ================================================================== --- src/main/java/com/mg105/interface_adapters/Toggler.java +++ src/main/java/com/mg105/interface_adapters/Toggler.java @@ -22,10 +22,16 @@ /** * All the possible components that could theoretically be toggled. */ enum ToggleableComponent { + /** + * An alert is visible. + *

+ * Note: in practice this one should not be directly toggled. + */ + ALERT, /** * The main menu */ MAIN_MENU, /** @@ -43,14 +49,10 @@ /** * The battle menu */ BATTLE, /** - * The tutorial window - */ - TUTORIAL, - /** * The character selection screen. */ WALK_MENU, /** * The game Lose menu @@ -57,9 +59,16 @@ */ LOSE_MENU, /** * The game Win menu */ - WIN_MENU - + WIN_MENU, + /** + * The intro message + */ + INTRO, + /** + * The keyboard layout + */ + CONTROLS } } Index: src/main/java/com/mg105/interface_adapters/battle/BattlePresenter.java ================================================================== --- src/main/java/com/mg105/interface_adapters/battle/BattlePresenter.java +++ src/main/java/com/mg105/interface_adapters/battle/BattlePresenter.java @@ -112,10 +112,17 @@ * @return a String of the name of the moving character */ public String roundStart() { return interactor.roundStart(); } + + /** + * Have the opponents apply an attack, if necessary. + */ + public void applyOpponentAttack() { + interactor.applyOpponentAttack(); + } /** * Returns the name of every BattleCharacter which can be targeted by the given move. * Note: Function should only be called from view when caster is friendly, so method does not accommodate for case * where caster is an opponent. Index: src/main/java/com/mg105/interface_adapters/inventory/InventoryPresenter.java ================================================================== --- src/main/java/com/mg105/interface_adapters/inventory/InventoryPresenter.java +++ src/main/java/com/mg105/interface_adapters/inventory/InventoryPresenter.java @@ -50,11 +50,11 @@ * @see InventoryViewInterface */ @Override public void addItem(boolean isSuccessful, ItemDetails itemDetails) { if (isSuccessful) { - this.display.alert("Successfully added a " + itemDetails.getName() + "."); + this.display.alert("Successfully added a(n) " + itemDetails.getName() + "."); return; } this.display.alert(itemDetails.getName() + " could not be added. The inventory might be full, try removing an item."); @@ -71,16 +71,16 @@ */ @Override public void removeItem(boolean isSuccessful, ItemDetails itemDetails) { if (!isSuccessful) { - this.display.alert("Could not remove a " + itemDetails.getDescription() + "."); + this.display.alert("Could not remove a(n) " + itemDetails.getDescription() + "."); return; } - this.display.alert("Successfully removed a " + itemDetails.getName() + "."); + this.display.alert("Successfully removed a(n) " + itemDetails.getName() + "."); if (itemDetails.getCount() == 0) { this.display.removeItemView(itemDetails.getName()); return; Index: src/main/java/com/mg105/interface_adapters/map/RoomInterpreter.java ================================================================== --- src/main/java/com/mg105/interface_adapters/map/RoomInterpreter.java +++ src/main/java/com/mg105/interface_adapters/map/RoomInterpreter.java @@ -28,69 +28,47 @@ * @return the room as a 2 dimension array of TileType representing the current state of the room. Note that (0, 0) * represents the top-left corner and (MapConstants.ROOM_SIZE, MapConstants.ROOM_SIZE) represents the bottom * right corner. */ @Override - public RoomTileType[][] getCurrentRoom() { - RoomTileType[][] canvas = new RoomTileType[MapConstants.ROOM_SIZE][MapConstants.ROOM_SIZE]; - - for (int y = 1; y < MapConstants.ROOM_SIZE - 1; y++) { - for (int x = 1; x < MapConstants.ROOM_SIZE - 1; x++) { - canvas[y][x] = RoomTileType.FLOOR; - } - } + public @NotNull RoomState getCurrentRoom() { + RoomTileType[][] baseLayer = new RoomTileType[MapConstants.ROOM_SIZE + 1][MapConstants.ROOM_SIZE]; + RoomTileType[][] offsetLayer = new RoomTileType[MapConstants.ROOM_SIZE + 1][MapConstants.ROOM_SIZE]; for (int i = 0; i < MapConstants.ROOM_SIZE; i++) { - canvas[i][MapConstants.ROOM_SIZE - 1] = RoomTileType.WALL; - canvas[i][0] = RoomTileType.WALL; + offsetLayer[i][MapConstants.ROOM_SIZE - 1] = RoomTileType.WALL; + offsetLayer[i][0] = RoomTileType.WALL; + offsetLayer[0][i] = RoomTileType.WALL; + offsetLayer[MapConstants.ROOM_SIZE - 1][i] = RoomTileType.WALL; + + offsetLayer[MapConstants.ROOM_SIZE][i] = RoomTileType.WALL_FACE; } for (int i = 1; i < MapConstants.ROOM_SIZE - 1; i++) { - canvas[MapConstants.ROOM_SIZE - 1][i] = RoomTileType.WALL_WITH_FACE; - canvas[0][i] = RoomTileType.WALL_WITH_FACE; + offsetLayer[1][i] = RoomTileType.WALL_FACE; } - canvas[MapConstants.ROOM_SIZE - 1][0] = RoomTileType.WALL_WITH_FACE; - canvas[MapConstants.ROOM_SIZE - 1][MapConstants.ROOM_SIZE - 1] = RoomTileType.WALL_WITH_FACE; RoomLayout room = getter.getCurrentRoomLayout(); for (Point doorway : room.getDoorways()) { - canvas[doorway.y][doorway.x] = RoomTileType.EXIT; if (doorway.x == 0 || doorway.x == MapConstants.ROOM_SIZE - 1) { - canvas[doorway.y - 1][doorway.x] = RoomTileType.WALL_WITH_FACE; + offsetLayer[doorway.y][doorway.x] = RoomTileType.WALL_FACE; + } else { + offsetLayer[doorway.y][doorway.x] = null; + offsetLayer[doorway.y + 1][doorway.x] = null; } } for (Point chest : room.getClosedChests()) { - canvas[chest.y][chest.x] = RoomTileType.CHEST; + baseLayer[chest.y][chest.x] = RoomTileType.CHEST; } for (Point chest : room.getOpenChests()) { - canvas[chest.y][chest.x] = RoomTileType.CHEST_OPEN; + baseLayer[chest.y][chest.x] = RoomTileType.CHEST_OPEN; } for (Point opponents : room.getOpponents()) { - canvas[opponents.y][opponents.x] = RoomTileType.OPPONENT_SET; - } - - return canvas; - } - - /** - * Get the current player position in the room. - * - * @return the current player position in the room. - */ - @Override - public @NotNull Point getPlayer() { - return getter.getCurrentRoomLayout().getPlayer(); - } - - /** - * Retrieves the sprite String currently associated with the WalkingCharacter. - * - * @return a file name/location as a String for the desired character sprite. - */ - @Override - public @NotNull String getCharacterSprite() { - return this.getter.getWalkingSprite(); + baseLayer[opponents.y][opponents.x] = RoomTileType.OPPONENT_SET; + } + + return new RoomState(baseLayer, offsetLayer, room.getPlayer(), getter.getWalkingSprite()); } } Index: src/main/java/com/mg105/interface_adapters/map/RoomInterpreterInterface.java ================================================================== --- src/main/java/com/mg105/interface_adapters/map/RoomInterpreterInterface.java +++ src/main/java/com/mg105/interface_adapters/map/RoomInterpreterInterface.java @@ -1,31 +1,15 @@ package com.mg105.interface_adapters.map; import org.jetbrains.annotations.NotNull; -import java.awt.*; - /** * Layout the current room. */ public interface RoomInterpreterInterface { /** - * Lay out the current room as a square of tiles to be displayed. + * Get the current room state. * * @return the state of the current room. */ - RoomTileType[][] getCurrentRoom(); - - /** - * Get the position of the player within the room. - * - * @return the position of the player within the room. - */ - @NotNull Point getPlayer(); - - /** - * Get the path of the current character sprite. - * - * @return the path of the current character sprite - */ - @NotNull String getCharacterSprite(); + @NotNull RoomState getCurrentRoom(); } ADDED src/main/java/com/mg105/interface_adapters/map/RoomState.java Index: src/main/java/com/mg105/interface_adapters/map/RoomState.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/interface_adapters/map/RoomState.java @@ -0,0 +1,73 @@ +package com.mg105.interface_adapters.map; + +import org.jetbrains.annotations.NotNull; + +import java.awt.*; + +/** + * RoomState is a easy-to-draw representation of the state of the current room. + */ +public class RoomState { + private final @NotNull RoomTileType[][] baseLayer; + private final @NotNull RoomTileType[][] offsetLayer; + private final @NotNull Point playerPosition; + private final @NotNull String playerSprite; + + /** + * Create a new RoomState. + * + * @param baseLayer the base layer (chests, battles) + * @param offsetLayer the wall and decoration layer (walls, wall faces) + * @param playerPosition the position of the player in the room. + * @param playerSprite the player sprite. + */ + public RoomState(@NotNull RoomTileType[][] baseLayer, + @NotNull RoomTileType[][] offsetLayer, + @NotNull Point playerPosition, + @NotNull String playerSprite) { + this.baseLayer = baseLayer; + this.offsetLayer = offsetLayer; + this.playerPosition = playerPosition; + this.playerSprite = playerSprite; + } + + /** + * Get the representation of the contents of the room. + *

+ * Note the return value is of size MapConstants.MAP_SIZE x MapConstants.MAP_SIZE+1. + * + * @return a 2D representation of the contents of the room. + */ + public @NotNull RoomTileType[][] getBaseLayer() { + return baseLayer; + } + + /** + * Get a representation of the border of the room + *

+ * Note the return value is of size MapConstants.MAP_SIZE x MapConstants.MAP_SIZE+1. + * + * @return a representation of the border of the room. + */ + public @NotNull RoomTileType[][] getOffsetLayer() { + return offsetLayer; + } + + /** + * Get the player position in the room. + * + * @return the player position in the room. + */ + public @NotNull Point getPlayerPosition() { + return playerPosition; + } + + /** + * Get the player sprite. + * + * @return the player sprite. + */ + public @NotNull String getPlayerSprite() { + return playerSprite; + } +} Index: src/main/java/com/mg105/interface_adapters/map/RoomTileType.java ================================================================== --- src/main/java/com/mg105/interface_adapters/map/RoomTileType.java +++ src/main/java/com/mg105/interface_adapters/map/RoomTileType.java @@ -3,25 +3,17 @@ /** * Types of tiles that can appear within a room. */ public enum RoomTileType { /** - * Empty floor that can be walked on - */ - FLOOR, - /** * Wall that cannot be walked on. Mainly used for the border */ WALL, /** - * A wall where you can see the side + * The isometric wall face. */ - WALL_WITH_FACE, - /** - * A doorway - */ - EXIT, + WALL_FACE, /** * A treasure chest */ CHEST, /** DELETED src/main/java/com/mg105/interface_adapters/tutorial/TutorialTextController.java Index: src/main/java/com/mg105/interface_adapters/tutorial/TutorialTextController.java ================================================================== --- src/main/java/com/mg105/interface_adapters/tutorial/TutorialTextController.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.mg105.interface_adapters.tutorial; - -import com.mg105.use_cases.PlayerGetsTutorial; -import com.mg105.utils.TutorialTexts; - -import java.util.List; - -/** - * A controller that converts phase number into displayed text - */ -public class TutorialTextController { - - private final PlayerGetsTutorial tutorial = new PlayerGetsTutorial(TutorialTexts.PHASES, 0); - private boolean changeText; - private boolean showControls = false; - - /** - * A constructor for the tutorial controller - * - * @param changeText whether to initially change the text. - */ - public TutorialTextController(boolean changeText) { - this.changeText = changeText; - } - - /** - * Gets the current text that should be displayed - * - * @return what the text displayed at the bottom of the screen should be - */ - public String bottomText() { - return tutorial.allPhases().get(tutorial.currentPhase()); - } - - /** - * Go to the next tutorial phase - */ - public void nextPhase() { - this.tutorial.nextPhase(); - } - - /** - * Make text start changing - */ - public void setChangeText() { - this.changeText = !this.changeText; - } - - /** - * Check if tutorial phases should advance - * - * @return if text should start changing - */ - public boolean changeText() { - return this.changeText; - } - - /** - * Check if player should be shown controls again - * - * @return whether player should be shown the control texts - */ - public boolean getShowControls() { - return this.showControls; - } - - /** - * Tell player the controls again - * - * @param show the text on the screen when true - */ - public void setShowControls(boolean show) { - this.showControls = show; - } - - /** - * Get an instance of the PlayerGetsTutorial use case - * - * @return the tutorial instance - */ - public PlayerGetsTutorial getTutorial() { - return this.tutorial; - } - - /** - * Get if the tutorial is complete, changes text if it is - * - * @return if the tutorial is complete - */ - public boolean isComplete() { - return tutorial.isComplete(); - } - - /** - * Returns if the action has been performed - * - * @param action that is checked - * @return if the specified action has been performed - */ - public boolean getActionPerformed(String action) { - return !tutorial.getActionPerformed(action); - } - - /** - * Get names of all phases of tutorial - * - * @return the list of all tutorial phases - */ - public List allPhases() { - return tutorial.allPhases(); - } - -} - ADDED src/main/java/com/mg105/use_cases/PartyStatGetter.java Index: src/main/java/com/mg105/use_cases/PartyStatGetter.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/use_cases/PartyStatGetter.java @@ -0,0 +1,64 @@ +package com.mg105.use_cases; + +import com.mg105.entities.GameState; +import org.jetbrains.annotations.NotNull; + +/** + * Get ad hoc information about party members. + */ +public class PartyStatGetter { + private final @NotNull GameState state; + + /** + * Create a new PartyStatGetter. + * + * @param state the game state that stores the data. + */ + public PartyStatGetter(@NotNull GameState state) { + this.state = state; + } + + /** + * Get if name is fainted. + * + * @param name the party member's name. + * + * @return if name is fainted. + */ + public boolean isFainted(@NotNull String name) { + return state.getFaintedPartyMember(name) != null; + } + + /** + * Get the HP of a party member. + * + * @param name the party member's name. + * + * @return the HP of a party member. + */ + public int getHp(@NotNull String name) { + return state.getPartyMember(name).getHp(); + } + + /** + * Get the maximum HP of a party member. + * + * @param name the party member's name. + * + * @return the maximum HP of a party member. + */ + public int getMaxHp(@NotNull String name) { + return state.getPartyMember(name).getMaxHp(); + } + + /** + * Get the Dmg of a party member. + * + * @param name the party member's name. + * + * @return the Dmg of a party member. + */ + public int getDmg(@NotNull String name) { + return state.getPartyMember(name).getDmg(); + } +} DELETED src/main/java/com/mg105/use_cases/PlayerGetsTutorial.java Index: src/main/java/com/mg105/use_cases/PlayerGetsTutorial.java ================================================================== --- src/main/java/com/mg105/use_cases/PlayerGetsTutorial.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.mg105.use_cases; - -import com.mg105.entities.GiveTutorial; -import com.mg105.utils.TutorialTexts; - -import java.util.List; - -/** - * Class for determining what phase of the tutorial the player is on, and changing the phase - */ -public class PlayerGetsTutorial { - private final List tutorialPhases; // Go through multiple phases of tutorial in order - private final GiveTutorial tutorial; - private int currentPhase; - - - /** - * Constructor for PlayerGetsTutorial use case - * - * @param tutorialPhases a list of all possible phases in the tutorial - * @param currentPhase the integer representing what phase the player is on in the tutorial - */ - public PlayerGetsTutorial(List tutorialPhases, int currentPhase) { - this.tutorialPhases = tutorialPhases; - this.currentPhase = currentPhase; - this.tutorial = new GiveTutorial(false, false, false); - } - - /** - * Check if all required actions performed by player - * - * @return whether all actions are complete - */ - public boolean isComplete() { - return tutorial.actionPerformedGetter(TutorialTexts.MOVED) & tutorial.actionPerformedGetter(TutorialTexts.ATTACKED) & tutorial.actionPerformedGetter(TutorialTexts.USED_ITEM); - } - - /** - * Get names of all phases of tutorial - * - * @return the list of all tutorial phases - */ - public List allPhases() { - return this.tutorialPhases; - } - - /** - * Get current phase of tutorial, which is first index of phase list - * - * @return the current phase of tutorial - */ - public int currentPhase() { - return this.currentPhase; - } - - /** - * Advance current phase by 1 - */ - public void nextPhase() { - if (currentPhase < TutorialTexts.PHASES.size() - 1) { - this.currentPhase++; - } - } - - /** - * Set the action to true if it has been performed - * - * @param action to set to performed - */ - public void setActionPerformed(String action) { - this.tutorial.actionPerformedSetter(action); - } - - /** - * Check if specific action has been performed - * - * @param action get if it has been performed yet - * @return if the action has been performed - */ - public boolean getActionPerformed(String action) { - return this.tutorial.actionPerformedGetter(action); - } - -} - Index: src/main/java/com/mg105/use_cases/ReplayGenerator.java ================================================================== --- src/main/java/com/mg105/use_cases/ReplayGenerator.java +++ src/main/java/com/mg105/use_cases/ReplayGenerator.java @@ -1,11 +1,10 @@ package com.mg105.use_cases; import com.mg105.entities.BattleCharacter; import com.mg105.entities.GameState; import com.mg105.entities.items.MegaPotion; -import com.mg105.use_cases.map.MapGenerator; import com.mg105.utils.PartyConstants; import org.jetbrains.annotations.NotNull; /** @@ -80,14 +79,10 @@ state.getFainted().removeAll(state.getParty()); this.inventoryClean(); this.attributeInheritance(); - // incomplete remake, need to amend later. exactly implementation should depend - // on other use cases' implementation - MapGenerator isekai = new MapGenerator(state); - isekai.generateMap(); for (Resetable resetable : resetables) { resetable.reset(); } } } Index: src/main/java/com/mg105/use_cases/battle/BattleInteractor.java ================================================================== --- src/main/java/com/mg105/use_cases/battle/BattleInteractor.java +++ src/main/java/com/mg105/use_cases/battle/BattleInteractor.java @@ -24,10 +24,11 @@ private final InventoryInteractor inventoryInteractor; private final Saver saver; private BattlePresenterInterface presenter; + private BattleCharacter moving; /** * Creates a new BattleInteractor with a reference to the GameState. * * @param state the GameState to be referred to. @@ -67,10 +68,12 @@ } for (int i = 0; i < opponents.size(); ++i) { opponentNames[i] = opponents.get(i).getName(); } + + moving = null; presenter.setViewNames(partyNames, opponentNames); } /** @@ -148,49 +151,57 @@ * returns null iff the battle has ended * * @return a String of the name of the moving character */ public String roundStart() { + moving = null; + int status = getBattleStatus(); if (Math.abs(status) == 1) { //Player either lost or won the battle return null; } else { //Battle is ongoing - BattleCharacter moving = state.getCurrEncounter().getMovingCharacter(); - - if (moving.isOpponent()) { //Opponent character is moving - Random rand = new Random(); - int moveNumber = rand.nextInt(2); - - Move chosenMove; - if (moveNumber == 0) { - chosenMove = moving.getMoveOne(); - } else { - chosenMove = moving.getMoveTwo(); - } - - if (!chosenMove.isFriendly()) { - ArrayList players = state.getCurrEncounter().getPlayerCharacters(); - - //Choose a random player character to attack - int target = rand.nextInt(players.size()); - - this._useMove(chosenMove, moving, players.get(target)); - } else { - ArrayList opponents = state.getCurrEncounter().getOpponents(); - - //Choose a random opponent to use the friendly move on - int target = rand.nextInt(opponents.size()); - - this._useMove(chosenMove, moving, opponents.get(target)); - } - } + moving = state.getCurrEncounter().getMovingCharacter(); //No matter what, the moving BattleCharacter's name is returned return moving.getName(); } } + + /** + * Have the opponents apply an attack. + */ + public void applyOpponentAttack() { + assert moving != null; + assert moving.isOpponent(); + + Random rand = new Random(); + int moveNumber = rand.nextInt(2); + + Move chosenMove; + if (moveNumber == 0) { + chosenMove = moving.getMoveOne(); + } else { + chosenMove = moving.getMoveTwo(); + } + + if (!chosenMove.isFriendly()) { + ArrayList players = state.getCurrEncounter().getPlayerCharacters(); + + //Choose a random player character to attack + int target = rand.nextInt(players.size()); + + this._useMove(chosenMove, moving, players.get(target)); + } else { + ArrayList opponents = state.getCurrEncounter().getOpponents(); + + //Choose a random opponent to use the friendly move on + int target = rand.nextInt(opponents.size()); + + this._useMove(chosenMove, moving, opponents.get(target)); + } + } /** * Returns the name of every BattleCharacter which can be targeted by the given move. * Note: Function should only be called from view when caster is friendly, so method does not accommodate for case * where caster is an opponent. DELETED src/main/java/com/mg105/user_interface/AlertBox.java Index: src/main/java/com/mg105/user_interface/AlertBox.java ================================================================== --- src/main/java/com/mg105/user_interface/AlertBox.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.mg105.user_interface; - -import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.stage.Modality; -import javafx.stage.Stage; - -/** - * AlertBox is a component that creates a new window and display a users a message. - * This message is usually related to if an action the user performed was successful or not. - */ -public class AlertBox { - - - /** - * Displays a modal that must be resolved before switches to a different window - * - * @param msg the message to display` - */ - - public void display(String msg) { - Stage window = new Stage(); - window.initModality(Modality.APPLICATION_MODAL); - window.setTitle("Alert"); - window.setHeight(400); - window.setWidth(400); - window.setResizable(false); - Label label = new Label(); - label.setText(msg); - Button close = new Button("Close"); - close.setOnAction(e -> window.close()); - - VBox layout = new VBox(10); - layout.getChildren().addAll(label, close); - layout.setAlignment(Pos.CENTER); - Scene scene = new Scene(layout, Color.LIGHTBLUE); - window.setScene(scene); - window.showAndWait(); - } -} ADDED src/main/java/com/mg105/user_interface/Alerter.java Index: src/main/java/com/mg105/user_interface/Alerter.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/user_interface/Alerter.java @@ -0,0 +1,16 @@ +package com.mg105.user_interface; + +import javafx.scene.Parent; +import org.jetbrains.annotations.NotNull; + +/** + * An Alerter can display and handle alerts (modal dialogues). + */ +public interface Alerter { + /** + * Display an alert. + * + * @param body the actual alert. + */ + void displayAlert(@NotNull Parent body); +} Index: src/main/java/com/mg105/user_interface/BattleMenu.java ================================================================== --- src/main/java/com/mg105/user_interface/BattleMenu.java +++ src/main/java/com/mg105/user_interface/BattleMenu.java @@ -1,146 +1,144 @@ package com.mg105.user_interface; +import com.mg105.interface_adapters.InputControllable; import com.mg105.interface_adapters.battle.BattleMenuInterface; import com.mg105.interface_adapters.battle.BattlePresenter; +import com.mg105.utils.PartyConstants; +import com.mg105.utils.UIConstants; +import javafx.animation.FadeTransition; +import javafx.animation.TranslateTransition; import javafx.event.ActionEvent; -import javafx.event.EventHandler; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; +import javafx.scene.Group; import javafx.scene.control.Label; -import javafx.scene.layout.GridPane; +import javafx.scene.layout.*; +import javafx.util.Duration; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; +import java.util.Arrays; /** * This class uses JavaFX and is displayed during an active battle. */ -public class BattleMenu implements EventHandler, BattleMenuInterface, Toggleable { - - private final BattlePresenter presenter; - private final String[] playerNames = new String[4]; - private final int[] playerHealth = new int[4]; - private final int[] playerDmg = new int[4]; - private final String[] opponentNames = new String[4]; - private final int[] opponentHealth = new int[4]; - private final int[] opponentDmg = new int[4]; - - private final Label p0 = new Label(); - private final Label p1 = new Label(); - private final Label p2 = new Label(); - private final Label p3 = new Label(); - - private final Label o0 = new Label(); - private final Label o1 = new Label(); - private final Label o2 = new Label(); - private final Label o3 = new Label(); - - private final Button nextRound; - - private final Button forfeit; - private final Button moveOne; - private final Button moveTwo; - - private final Button targetP0; - private final Button targetP1; - private final Button targetP2; - private final Button targetP3; - private final Button targetO0; - private final Button targetO1; - private final Button targetO2; - private final Button targetO3; - private final GridPane grid; - private final Scene scene; - private String moving; - private int moveNum; +public class BattleMenu implements InputControllable, BattleMenuInterface, Toggleable { + private final @NotNull BattlePresenter presenter; + private final @NotNull StandardComponentFactory componentFactory; + + private final @NotNull Label[] opponentNames; + private final @NotNull Label[] opponentHps; + private final @NotNull Label[] opponentDmgs; + + private final @NotNull Label[] playerNames; + private final @NotNull Label[] playerHps; + private final @NotNull Label[] playerDmgs; + + private final @NotNull Label turnHint; + private final @NotNull Label actionHint; + private final @NotNull MultiselectGrid options; + + private final @NotNull StackPane layoutWrapper; + private final @NotNull Group effectLayer; + + private boolean needsSelection; + private String mover; + private int selectedMove; + private String selectedTarget; /** * Creates a new BattleMenu. * Sets every UI element, except for labels which are set when the view is toggled on. * - * @param battlePres the BattlePresenter to refer to (following MVP pattern) + * @param battlePres the BattlePresenter to refer to (following MVP pattern) + * @param componentFactory the component factory to use. */ - public BattleMenu(BattlePresenter battlePres) { + public BattleMenu(@NotNull BattlePresenter battlePres, @NotNull StandardComponentFactory componentFactory) { this.presenter = battlePres; presenter.setView(this); - grid = new GridPane(); - grid.setVgap(10); - grid.setHgap(10); - grid.setAlignment(Pos.CENTER); - - nextRound = new Button("Next Round"); - nextRound.setId("Next Round"); - nextRound.setOnAction(this); - grid.add(nextRound, 10, 30); - - forfeit = new Button("Forfeit"); - forfeit.setId("Forfeit"); - forfeit.setOnAction(this); - grid.add(forfeit, 10, 2); - - grid.add(p0, 1, 4); - grid.add(p1, 1, 8); - grid.add(p2, 1, 12); - grid.add(p3, 1, 16); - - grid.add(o0, 18, 4); - grid.add(o1, 18, 8); - grid.add(o2, 18, 12); - grid.add(o3, 18, 16); - - targetP0 = new Button("TARGET"); - targetP0.setOnAction(this); - targetP0.setVisible(false); - grid.add(targetP0, 0, 4); - - targetP1 = new Button("TARGET"); - targetP1.setOnAction(this); - targetP1.setVisible(false); - grid.add(targetP1, 0, 8); - - targetP2 = new Button("TARGET"); - targetP2.setOnAction(this); - targetP2.setVisible(false); - grid.add(targetP2, 0, 12); - - targetP3 = new Button("TARGET"); - targetP3.setOnAction(this); - targetP3.setVisible(false); - grid.add(targetP3, 0, 16); - - targetO0 = new Button("TARGET"); - targetO0.setOnAction(this); - grid.add(targetO0, 19, 4); - targetO0.setVisible(false); - - targetO1 = new Button("TARGET"); - targetO1.setOnAction(this); - targetO1.setVisible(false); - grid.add(targetO1, 19, 8); - - targetO2 = new Button("TARGET"); - targetO2.setOnAction(this); - targetO2.setVisible(false); - grid.add(targetO2, 19, 12); - - targetO3 = new Button("TARGET"); - targetO3.setOnAction(this); - targetO3.setVisible(false); - grid.add(targetO3, 19, 16); - - moveOne = new Button(""); - moveOne.setOnAction(this); - moveOne.setVisible(false); - - moveTwo = new Button(""); - moveTwo.setOnAction(this); - moveTwo.setVisible(false); - - scene = new Scene(grid, 800, 800); + this.componentFactory = componentFactory; + + opponentNames = new Label[4]; + opponentHps = new Label[4]; + opponentDmgs = new Label[4]; + + playerNames = new Label[4]; + playerHps = new Label[4]; + playerDmgs = new Label[4]; + + for (int i = 0; i < 4; i++) { + opponentNames[i] = componentFactory.createSubtitleLabel("UNSET", true); + opponentHps[i] = componentFactory.createLabel("UNSET", true); + opponentDmgs[i] = componentFactory.createLabel("UNSET", true); + + playerNames[i] = componentFactory.createSubtitleLabel("UNSET", true); + playerHps[i] = componentFactory.createLabel("UNSET", true); + playerDmgs[i] = componentFactory.createLabel("UNSET", true); + } + + turnHint = componentFactory.createSubtitleLabel("UNSET", true); + actionHint = componentFactory.createSubtitleLabel("UNSET", true); + + HBox hintSlot = new HBox(); + hintSlot.getChildren().addAll(turnHint, actionHint); + + options = componentFactory.createMultiselectGrid(); + + Region optionsSpacer = componentFactory.createFixedHalfSpacer(); + optionsSpacer.setPrefSize(0, 11 * UIConstants.SINGLE_GAP); + + HBox optionsSlot = new HBox(); + optionsSlot.getChildren().addAll(optionsSpacer, options.getGrid()); + + VBox layout = new VBox(); + layout.setMinHeight(UIConstants.CANVAS_SIZE - 4 * UIConstants.SINGLE_GAP); // ???? + layout.getChildren().addAll( + makeBlock(opponentNames, opponentHps, opponentDmgs), + componentFactory.createSpacer(), + makeBlock(playerNames, playerHps, playerDmgs), + componentFactory.createSpacer(), + componentFactory.createSpacer(), + hintSlot, + componentFactory.createFixedHalfSpacer(), + optionsSlot + ); + + effectLayer = new Group(); + effectLayer.setManaged(false); + + layoutWrapper = new StackPane(); + layoutWrapper.setBackground(componentFactory.createBackground(true)); + layoutWrapper.setPadding(componentFactory.createInsets()); + layoutWrapper.setAlignment(Pos.CENTER); + layoutWrapper.getChildren().addAll(layout, effectLayer); + } + + /** + * Make a row of characters. + * + * @param names the names of the characters. + * @param hps the health values of the characters + * @param dmgs the damage values of the characters. + * + * @return a layout of a row of characters. + */ + private @NotNull HBox makeBlock(@NotNull Label[] names, @NotNull Label[] hps, @NotNull Label[] dmgs) { + HBox block = new HBox(); + for (int i = 0; i < names.length; i++) { + GridPane info = new GridPane(); + info.addRow(0, componentFactory.createLabel("HP: ", true), hps[i]); + info.addRow(1, componentFactory.createLabel("Dmg: ", true), dmgs[i]); + + VBox stack = new VBox(); + stack.getChildren().addAll(names[i], info); + + block.getChildren().addAll(stack, componentFactory.createSpacer()); + } + block.getChildren().remove(block.getChildren().size() - 1); + return block; } /** * Sets the names of the player and opponent characters participating in the active battle. * @@ -147,25 +145,13 @@ * @param playerNames array of name Strings representing player characters. * @param opponentNames array of name Strings representing opponents. */ @Override public void setNames(String[] playerNames, String[] opponentNames) { - for (int i = 0; i < 4; ++i) { - //Check if any player characters have fainted. Opponent set will always contain four characters initially. - if (playerNames.length <= i) { - this.playerNames[i] = "FAINTED"; - this.playerHealth[i] = 0; - this.playerDmg[i] = 0; - } else { - this.playerNames[i] = playerNames[i]; - this.playerHealth[i] = presenter.givenCharacterHealth(this.playerNames[i]); - this.playerDmg[i] = presenter.givenCharacterDamage(this.playerNames[i]); - } - - this.opponentNames[i] = opponentNames[i]; - this.opponentHealth[i] = presenter.givenCharacterHealth(this.opponentNames[i]); - this.opponentDmg[i] = presenter.givenCharacterDamage(this.opponentNames[i]); + for (int i = 0; i < this.playerNames.length; i++) { + this.playerNames[i].setText(PartyConstants.ALL_PARTY_MEMBER_NAMES[i]); // HACK: we shouldn't just ignore the playerNames parameter, but any discrepancy that this will cause will get instantly fixed somewhere else. + this.opponentNames[i].setText(opponentNames[i]); } } /** * Updates the display corresponding to the given affected character. @@ -172,37 +158,111 @@ * * @param character the character who needs to be updated on the screen. */ @Override public void updateCharacter(String character) { - if (playerNames[0].equals(character)) { - updateCharacterData(character, 0, p0, false); - } else if (playerNames[1].equals(character)) { - updateCharacterData(character, 1, p1, false); - } else if (playerNames[2].equals(character)) { - updateCharacterData(character, 2, p2, false); - } else if (playerNames[3].equals(character)) { - updateCharacterData(character, 3, p3, false); - } else if (opponentNames[0].equals(character)) { - updateCharacterData(character, 0, o0, true); - } else if (opponentNames[1].equals(character)) { - updateCharacterData(character, 1, o1, true); - } else if (opponentNames[2].equals(character)) { - updateCharacterData(character, 2, o2, true); - } else if (opponentNames[3].equals(character)) { - updateCharacterData(character, 3, o3, true); + for (int i = 0; i < playerNames.length; i++) { + if (playerNames[i].getText().equals(character)) { + updateCharacter(character, playerNames[i], playerHps[i], playerDmgs[i], true); + return; + } + + if (opponentNames[i].getText().equals(character)) { + updateCharacter(character, opponentNames[i], opponentHps[i], opponentDmgs[i], true); + return; + } + } + } + + /** + * Updates the information once the slot of the update is known. + * + * @param character the character to update. + * @param name the name location. + * @param hp the hp location. + * @param dmg the damage location. + * @param effect if an effect should be drawn to visually represent the change. + */ + private void updateCharacter(@NotNull String character, @NotNull Label name, + @NotNull Label hp, @NotNull Label dmg, boolean effect) { + int startHp = Integer.MIN_VALUE; + int startDmg = Integer.MIN_VALUE; + if (effect) { + startHp = Integer.parseInt(hp.getText()); + startDmg = Integer.parseInt(dmg.getText()); + } + + int deltaHp = Integer.MIN_VALUE; + int deltaDmg = Integer.MIN_VALUE; + + if (presenter.givenCharacterFainted(character)) { + name.setText("FAINTED"); + hp.setText("N/A"); + dmg.setText("N/A"); + + if (effect) { + deltaHp = -startHp; + deltaDmg = 0; + } + } else { + final int endHp = presenter.givenCharacterHealth(character); + final int endDmg = presenter.givenCharacterDamage(character); + + hp.setText(Integer.toString(endHp)); + dmg.setText(Integer.toString(endDmg)); + + if (effect) { + deltaHp = endHp - startHp; + deltaDmg = endDmg - startDmg; + } + } + + if (effect) { + Bounds anchorLocation = name.localToScene(name.getBoundsInLocal()); + if (anchorLocation == null) { + return; + } + + HBox effectBox = new HBox(); + effectBox.getChildren().addAll( + componentFactory.createNumberedLabel(deltaHp, true), + componentFactory.createFixedHalfSpacer(), + componentFactory.createNumberedLabel(deltaDmg, true) + ); + effectBox.setLayoutX(anchorLocation.getCenterX()); + effectBox.setLayoutY(anchorLocation.getMinY() - (int)(UIConstants.SINGLE_GAP / 2)); + effectBox.setTranslateX(0); + effectBox.setTranslateY(0); + + Duration duration = Duration.millis(1000); + + TranslateTransition translate = new TranslateTransition(); + translate.setDuration(duration); + translate.setByY(-UIConstants.SINGLE_GAP); + translate.setNode(effectBox); + + FadeTransition fade = new FadeTransition(); + fade.setDuration(duration); + fade.setFromValue(1); + fade.setToValue(0); + fade.setNode(effectBox); + fade.setOnFinished((ActionEvent event) -> effectLayer.getChildren().remove(effectBox)); + + translate.play(); + fade.play(); + effectLayer.getChildren().add(effectBox); } } /** * Get the scene of this toggleable object. It is this scene that will be displayed. * * @return the scene to be displayed. */ @Override - public @NotNull Scene getScene() { - return this.scene; + public @NotNull Pane getScene() { + return layoutWrapper; } /** * Set the visibility of this component. * @@ -210,272 +270,141 @@ * to do nothing on ANY user inputs. */ @Override public void toggle(boolean isVisible) { if (isVisible) { - presenter.startBattle(); //Will call setNames and update character data. - - setupCharacterLabel(p0, 0, false); - setupCharacterLabel(p1, 1, false); - setupCharacterLabel(p2, 2, false); - setupCharacterLabel(p3, 3, false); - - setupCharacterLabel(o0, 0, true); - setupCharacterLabel(o1, 1, true); - setupCharacterLabel(o2, 2, true); - setupCharacterLabel(o3, 3, true); - - nextRound.setDisable(false); - } - } - - /** - * Handles button events. - * NextRound will start the next round, which is handled in BattlePresenter. - * The two move buttons represent a move to be selected, and will cause target buttons to appear next to valid - * target characters and the move buttons to disappear. - * The target buttons only become visible when the character they represent can be targeted with the selected move, - * and pressing them will apply the move onto the character and make the target buttons disappear. - * - * @param event the event which occurred. - */ - @Override - public void handle(ActionEvent event) { - Object source = event.getSource(); - if (source.equals(nextRound)) { - nextRound.setDisable(true); - - moving = presenter.roundStart(); - - if (moving == null) { - //Battle ended - presenter.endBattle(); - return; - } - - //moving != null - if (playerNames[0].equals(moving)) { - int[] moveStats = presenter.givenCharacterMoveStats(moving); - String[] moveNames = presenter.givenCharacterMoveNames(moving); - - grid.add(moveOne, 2, 4); - moveOne.setVisible(true); - grid.add(moveTwo, 3, 4); - moveTwo.setVisible(true); - - moveOne.setText(moveNames[0] + "\n Hp: " + moveStats[0] + ", Dmg: " + moveStats[1]); - moveTwo.setText(moveNames[1] + "\n Hp: " + moveStats[2] + ", Dmg: " + moveStats[3]); - } else if (playerNames[1].equals(moving)) { - int[] moveStats = presenter.givenCharacterMoveStats(moving); - String[] moveNames = presenter.givenCharacterMoveNames(moving); - - grid.add(moveOne, 2, 8); - moveOne.setVisible(true); - grid.add(moveTwo, 3, 8); - moveTwo.setVisible(true); - - moveOne.setText(moveNames[0] + "\n Hp: " + moveStats[0] + ", Dmg: " + moveStats[1]); - moveTwo.setText(moveNames[1] + "\n Hp: " + moveStats[2] + ", Dmg: " + moveStats[3]); - } else if (playerNames[2].equals(moving)) { - int[] moveStats = presenter.givenCharacterMoveStats(moving); - String[] moveNames = presenter.givenCharacterMoveNames(moving); - - grid.add(moveOne, 2, 12); - moveOne.setVisible(true); - grid.add(moveTwo, 3, 12); - moveTwo.setVisible(true); - - moveOne.setText(moveNames[0] + "\n Hp: " + moveStats[0] + ", Dmg: " + moveStats[1]); - moveTwo.setText(moveNames[1] + "\n Hp: " + moveStats[2] + ", Dmg: " + moveStats[3]); - } else if (playerNames[3].equals(moving)) { - int[] moveStats = presenter.givenCharacterMoveStats(moving); - String[] moveNames = presenter.givenCharacterMoveNames(moving); - - grid.add(moveOne, 2, 16); - moveOne.setVisible(true); - grid.add(moveTwo, 3, 16); - moveTwo.setVisible(true); - - moveOne.setText(moveNames[0] + "\n Hp: " + moveStats[0] + ", Dmg: " + moveStats[1]); - moveTwo.setText(moveNames[1] + "\n Hp: " + moveStats[2] + ", Dmg: " + moveStats[3]); - } else if (opponentNames[0].equals(moving) || opponentNames[1].equals(moving) || - opponentNames[2].equals(moving) || opponentNames[3].equals(moving)) { - //If opponent moved, re-enable nextRound button - nextRound.setDisable(false); - } - } else if (source.equals(forfeit)) { - presenter.endBattle(); - } else if (source.equals(moveOne)) { - moveNum = 1; - displayTargets(); - } else if (source.equals(moveTwo)) { - moveNum = 2; - displayTargets(); - } else if (source.equals(targetP0)) { - targetP0.setVisible(false); - targetP1.setVisible(false); - targetP2.setVisible(false); - targetP3.setVisible(false); - - presenter.executeTurn(moveNum, moving, playerNames[0]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetP1)) { - targetP0.setVisible(false); - targetP1.setVisible(false); - targetP2.setVisible(false); - targetP3.setVisible(false); - - presenter.executeTurn(moveNum, moving, playerNames[1]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetP2)) { - targetP0.setVisible(false); - targetP1.setVisible(false); - targetP2.setVisible(false); - targetP3.setVisible(false); - - presenter.executeTurn(moveNum, moving, playerNames[2]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetP3)) { - targetP0.setVisible(false); - targetP1.setVisible(false); - targetP2.setVisible(false); - targetP3.setVisible(false); - - presenter.executeTurn(moveNum, moving, playerNames[3]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetO0)) { - targetO0.setVisible(false); - targetO1.setVisible(false); - targetO2.setVisible(false); - targetO3.setVisible(false); - - presenter.executeTurn(moveNum, moving, opponentNames[0]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetO1)) { - targetO0.setVisible(false); - targetO1.setVisible(false); - targetO2.setVisible(false); - targetO3.setVisible(false); - - presenter.executeTurn(moveNum, moving, opponentNames[1]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetO2)) { - targetO0.setVisible(false); - targetO1.setVisible(false); - targetO2.setVisible(false); - targetO3.setVisible(false); - - presenter.executeTurn(moveNum, moving, opponentNames[2]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } else if (source.equals(targetO3)) { - targetO0.setVisible(false); - targetO1.setVisible(false); - targetO2.setVisible(false); - targetO3.setVisible(false); - - presenter.executeTurn(moveNum, moving, opponentNames[3]); - - //Re-enable nextRound button after a move has been made. - nextRound.setDisable(false); - } - } - - /** - * Helper function for updateCharacter. - * - * @param character name String of the character to be updated. - * @param position index of the character's data in the data arrays. - * @param lbl Label object to be updated. - * @param isOpponent boolean for whether the character is an opponent or not. - */ - private void updateCharacterData(String character, int position, Label lbl, boolean isOpponent) { - if (presenter.givenCharacterFainted(character)) { - lbl.setText("FAINTED"); - return; - } - int[] hpData; - int[] dmgData; - String[] nameData; - - if (isOpponent) { - hpData = opponentHealth; - dmgData = opponentDmg; - nameData = opponentNames; - } else { - hpData = playerHealth; - dmgData = playerDmg; - nameData = playerNames; - } - hpData[position] = presenter.givenCharacterHealth(character); - dmgData[position] = presenter.givenCharacterDamage(character); - lbl.setText(nameData[position] + "\n Hp: " + hpData[position] + ", Dmg: " + dmgData[position]); - } - - /** - * Helper function for moveOne/moveTwo button click events. - * Removes the two buttons, retrieves targets based on the chosen move, displays respective target buttons. - */ - private void displayTargets() { - moveOne.setVisible(false); - moveTwo.setVisible(false); - - grid.getChildren().remove(moveOne); - grid.getChildren().remove(moveTwo); - - ArrayList targets = presenter.retrieveTargets(moveNum, moving); - - for (String s : targets) { - if (playerNames[0].equals(s)) { - targetP0.setVisible(true); - } else if (playerNames[1].equals(s)) { - targetP1.setVisible(true); - } else if (playerNames[2].equals(s)) { - targetP2.setVisible(true); - } else if (playerNames[3].equals(s)) { - targetP3.setVisible(true); - } else if (opponentNames[0].equals(s)) { - targetO0.setVisible(true); - } else if (opponentNames[1].equals(s)) { - targetO1.setVisible(true); - } else if (opponentNames[2].equals(s)) { - targetO2.setVisible(true); - } else if (opponentNames[3].equals(s)) { - targetO3.setVisible(true); - } - } - } - - /** - * Helper function to initialize the character labels. - * - * @param lbl Label object corresponding to the character. - * @param position Integer representing index of character's data in data arrays. - * @param isOpponent Boolean of whether the character is an opponent. - */ - private void setupCharacterLabel(Label lbl, int position, boolean isOpponent) { - if (isOpponent) { - lbl.setText(opponentNames[position] + "\n Hp: " + opponentHealth[position] + ", Dmg: " + - opponentDmg[position]); - } else { - if (playerNames[position].equals("FAINTED")) { - lbl.setText("FAINTED"); - } else { - lbl.setText(playerNames[position] + "\n Hp: " + playerHealth[position] + ", Dmg: " + - playerDmg[position]); + presenter.startBattle(); // Will call setNames and update character data. + for (int i = 0; i < playerNames.length; i++) { + updateCharacter(playerNames[i].getText(), playerNames[i], playerHps[i], playerDmgs[i], false); + updateCharacter(opponentNames[i].getText(), opponentNames[i], opponentHps[i], opponentDmgs[i], false); + } + + startTurn(); + } + } + + /** + * Create a move description. + * + * @param name the name of the move. + * @param hp the hp damage of the move. + * @param dmg the dmg damage of the move. + * + * @return the move description. + */ + private @NotNull VBox makeMoveStack(@NotNull String name, int hp, int dmg) { + GridPane info = new GridPane(); + info.addRow(0, + componentFactory.createLabel("HP: ", true), + componentFactory.createLabel(Integer.toString(hp), true) + ); + info.addRow(1, + componentFactory.createLabel("Dmg: ", true), + componentFactory.createLabel(Integer.toString(dmg), true) + ); + + VBox stack = new VBox(); + stack.getChildren().addAll(componentFactory.createLabel(name, true), info); + return stack; + } + + /** + * Start a new turn. + */ + private void startTurn() { + options.reset(); + options.addColumn(componentFactory.createLabel("Forfeit", true)); + + mover = presenter.roundStart(); + if (mover == null) { + presenter.endBattle(); + } + + needsSelection = Arrays.stream(playerNames).anyMatch(x -> x.getText().equals(mover)); + selectedMove = -1; + selectedTarget = null; + + advanceOptions(); + } + + /** + * Advance the possible options to the correct state. + */ + private void advanceOptions() { + turnHint.setText("It's " + mover + "'s turn... "); + + if (needsSelection) { + actionHint.setVisible(true); + + if (selectedMove < 0) { + actionHint.setText("Select a move:"); + + String[] moveNames = presenter.givenCharacterMoveNames(mover); + int[] moveStats = presenter.givenCharacterMoveStats(mover); + + options.addColumn( + makeMoveStack(moveNames[0], moveStats[0], moveStats[1]), + makeMoveStack(moveNames[1], moveStats[2], moveStats[3]) + ); + } else if (selectedTarget == null) { + actionHint.setText("Select a target:"); + + options.selectActive(); + + options.addColumn(); + for (String target : presenter.retrieveTargets(selectedMove, mover)) { + options.addToColumn(componentFactory.createLabel(target, true)); + } + } + } else { + actionHint.setVisible(false); + + options.addColumn(componentFactory.createLabel("Next Move", true)); + } + + while (options.activeRight()); // Make sure we are in the right-most slot, for convenience + } + + @Override + public void interpret(String key) { + switch (key) { + case "w" -> options.activeUp(); + case "s" -> options.activeDown(); + case "a" -> { + if (options.getActive().x == 2) { + actionHint.setText("Select a move:"); + selectedMove = -1; + options.popColumn(); + } else { + options.activeLeft(); + } + } + case "d" -> { + if (options.getActive().x == 0) { + options.activeRight(); + } + } + case " " -> { + if (options.getActive().x == 0) { // Forfeit + presenter.endBattle(); + return; + } + + if (needsSelection) { + if (options.getActive().x == 1) { + selectedMove = options.getActive().y + 1; // options are 1 or 2 not 0 or 1 + advanceOptions(); + } else if (options.getActive().x == 2) { + selectedTarget = presenter.retrieveTargets(selectedMove, mover).get(options.getActive().y); + + presenter.executeTurn(selectedMove, mover, selectedTarget); + + startTurn(); + } + } else { + presenter.applyOpponentAttack(); + startTurn(); + } } } } } ADDED src/main/java/com/mg105/user_interface/IntroDisplay.java Index: src/main/java/com/mg105/user_interface/IntroDisplay.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/user_interface/IntroDisplay.java @@ -0,0 +1,152 @@ +package com.mg105.user_interface; + +import com.mg105.interface_adapters.InputControllable; +import com.mg105.interface_adapters.Toggler; +import com.mg105.utils.UIConstants; +import javafx.animation.AnimationTimer; +import javafx.animation.FadeTransition; +import javafx.event.ActionEvent; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.layout.TilePane; +import javafx.util.Duration; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * IntroDisplay displays the intro text when you start a new game. + */ +public class IntroDisplay implements Toggleable, InputControllable { + private final @NotNull Toggler toggler; + private final @NotNull KeymapDisplay keymap; + private final @NotNull StandardComponentFactory componentFactory; + private final @NotNull TilePane pane; + private final @NotNull Label[] messages; + private BodyAnimation animations; + + /** + * Create a new intro display. + * + * @param toggler the toggler used to toggle components. + * @param keymap the keymap display. + * @param componentFactory the component factory. + */ + public IntroDisplay(@NotNull Toggler toggler, @NotNull KeymapDisplay keymap, @NotNull StandardComponentFactory componentFactory) { + this.toggler = toggler; + this.keymap = keymap; + this.componentFactory = componentFactory; + + pane = new TilePane(Orientation.VERTICAL); + pane.setBackground(componentFactory.createBackground(false)); + pane.setPadding(componentFactory.createInsets()); + pane.setVgap(UIConstants.SINGLE_GAP); + pane.setAlignment(Pos.CENTER); + + messages = new Label[]{ + componentFactory.createLabel("You cannot recall anything but the endless action of putting one foot in front of the other, traversing this empty landscape.", false), + componentFactory.createLabel("Suddenly, you look up, startled out of your reverie. An imposing mountain looms before you. Your party steps forward, drawn to it for some inexplicable reason.", false), + componentFactory.createLabel("You crane your neck to see if you can make out the peak, but the morning mist impedes your view.", false), + componentFactory.createLabel("You all know you cannot go back to wherever you came from. You have to keep going. You have to climb the mountain.", false), + componentFactory.createLabel("Maybe whatever waits for you up there will remind you of who you are... and what you’re searching for.", false), + componentFactory.createLabel("Press any key to continue...", false) + }; + for (Label l : messages) { + l.setWrapText(true); + l.setAlignment(Pos.CENTER); + l.setMaxWidth(UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP); + } + pane.getChildren().addAll(messages); + } + + @Override + public void interpret(String key) { + if (animations != null && animations.advance()) { + keymap.addExtraHints(componentFactory.createLabel("Pressing 'k' starts the game.", true)); + + toggler.toggle(Toggler.ToggleableComponent.CONTROLS); + toggler.toggle(Toggler.ToggleableComponent.INTRO); + } + } + + @Override + public @NotNull Pane getScene() { + return pane; + } + + @Override + public void toggle(boolean isVisible) { + if (isVisible) { + animations = new BodyAnimation(); + animations.start(); + } else { + animations.stop(); + animations = null; + } + } + + /** + * BodyAnimation slowly fades in the messages one by one. + */ + private class BodyAnimation extends AnimationTimer { + private long last; + private final @NotNull Queue transitions; + + /** + * Create a new BodyAnimation. + */ + public BodyAnimation() { + last = 0; + + transitions = new LinkedList<>(); + for (Label l : messages) { + l.setOpacity(0); + FadeTransition fade = new FadeTransition(); + fade.setDuration(Duration.millis(2500)); + fade.setFromValue(0); + fade.setToValue(1); + fade.setOnFinished((ActionEvent value) -> l.setOpacity(1)); + fade.setNode(l); + transitions.add(fade); + } + + advance(); // Don't want to wait 2 seconds for the first one to start + } + + /** + * Trigger the next animation if more than 2 seconds have passed. + * + * @param now + * The timestamp of the current frame given in nanoseconds. This + * value will be the same for all {@code AnimationTimers} called + * during one frame. + */ + @Override + public void handle(long now) { + if (last == 0) { + last = now; + } else if (now - last > 1750e6 && advance()) { + stop(); + } + } + + /** + * Advance the animations by one. + * + * @return true if this did nothing. + */ + public boolean advance() { + if (transitions.isEmpty()) { + return true; + } + + last = 0; + transitions.remove().play(); + + return false; + } + } +} Index: src/main/java/com/mg105/user_interface/InventoryDisplay.java ================================================================== --- src/main/java/com/mg105/user_interface/InventoryDisplay.java +++ src/main/java/com/mg105/user_interface/InventoryDisplay.java @@ -1,87 +1,94 @@ package com.mg105.user_interface; +import com.mg105.interface_adapters.InputControllable; +import com.mg105.interface_adapters.PartyGetter; import com.mg105.interface_adapters.inventory.InventoryController; import com.mg105.interface_adapters.inventory.InventoryViewInterface; import com.mg105.utils.PartyConstants; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.ComboBox; +import com.mg105.utils.UIConstants; +import javafx.geometry.Insets; +import javafx.geometry.Pos; import javafx.scene.control.Label; -import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.TilePane; import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * A user interface class that displays the state of the inventory to the user and allows them to * perform actions that relate to the inventory and its items. */ - -public class InventoryDisplay implements InventoryViewInterface, Toggleable { - - private final HashMap itemNameToInfo = new HashMap<>(); - private final InventoryController controller; - private Boolean isVisible = false; - private VBox itemsDisplay; - private String characterSelected = PartyConstants.ALL_PARTY_MEMBER_NAMES[0]; +public class InventoryDisplay implements InventoryViewInterface, Toggleable, InputControllable { + private final @NotNull InventoryController controller; + private final @NotNull PartyGetter partyGetter; + private boolean isVisible = false; + private final @NotNull StandardComponentFactory componentFactory; + private final @NotNull MultiselectGrid options; + private final @NotNull TilePane layoutWrapper; + private final @NotNull Label hintLabel; + + private final @NotNull Map descriptionCache; + private final @NotNull Map usableCache; + private final @NotNull Map quantityCache; + private final @NotNull List nameCache; + /** * Creates a new instance of inventory display * - * @param controller an object that interprets user inputs to make changes about the inventory + * @param controller an object that interprets user inputs to make changes about the inventory + * @param componentFactory the component factory used to create UI components + * @param partyGetter the party getter used to get party information. */ - - public InventoryDisplay(InventoryController controller) { + public InventoryDisplay(@NotNull InventoryController controller, + @NotNull StandardComponentFactory componentFactory, + @NotNull PartyGetter partyGetter) { this.controller = controller; - } - - /** - * Returns the character selected by the user - * - * @return the character selected by the user - */ - private String getCharacterSelected() { - return this.characterSelected; - } - - /** - * Returns the dropdown where user can select a party member - * - * @return the dropdown where user can select a party member - */ - private @NotNull ComboBox buildCharacterDropdown() { - ComboBox partySelector = new ComboBox<>(); - partySelector.getItems().addAll(PartyConstants.ALL_PARTY_MEMBER_NAMES); - partySelector.setOnAction(e -> this.characterSelected = partySelector.getSelectionModel().getSelectedItem()); - partySelector.setValue(characterSelected); - return partySelector; - } - - /** - * Returns the component that really has all the details necessary for the scene - * - * @return the component that really has all the details necessary for the scene - */ - private @NotNull VBox buildLayout() { - this.itemsDisplay = new VBox(10); - this.itemsDisplay.getChildren().add(new Label("Inventory")); - controller.getInventoryDetails(); - return new VBox(5, this.itemsDisplay, buildCharacterDropdown()); + this.componentFactory = componentFactory; + this.partyGetter = partyGetter; + + layoutWrapper = new TilePane(); + layoutWrapper.setPadding(componentFactory.createInsets()); + layoutWrapper.setAlignment(Pos.CENTER); + layoutWrapper.setBackground(componentFactory.createBackground(true)); + + VBox layout = new VBox(); + layout.setPrefSize( + UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP, + UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP + ); + layoutWrapper.getChildren().add(layout); + + options = componentFactory.createMultiselectGrid(); + layout.getChildren().add(options.getGrid()); + + layout.getChildren().add(componentFactory.createSpacer()); + layout.getChildren().add(componentFactory.createSubtitleLabel("Hint(s):", true)); + + hintLabel = componentFactory.createLabel("UNSET TEXT", true); + layout.getChildren().add(hintLabel); + + descriptionCache = new HashMap<>(); + usableCache = new HashMap<>(); + quantityCache = new HashMap<>(); + nameCache = new ArrayList<>(); } /** * Returns the scene that displays the information for the Inventory * * @return the scene that displays the information for the Inventory. */ - @Override - public @NotNull Scene getScene() { - return new Scene(buildLayout(), 800, 800, Color.LIGHTBLUE); + public @NotNull Pane getScene() { + return layoutWrapper; } /** * Changes the state of the InventoryDisplay based on if the inventory display is shown in the ui * @@ -89,36 +96,33 @@ * to do nothing on ANY user inputs. */ @Override public void toggle(boolean isVisible) { this.isVisible = isVisible; - } - - /** - * Removes an item from the inventory ui. - * Precondition there must not be an items of the type name in the inventory - * - * @param name the name of the item to be removed - */ - @Override - public void removeItemView(String name) { - HBox itemInfo = this.itemNameToInfo.get(name); - if (itemInfo == null) { - return; - } - itemsDisplay.getChildren().remove(itemInfo); + + if (isVisible) { + nameCache.clear(); + controller.getInventoryDetails(); + options.reset(); + if (nameCache.size() > 0) { + options.addColumn(); + for (String name : nameCache) { + options.addToColumn(componentFactory.createLabel(name + " (" + quantityCache.get(name) + ")", true)); + } + } + updateInfo(); + } } /** * Opens a window displaying a message to the user * * @param msg the message to alert the user with */ @Override public void alert(String msg) { - AlertBox alert = new AlertBox(); - alert.display(msg); + componentFactory.createAlert(msg, true); } /** * Adds the details of the item to the display * @@ -127,29 +131,12 @@ * @param isUsable true iff the item can be used by a user * @param quantity the number of items of this type in the Inventory */ @Override public void addItemView(String name, String description, boolean isUsable, String quantity) { - if (!this.isVisible) { - return; - } - Label nameLabel = new Label(name); - Label descriptionLabel = new Label(description); - Label quantityLabel = new Label(quantity); - Button removeItem = new Button("Remove Item"); - removeItem.setOnAction(e -> - controller.removeItem(name)); - HBox info = new HBox(10); - - if (isUsable) { - info.getChildren().addAll(nameLabel, descriptionLabel, quantityLabel, getUseItemButton(name), removeItem); - } else { - info.getChildren().addAll(nameLabel, descriptionLabel, quantityLabel, removeItem); - } - - itemsDisplay.getChildren().add(info); - itemNameToInfo.put(name, info); + changeItemView(name, description, isUsable, quantity); + nameCache.add(name); } /** * Changes the details of an already displayed item and displays the changes * @@ -158,28 +145,108 @@ * @param isUsable true iff the item is usable * @param quantity the number of items of this type in the Inventory */ @Override public void changeItemView(String name, String description, boolean isUsable, String quantity) { - - HBox currentInfo = itemNameToInfo.get(name); - if (currentInfo != null) { - itemNameToInfo.remove(name); - itemsDisplay.getChildren().remove(currentInfo); - } - - addItemView(name, description, isUsable, quantity); + descriptionCache.put(name, description); + usableCache.put(name, isUsable); + quantityCache.put(name, quantity); + } + + /** + * Removes an item from the inventory ui. + * Precondition there must not be an items of the type name in the inventory + * + * @param name the name of the item to be removed + */ + @Override + public void removeItemView(String name) { + descriptionCache.remove(name); + usableCache.remove(name); + quantityCache.remove(name); + nameCache.remove(name); + } + + @Override + public void interpret(String key) { + if (!isVisible) { + return; + } + + switch (key) { + case "w" -> options.activeUp(); + case "s" -> options.activeDown(); + case "a" -> { + if (options.getActive().x > 0) { + options.popColumn(); + } + } + case " " -> { + if (options.isEmpty()) { + return; + } + + if (options.getActive().x == 0) { + options.selectActive(); + options.addColumn(componentFactory.createLabel("Remove Item", true)); + if (usableCache.get(nameCache.get(options.getActive().y))) { + options.addToColumn(componentFactory.createLabel("Use Item", true)); + } + options.activeRight(); + } else if (options.getActive().x == 1) { + if (options.getActive().y == 0) { + controller.removeItem(nameCache.get(options.getSelected().get(0))); + toggle(true); + } else { + options.selectActive(); + options.addColumn(); + for (String name : PartyConstants.ALL_PARTY_MEMBER_NAMES) { + options.addToColumn(componentFactory.createLabel(name, true)); + } + options.activeRight(); + } + } else { + controller.useItem( + nameCache.get(options.getSelected().get(0)), + PartyConstants.ALL_PARTY_MEMBER_NAMES[options.getActive().y] + ); + toggle(true); + } + } + } + updateInfo(); } /** - * Creates a use item button - * - * @param name of item to create use item button - * @return the use item button + * Update the hint bottom text. */ - private Button getUseItemButton(String name) { - Button useItem = new Button("Use Item"); - useItem.setOnAction(e -> - controller.useItem(name, getCharacterSelected())); - return useItem; + private void updateInfo() { + if (options.isEmpty()) { + options.getGrid().setVisible(false); + hintLabel.setText("Inventory is empty"); + return; + } + + options.getGrid().setVisible(true); + + if (options.getActive().x == 0) { + hintLabel.setText(descriptionCache.get(nameCache.get(options.getActive().y))); + return; + } + + if (options.getActive().x == 1) { + if (options.getActive().y == 0) { + hintLabel.setText("Drop this item from your inventory."); + } else { + hintLabel.setText("Use this item on a character."); + } + return; + } + + String name = PartyConstants.ALL_PARTY_MEMBER_NAMES[options.getActive().y]; + if (partyGetter.isFainted(name)) { + hintLabel.setText(name + " is fainted."); + } else { + hintLabel.setText(name + " has " + partyGetter.getHp(name) + "/" + partyGetter.getMaxHp(name) + " HP and " + partyGetter.getDmg(name) + " Dmg."); + } } } ADDED src/main/java/com/mg105/user_interface/KeymapDisplay.java Index: src/main/java/com/mg105/user_interface/KeymapDisplay.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/user_interface/KeymapDisplay.java @@ -0,0 +1,201 @@ +package com.mg105.user_interface; + +import com.mg105.utils.ColorConstants; +import com.mg105.utils.UIConstants; +import javafx.event.EventHandler; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.*; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +/** + * Display the keyboard mappings. + */ +public class KeymapDisplay implements Toggleable, EventHandler { + private static final Border KEY_BORDER_UNSELECTED = new Border(new BorderStroke( + ColorConstants.WALL, + BorderStrokeStyle.SOLID, + new CornerRadii((int)(UIConstants.SINGLE_GAP / 4)), + new BorderWidths((int)(UIConstants.SINGLE_GAP / 8)) + )); + private static final Border KEY_BORDER_SELECTED = new Border(new BorderStroke( + ColorConstants.ACTIVE, + BorderStrokeStyle.SOLID, + new CornerRadii((int)(UIConstants.SINGLE_GAP / 4)), + new BorderWidths((int)(UIConstants.SINGLE_GAP / 8)) + )); + + private final @NotNull TilePane pane; + private final @NotNull VBox layout; + private final int layoutSize; + private boolean isVisible = false; + private final @NotNull Map buttons; + + /** + * Create a new keymap display. + * + * @param componentFactory the component factory used to create components. + */ + public KeymapDisplay(StandardComponentFactory componentFactory) { + pane = new TilePane(Orientation.VERTICAL); + pane.setBackground(componentFactory.createBackground(true)); + pane.setPadding(componentFactory.createInsets()); + pane.setAlignment(Pos.CENTER); + + layout = new VBox(); + layout.setPrefHeight(UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP); + pane.getChildren().add(layout); + + GridPane grid = new GridPane(); + grid.setHgap(UIConstants.SINGLE_GAP); + grid.setVgap(UIConstants.SINGLE_GAP); + grid.setPrefWidth(UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP); + ColumnConstraints keyConstraints = new ColumnConstraints(); + keyConstraints.setPercentWidth(25); + ColumnConstraints textConstraints = new ColumnConstraints(); + textConstraints.setPercentWidth(75); + grid.getColumnConstraints().addAll(keyConstraints, textConstraints); + layout.getChildren().add(grid); + + layout.getChildren().add(componentFactory.createSpacer()); + layout.getChildren().add(componentFactory.createSubtitleLabel("Hint(s):", true)); + layout.getChildren().add(componentFactory.createLabel("Try pressing the keys!", true)); + layoutSize = layout.getChildren().size(); + + Label wKey = componentFactory.createLabel("W", true); + Label aKey = componentFactory.createLabel("A", true); + Label sKey = componentFactory.createLabel("S", true); + Label dKey = componentFactory.createLabel("D", true); + GridPane wasdGrid = new GridPane(); + wasdGrid.add(wKey, 1, 0); + wasdGrid.addRow(1, aKey, sKey, dKey); + Label wasdLabel = componentFactory.createLabel("Cursor keys (WASD for up, left, down, right respectively).", true); + grid.addRow(0, wasdGrid, wasdLabel); + + Label spaceKey = componentFactory.createLabel("", true); + Label spaceLabel = componentFactory.createLabel("Make selection.", true); + grid.addRow(1, spaceKey, spaceLabel); + + Label eKey = componentFactory.createLabel("E", true); + Label fKey = componentFactory.createLabel("F", true); + HBox efGrid = new HBox(); + efGrid.getChildren().addAll(eKey, componentFactory.createFixedDoubleSpacer(), fKey); + Label efLabel = componentFactory.createLabel("Open chest/start battle respectively (map only).", true); + grid.addRow(2, efGrid, efLabel); + + Label iKey = componentFactory.createLabel("I", true); + Label mKey = componentFactory.createLabel("M", true); + Label cKey = componentFactory.createLabel("C", true); + HBox imcGrid = new HBox(); + imcGrid.getChildren().addAll(iKey, mKey, cKey); + Label imcLabel = componentFactory.createLabel("Open/close inventory/minimap/character selector respectively (map only).", true); + grid.addRow(3, imcGrid, imcLabel); + + Label kKey = componentFactory.createLabel("K", true); + HBox kGrid = new HBox(); + kGrid.getChildren().addAll(componentFactory.createFixedDoubleSpacer(), kKey, componentFactory.createFixedDoubleSpacer()); + Label kLabel = componentFactory.createLabel("Open/close this menu (anywhere, even on the main menu!).", true); + grid.addRow(4, kGrid, kLabel); + + buttons = new HashMap<>(); + buttons.put("w", wKey); + buttons.put("a", aKey); + buttons.put("s", sKey); + buttons.put("d", dKey); + buttons.put(" ", spaceKey); + buttons.put("e", eKey); + buttons.put("f", fKey); + buttons.put("i", iKey); + buttons.put("m", mKey); + buttons.put("c", cKey); + buttons.put("k", kKey); + + for (Label l : new Label[]{wasdLabel, spaceLabel, efLabel, imcLabel, kLabel}) { + l.setWrapText(true); + l.setMaxWidth(0.7 * (UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP)); + } + + for (Map.Entry key : buttons.entrySet()) { + key.getValue().setPrefSize(2 * UIConstants.SINGLE_GAP, 2 * UIConstants.SINGLE_GAP); + key.getValue().setAlignment(Pos.CENTER); + key.getValue().setFont(UIConstants.FONT_MONO); + releaseKey(key.getKey()); + } + + spaceKey.setMaxWidth(6 * UIConstants.SINGLE_GAP); // space edge case + } + + @Override + public @NotNull Pane getScene() { + return pane; + } + + @Override + public void toggle(boolean isVisible) { + this.isVisible = isVisible; + + if (isVisible) { + for (Map.Entry key : buttons.entrySet()) { + releaseKey(key.getKey()); + } + } else { + while (layout.getChildren().size() > layoutSize) { + layout.getChildren().remove(layout.getChildren().size() - 1); + } + } + } + + @Override + public void handle(KeyEvent event) { + if (!isVisible) { + return; + } + + if (event.getEventType().equals(KeyEvent.KEY_PRESSED)) { + pressKey(event.getText()); + } else if (event.getEventType().equals(KeyEvent.KEY_RELEASED)) { + releaseKey(event.getText()); + } + } + + /** + * Add extra hints at the bottom that disappear when the menu is closed. + * + * @param hints the hints to add. + */ + public void addExtraHints(@NotNull Label... hints) { + for (Label l : hints) { + l.setMaxWidth(UIConstants.CANVAS_SIZE - 2 * UIConstants.SINGLE_GAP); + layout.getChildren().add(l); + } + } + + /** + * Set the style of the key to pressed. + * + * @param keyKey the key to be pressed. + */ + private void pressKey(@NotNull String keyKey) { + if (buttons.containsKey(keyKey)) { + buttons.get(keyKey).setTextFill(ColorConstants.ACTIVE); + buttons.get(keyKey).setBorder(KEY_BORDER_SELECTED); + } + } + + /** + * Set the style of the key to be released. + * + * @param keyKey the key to be released. + */ + private void releaseKey(@NotNull String keyKey) { + if (buttons.containsKey(keyKey)) { + buttons.get(keyKey).setTextFill(ColorConstants.WALL); + buttons.get(keyKey).setBorder(KEY_BORDER_UNSELECTED); + } + } +} Index: src/main/java/com/mg105/user_interface/LoseMenu.java ================================================================== --- src/main/java/com/mg105/user_interface/LoseMenu.java +++ src/main/java/com/mg105/user_interface/LoseMenu.java @@ -1,38 +1,65 @@ package com.mg105.user_interface; -import javafx.scene.Scene; -import javafx.scene.control.Button; +import com.mg105.interface_adapters.InputControllable; +import com.mg105.utils.UIConstants; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; +import javafx.scene.layout.TilePane; import org.jetbrains.annotations.NotNull; /** * LoseMenu is displayed when the player loses the game. */ -public class LoseMenu implements Toggleable { - private final @NotNull Scene scene; +public class LoseMenu implements Toggleable, InputControllable { + private boolean isVisible = false; + private final @NotNull Runnable replayButton; + private final @NotNull MainMenu mainMenu; + private final @NotNull TilePane layout; /** * Create a new LoseMenu * - * @param replayButton the button that performs the replay action + * @param replayButton the button that performs the replay action + * @param componentFactory the component factory used to create UI components + * @param mainMenu the main menu component. */ - public LoseMenu(@NotNull ReplayGeneratorButton replayButton) { - Button generateMapButton = new Button("Replay The Game"); - //Clean Inventory - generateMapButton.setOnAction(replayButton); - @NotNull Pane layout = new StackPane(); - layout.getChildren().add(generateMapButton); - scene = new Scene(layout, 600, 600); + public LoseMenu(@NotNull Runnable replayButton, @NotNull StandardComponentFactory componentFactory, + @NotNull MainMenu mainMenu) { + this.replayButton = replayButton; + this.mainMenu = mainMenu; + + layout = new TilePane(Orientation.VERTICAL); + layout.setBackground(componentFactory.createBackground(false)); + layout.setVgap(UIConstants.SINGLE_GAP); + layout.setAlignment(Pos.CENTER); + + layout.getChildren().addAll( + componentFactory.createTitleLabel("Battle Lost", false), + componentFactory.createLabel("Well that's unfortunate.", false) + ); + + MultiselectGrid options = componentFactory.createMultiselectGrid(); + options.getGrid().setAlignment(Pos.CENTER); + options.addColumn(componentFactory.createLabel("Now what...", false)); + layout.getChildren().add(options.getGrid()); } @Override - public @NotNull Scene getScene() { - return scene; + public @NotNull Pane getScene() { + return layout; } @Override public void toggle(boolean isVisible) { - // Does nothing + this.isVisible = isVisible; + } + + @Override + public void interpret(String key) { + if (isVisible && key.equals(" ")) { + mainMenu.lost(); + replayButton.run(); + } } } Index: src/main/java/com/mg105/user_interface/MainMenu.java ================================================================== --- src/main/java/com/mg105/user_interface/MainMenu.java +++ src/main/java/com/mg105/user_interface/MainMenu.java @@ -1,37 +1,111 @@ package com.mg105.user_interface; -import javafx.scene.Scene; -import javafx.scene.control.Button; +import com.mg105.interface_adapters.InputControllable; +import com.mg105.interface_adapters.Toggler; +import com.mg105.utils.ColorConstants; +import com.mg105.utils.UIConstants; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; +import javafx.scene.layout.TilePane; import org.jetbrains.annotations.NotNull; /** * The main game menu. */ -public class MainMenu implements Toggleable { - private final @NotNull Scene scene; +public class MainMenu implements Toggleable, InputControllable { + private final @NotNull Toggler toggler; + private final @NotNull Runnable startButton; + private final @NotNull MultiselectGrid options; + private boolean isVisible = false; + private final @NotNull TilePane layout; + + private int playCount = 105; + private final @NotNull Label playLabel; /** * Create a new MainMenu. * - * @param startButton the button that starts the game. + * @param toggler the toggler used to toggle displays. + * @param startButton the button that starts the game. + * @param componentFactory the component factory used to create UI components */ - public MainMenu(@NotNull MapGeneratorButton startButton) { - Button generateMapButton = new Button("Start Game"); - generateMapButton.setOnAction(startButton); - @NotNull Pane layout = new StackPane(); - layout.getChildren().add(generateMapButton); - scene = new Scene(layout, 600, 600); + public MainMenu(@NotNull Toggler toggler, + @NotNull Runnable startButton, + @NotNull StandardComponentFactory componentFactory) { + this.toggler = toggler; + this.startButton = startButton; + + layout = new TilePane(Orientation.VERTICAL); + layout.setVgap(UIConstants.SINGLE_GAP); + layout.setAlignment(Pos.CENTER); + layout.setBackground(componentFactory.createBackground(false)); + + playLabel = componentFactory.createTitleLabel(Integer.toString(playCount), false); + playLabel.setFont(UIConstants.TITLE_FONT_ALT); + + HBox title = new HBox(); + title.setAlignment(Pos.BASELINE_CENTER); + title.getChildren().addAll( + componentFactory.createTitleLabel("Mountain Group ", false), + playLabel + ); + layout.getChildren().add(title); + + options = componentFactory.createMultiselectGrid(); + options.addColumn( + componentFactory.createLabel("Start Game", false), + componentFactory.createLabel("Controls", false), + componentFactory.createLabel("Quit Game", false) + ); + options.getGrid().setAlignment(Pos.CENTER); + layout.getChildren().add(options.getGrid()); } @Override - public @NotNull Scene getScene() { - return scene; + public @NotNull Pane getScene() { + return layout; } @Override public void toggle(boolean isVisible) { - // Does nothing, for now + this.isVisible = isVisible; + } + + @Override + public void interpret(String key) { + if (isVisible) { + switch (key) { + case "w" -> options.activeUp(); + case "s" -> options.activeDown(); + case " " -> { + switch (options.getActive().y) { + case 0 -> startButton.run(); + case 1 -> toggler.toggle(Toggler.ToggleableComponent.CONTROLS); + default -> System.exit(0); + } + } + } + } + } + + /** + * Notify the title screen that the game has been lost. + */ + public void lost() { + playLabel.setTextFill(ColorConstants.SNOW_ALT); + playCount += 1; + playLabel.setText(Integer.toString(playCount)); + } + + /** + * Gets the number displayed after 'mountain group' + * + * @return the number displayed after 'mountain group' + */ + public int getPlayCount() { + return playCount; } } Index: src/main/java/com/mg105/user_interface/MapDrawer.java ================================================================== --- src/main/java/com/mg105/user_interface/MapDrawer.java +++ src/main/java/com/mg105/user_interface/MapDrawer.java @@ -1,34 +1,34 @@ package com.mg105.user_interface; import com.mg105.interface_adapters.map.RoomInterpreterInterface; +import com.mg105.interface_adapters.map.RoomState; import com.mg105.interface_adapters.map.RoomTileType; import com.mg105.utils.MapConstants; -import javafx.scene.Group; -import javafx.scene.Scene; +import com.mg105.utils.Textures; +import com.mg105.utils.UIConstants; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.shape.Rectangle; import org.jetbrains.annotations.NotNull; import java.awt.*; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashMap; import java.util.Map; -import java.util.Objects; /** * MapDrawer draws the map as a grid of tiles. */ public class MapDrawer implements PropertyChangeListener, Toggleable { private final @NotNull RoomInterpreterInterface interpreter; - private final @NotNull Scene scene; - private final @NotNull Group group; + private final @NotNull Pane pane; private final @NotNull Map tiles; private final @NotNull Map playerSprites; - private final @NotNull Image missingTile; private boolean isVisible; /** * Create an instance of MapDrawer. * @@ -35,46 +35,36 @@ * @param interpreter the room interpreter used to format the data in an acceptable way for this class. */ public MapDrawer(@NotNull RoomInterpreterInterface interpreter) { this.interpreter = interpreter; - group = new Group(); - scene = new Scene( - group, - MapConstants.ROOM_SIZE * MapConstants.TILE_SIZE, - MapConstants.ROOM_SIZE * MapConstants.TILE_SIZE - ); isVisible = false; - missingTile = new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/missing.png"))); + pane = new Pane(); tiles = new HashMap<>(6); - tiles.put(RoomTileType.FLOOR, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/floor.png")))); - tiles.put(RoomTileType.WALL, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/wall.png")))); - tiles.put(RoomTileType.WALL_WITH_FACE, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/wall_face.png")))); - tiles.put(RoomTileType.EXIT, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/exit.png")))); - tiles.put(RoomTileType.CHEST, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/chest.png")))); - tiles.put(RoomTileType.CHEST_OPEN, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/chest_open.png")))); - tiles.put(RoomTileType.OPPONENT_SET, new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/tiles/battle.png")))); + tiles.put(RoomTileType.WALL, Textures.TILE_WALL); + tiles.put(RoomTileType.WALL_FACE, Textures.TILE_WALL_FACE); + tiles.put(RoomTileType.CHEST, Textures.TILE_CHEST); + tiles.put(RoomTileType.CHEST_OPEN, Textures.TILE_CHEST_OPEN); + tiles.put(RoomTileType.OPPONENT_SET, Textures.TILE_OPPONENT_SET); playerSprites = new HashMap<>(4); - playerSprites.put("/sprites/A.png", new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/sprites/A.png")))); - playerSprites.put("/sprites/B.png", new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/sprites/B.png")))); - playerSprites.put("/sprites/C.png", new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/sprites/C.png")))); - playerSprites.put("/sprites/D.png", new Image(Objects.requireNonNull(MapDrawer.class.getResourceAsStream("/sprites/D.png")))); - // While in theory getResourceAsStream can fail, in practice this will never happen because the images are - // bundled in the Jar. If this isn't the case then the NullPointerException is the least of your worries. + playerSprites.put("/sprites/A.png", Textures.PLAYER_A); + playerSprites.put("/sprites/B.png", Textures.PLAYER_B); + playerSprites.put("/sprites/C.png", Textures.PLAYER_C); + playerSprites.put("/sprites/D.png", Textures.PLAYER_D); } /** * Get the scene that will be used to draw to. * * @return the scene that the MapDrawer will draw to. */ @Override - public @NotNull Scene getScene() { - return scene; + public @NotNull Pane getScene() { + return pane; } /** * Toggle the visibility of map drawer. In this case we need to make sure the map drawing is up-to-date. * @@ -92,36 +82,56 @@ /** * Redraw the current room. This method only needs to be called if something has changed in the underlying * current room. */ public void updateRoom() { - RoomTileType[][] room = interpreter.getCurrentRoom(); - - group.getChildren().clear(); - - for (int y = 0; y < MapConstants.ROOM_SIZE; y++) { - for (int x = 0; x < MapConstants.ROOM_SIZE; x++) { - ImageView tile = new ImageView(tiles.getOrDefault(room[y][x], missingTile)); - - tile.setPreserveRatio(true); - tile.setX(x * MapConstants.TILE_SIZE); - tile.setY(y * MapConstants.TILE_SIZE); - tile.setFitHeight(MapConstants.TILE_SIZE); - tile.setFitWidth(MapConstants.TILE_SIZE); - - group.getChildren().add(tile); - } - } - - Point player = interpreter.getPlayer(); - ImageView playerTile = new ImageView(playerSprites.getOrDefault(interpreter.getCharacterSprite(), missingTile)); - playerTile.setPreserveRatio(true); - playerTile.setX(player.x * MapConstants.TILE_SIZE); - playerTile.setY(player.y * MapConstants.TILE_SIZE); - playerTile.setFitHeight(MapConstants.TILE_SIZE); - playerTile.setFitWidth(MapConstants.TILE_SIZE); - group.getChildren().add(playerTile); + RoomState room = interpreter.getCurrentRoom(); + + pane.getChildren().clear(); + + Rectangle clip = new Rectangle(); + clip.setX(0); + clip.setY(0); + clip.setWidth(UIConstants.CANVAS_SIZE); + clip.setHeight(UIConstants.CANVAS_SIZE); + pane.setClip(clip); + + final int offsetLayerOffset = -MapConstants.TILE_SIZE / 4; + + for (int y = 0; y < MapConstants.ROOM_SIZE + 1; y++) { + for (int x = 0; x < MapConstants.ROOM_SIZE; x++) { + pane.getChildren().add(makeTile( + x * MapConstants.TILE_SIZE, + y * MapConstants.TILE_SIZE, + Textures.TILE_FLOOR + )); + + if (room.getOffsetLayer()[y][x] != null) { + pane.getChildren().add(makeTile( + x * MapConstants.TILE_SIZE, + y * MapConstants.TILE_SIZE + offsetLayerOffset, + tiles.getOrDefault(room.getOffsetLayer()[y][x], Textures.MISSING) + )); + } + + if (room.getBaseLayer()[y][x] != null) { + pane.getChildren().add(makeTile( + x * MapConstants.TILE_SIZE, + y * MapConstants.TILE_SIZE, + tiles.getOrDefault(room.getBaseLayer()[y][x], Textures.MISSING) + )); + } + + if (room.getPlayerPosition().equals(new Point(x, y))) { + pane.getChildren().add(makeTile( + room.getPlayerPosition().x * MapConstants.TILE_SIZE, + room.getPlayerPosition().y * MapConstants.TILE_SIZE, + playerSprites.getOrDefault(room.getPlayerSprite(), Textures.MISSING) + )); + } + } + } } /** * Update the current room based on evt. *

@@ -137,6 +147,27 @@ return; } updateRoom(); } + + /** + * Create a new tile appropriate given the size of the scene placed at a certain set of coordinates + * + * @param x the x position of the tile. + * @param y the y position of the tile. + * @param image the image used for the tile. + * + * @return the tile + */ + private @NotNull ImageView makeTile(int x, int y, @NotNull Image image) { + ImageView tile = new ImageView(image); + + tile.setPreserveRatio(true); + tile.setX(x); + tile.setY(y); + tile.setFitHeight(MapConstants.TILE_SIZE); + tile.setFitWidth(MapConstants.TILE_SIZE); + + return tile; + } } Index: src/main/java/com/mg105/user_interface/MapGeneratorButton.java ================================================================== --- src/main/java/com/mg105/user_interface/MapGeneratorButton.java +++ src/main/java/com/mg105/user_interface/MapGeneratorButton.java @@ -1,17 +1,15 @@ package com.mg105.user_interface; import com.mg105.interface_adapters.Toggler; import com.mg105.interface_adapters.map.MapGeneratorInterpreterInterface; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; import org.jetbrains.annotations.NotNull; /** * MapGeneratorButton acts as the event handler for the actual JavaFX button that will generate a map. */ -public class MapGeneratorButton implements EventHandler { +public class MapGeneratorButton implements Runnable { private final @NotNull MapGeneratorInterpreterInterface interpreter; private final @NotNull Toggler toggler; /** @@ -25,14 +23,12 @@ this.toggler = toggler; } /** * This method is called when the button is pressed. It passes control to the appropriate interpreter. - * - * @param event the event which occurred */ @Override - public void handle(ActionEvent event) { + public void run() { interpreter.generateMap(); toggler.toggle(Toggler.ToggleableComponent.MAIN_MENU); } } Index: src/main/java/com/mg105/user_interface/MinimapDrawer.java ================================================================== --- src/main/java/com/mg105/user_interface/MinimapDrawer.java +++ src/main/java/com/mg105/user_interface/MinimapDrawer.java @@ -1,12 +1,12 @@ package com.mg105.user_interface; import com.mg105.interface_adapters.map.MinimapInterpreterInterface; import com.mg105.interface_adapters.map.MinimapRoomState; -import javafx.scene.Group; -import javafx.scene.Scene; -import javafx.scene.paint.Color; +import com.mg105.utils.ColorConstants; +import com.mg105.utils.UIConstants; +import javafx.scene.layout.Pane; import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; import org.jetbrains.annotations.NotNull; import java.awt.*; @@ -13,40 +13,33 @@ /** * Draw the Minimap. */ public class MinimapDrawer implements Toggleable { - private static final int CANVAS_SIZE = 400; - private static final @NotNull Color PATH_COLOR = Color.rgb(135, 160, 227); - private static final @NotNull Color ROOM_COLOR = Color.rgb(205, 220, 255); - private static final @NotNull Color CURRENT_ROOM_COLOR = Color.rgb(200, 150, 61); - private static final @NotNull Color BACKGROUND_COLOR = Color.rgb(38, 44, 68); + private final @NotNull MinimapInterpreterInterface interpreter; - private final @NotNull Scene scene; - private final @NotNull Group group; + private final @NotNull Pane pane; /** * Create a new MinimapDrawer. * * @param interpreter the MinimapInterpreter that will process room change data. */ public MinimapDrawer(@NotNull MinimapInterpreterInterface interpreter) { this.interpreter = interpreter; - group = new Group(); - scene = new Scene(group, CANVAS_SIZE, CANVAS_SIZE); - scene.setFill(BACKGROUND_COLOR); + pane = new Pane(); } /** * Get the minimap scene. * * @return the minimap scene. */ @Override - public @NotNull Scene getScene() { - return scene; + public @NotNull Pane getScene() { + return pane; } /** * Toggle the minimap. * @@ -57,28 +50,35 @@ public void toggle(boolean isVisible) { if (isVisible) { MinimapRoomState[][] map = interpreter.getMapSoFar(); Point currentPosition = interpreter.getCurrentPosition(); - final int cellDimension = CANVAS_SIZE / Math.max(map.length, map[0].length); + final int cellDimension = UIConstants.CANVAS_SIZE / Math.max(map.length, map[0].length); final int innerCellPadding = cellDimension / 6; - final int topPadding = (CANVAS_SIZE - map.length * cellDimension) / 2; - final int leftPadding = (CANVAS_SIZE - map[0].length * cellDimension) / 2; + final int topPadding = (UIConstants.CANVAS_SIZE - map.length * cellDimension) / 2; + final int leftPadding = (UIConstants.CANVAS_SIZE - map[0].length * cellDimension) / 2; + + pane.getChildren().clear(); - group.getChildren().clear(); + Rectangle background = new Rectangle(); + background.setWidth(UIConstants.CANVAS_SIZE); + background.setHeight(UIConstants.CANVAS_SIZE); + background.setFill(ColorConstants.WALL); + pane.getChildren().add(background); + for (int y = 0; y < map.length; y++) { for (int x = 0; x < map[y].length; x++) { // Shading for the current room if (currentPosition.x == x && currentPosition.y == y) { Rectangle r = new Rectangle(); r.setX(leftPadding + x * cellDimension); r.setY(topPadding + y * cellDimension); r.setWidth(cellDimension); r.setHeight(cellDimension); - r.setFill(CURRENT_ROOM_COLOR); - group.getChildren().add(r); + r.setFill(ColorConstants.ACTIVE); + pane.getChildren().add(r); } // Rectangle for each room if (map[y][x] == MinimapRoomState.EXPLORED) { // First we draw the middle square @@ -85,12 +85,12 @@ Rectangle r = new Rectangle(); r.setX(leftPadding + innerCellPadding + x * cellDimension); r.setY(topPadding + innerCellPadding + y * cellDimension); r.setWidth(cellDimension - 2 * innerCellPadding); r.setHeight(cellDimension - 2 * innerCellPadding); - r.setFill(ROOM_COLOR); - group.getChildren().add(r); + r.setFill(ColorConstants.SNOW); + pane.getChildren().add(r); // Now we draw the lines that sick out final int xMidpoint = leftPadding + x * cellDimension + cellDimension / 2; final int yMidpoint = topPadding + y * cellDimension + cellDimension / 2; final int strokeWidth = cellDimension / 10; @@ -101,51 +101,51 @@ Line l = new Line(); l.setStartX(xMidpoint); l.setEndX(xMidpoint); l.setStartY(topPadding + y * cellDimension + innerCellPadding - strokeWidthCorrection); l.setEndY(topPadding + y * cellDimension + strokeWidthCorrection); - l.setStroke(PATH_COLOR); + l.setStroke(ColorConstants.SNOW_ALT); l.setStrokeWidth(strokeWidth); - group.getChildren().add(l); + pane.getChildren().add(l); } // Line coming out the bottom if (y < map.length - 1 && map[y + 1][x] != null) { Line l = new Line(); l.setStartX(xMidpoint); l.setEndX(xMidpoint); l.setStartY(topPadding + y * cellDimension + cellDimension - innerCellPadding + strokeWidthCorrection); l.setEndY(topPadding + y * cellDimension + cellDimension - strokeWidthCorrection); - l.setStroke(PATH_COLOR); + l.setStroke(ColorConstants.SNOW_ALT); l.setStrokeWidth(strokeWidth); - group.getChildren().add(l); + pane.getChildren().add(l); } // Line coming out the left if (x > 0 && map[y][x - 1] != null) { Line l = new Line(); l.setStartX(leftPadding + x * cellDimension + innerCellPadding - strokeWidthCorrection); l.setEndX(leftPadding + x * cellDimension + strokeWidthCorrection); l.setStartY(yMidpoint); l.setEndY(yMidpoint); - l.setStroke(PATH_COLOR); + l.setStroke(ColorConstants.SNOW_ALT); l.setStrokeWidth(strokeWidth); - group.getChildren().add(l); + pane.getChildren().add(l); } // Line coming out the right if (x < map[0].length - 1 && map[y][x + 1] != null) { Line l = new Line(); l.setStartX(leftPadding + x * cellDimension + cellDimension - innerCellPadding + strokeWidthCorrection); l.setEndX(leftPadding + x * cellDimension + cellDimension - strokeWidthCorrection); l.setStartY(yMidpoint); l.setEndY(yMidpoint); - l.setStroke(PATH_COLOR); + l.setStroke(ColorConstants.SNOW_ALT); l.setStrokeWidth(strokeWidth); - group.getChildren().add(l); + pane.getChildren().add(l); } } } } } } } ADDED src/main/java/com/mg105/user_interface/MultiselectGrid.java Index: src/main/java/com/mg105/user_interface/MultiselectGrid.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/user_interface/MultiselectGrid.java @@ -0,0 +1,234 @@ +package com.mg105.user_interface; + +import javafx.scene.Parent; +import javafx.scene.layout.GridPane; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Multiselect grid represents a grid of keyboard controllable virtual buttons. + *

+ * One option in each column can be selected, and there is only one active item. + */ +public class MultiselectGrid { + private final @NotNull StandardComponentFactory componentFactory; + private final @NotNull GridPane grid; + private final @NotNull List> columns; + private final @NotNull List selected; + private @NotNull Point active; + + /** + * Create a new MultiselectGrid. + * + * @param componentFactory the component factory used to create components + */ + public MultiselectGrid(@NotNull StandardComponentFactory componentFactory) { + this.componentFactory = componentFactory; + grid = new GridPane(); + columns = new ArrayList<>(); + selected = new ArrayList<>(); + active = new Point(); + } + + /** + * Get the actual displayable grid. + * + * @return the grid. + */ + public @NotNull GridPane getGrid() { + return grid; + } + + /** + * Update the visual representation of the grid. + */ + private void updateGrid() { + grid.getChildren().clear(); + + for (int x = 0; x < columns.size(); x++) { + for (int y = 0; y < columns.get(x).size(); y++) { + grid.add(columns.get(x).get(y), x * 3 + 1, y); + } + } + + for (int x = 0; x < selected.size(); x++) { + if (selected.get(x) != null) { + grid.add(componentFactory.createActiveLabel(" [ "), x * 3, selected.get(x)); + grid.add(componentFactory.createActiveLabel(" ] "), x * 3 + 2, selected.get(x)); + } + } + + grid.add(componentFactory.createActiveLabel(" > "), active.x * 3, active.y); + grid.add(componentFactory.createActiveLabel(" < "), active.x * 3 + 2, active.y); + } + + /** + * Add a column to the end of the grid. + * + * @param items the contents of the column. + */ + public void addColumn(@NotNull Parent... items) { + columns.add(new ArrayList<>(List.of(items))); + updateGrid(); + } + + /** + * Add an item to the last column. + * + * @param item the item to add. + */ + public void addToColumn(@NotNull Parent item) { + assert columns.size() > 0; + columns.get(columns.size() - 1).add(item); + updateGrid(); + } + + /** + * Remove the last column fromm the grid. + */ + public void popColumn() { + assert columns.size() > 0; + columns.remove(columns.size() - 1); + if (active.x == columns.size()) { + activeLeft(); + } + while (selected.size() >= columns.size()) { + selected.remove(selected.size() - 1); + } + updateGrid(); + } + + /** + * Set next as the next active point. + * + * @param next the next active position. + * + * @return true if the active position changed, false otherwise. + */ + private boolean setActive(@NotNull Point next) { + try { + if (columns.get(next.x).get(next.y) != null && !active.equals(next)) { + active = new Point(next); + updateGrid(); + return true; + } else { + return false; + } + } catch (IndexOutOfBoundsException e) { + return false; + } + } + + /** + * Get the active location. + * + * @return the active location. + */ + public @NotNull Point getActive() { + return new Point(active); + } + + /** + * Move the active location up by 1 unit. + * + * @return true if the active position changed, false otherwise. + */ + public boolean activeUp() { + Point next = new Point(active); + next.y -= 1; + return setActive(next); + } + + /** + * Move the active location down by 1 unit. + * + * @return true if the active position changed, false otherwise. + */ + public boolean activeDown() { + Point next = new Point(active); + next.y += 1; + return setActive(next); + } + + /** + * Sets the active point to start or a point above it. + * + * @param start a starting point. + * + * @return true if the active position changed, false otherwise. + */ + private boolean setClosestActive(Point start) { + if (setActive(start)) { + return true; + } else if (start.y >= 0) { + start.y -= 1; + return setClosestActive(start); + } else { + return false; + } + } + + /** + * Move the active location left by 1 unit. + * + * @return true if the active position changed, false otherwise. + */ + public boolean activeLeft() { + Point next = new Point(active); + next.x -= 1; + return setClosestActive(next); + } + + /** + * Move the active location right by 1 unit. + * + * @return true if the active position changed, false otherwise. + */ + public boolean activeRight() { + Point next = new Point(active); + next.x += 1; + return setClosestActive(next); + } + + /** + * Set the current selected as active. + */ + public void selectActive() { + while (selected.size() < columns.size()) { + selected.add(null); + } + + selected.set(active.x, active.y); + } + + /** + * Get the selected elements so far. + * + * @return the selected elements so far. + */ + public @NotNull List getSelected() { + return new ArrayList<>(selected); + } + + /** + * Reset the multiselect grid. + */ + public void reset() { + active.x = 0; + active.y = 0; + selected.clear(); + columns.clear(); + } + + /** + * Gets if this grid is empty. + * + * @return if the mutiselect grid contains no columns. + */ + public boolean isEmpty() { + return columns.isEmpty(); + } +} Index: src/main/java/com/mg105/user_interface/ReplayGeneratorButton.java ================================================================== --- src/main/java/com/mg105/user_interface/ReplayGeneratorButton.java +++ src/main/java/com/mg105/user_interface/ReplayGeneratorButton.java @@ -1,18 +1,16 @@ package com.mg105.user_interface; import com.mg105.interface_adapters.ReplayGeneratorInterpreter; import com.mg105.interface_adapters.Toggler; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; import org.jetbrains.annotations.NotNull; /** * ReplayGeneratorButton acts as the event handler for the actual JavaFx button that will replay the game. */ -public class ReplayGeneratorButton implements EventHandler { +public class ReplayGeneratorButton implements Runnable { private final @NotNull ReplayGeneratorInterpreter interpreter; private final @NotNull Toggler toggler; private final @NotNull Toggler.ToggleableComponent componentToToggle; /** @@ -26,17 +24,12 @@ this.interpreter = interpreter; this.toggler = toggler; this.componentToToggle = componentToToggle; } - /** - * this method is called when the button is pressed. It passes control to the appropriate interpreter. - * - * @param event the event which occurred - */ @Override - public void handle(ActionEvent event) { + public void run() { interpreter.replayGenerateMap(); toggler.toggle(componentToToggle); + toggler.toggle(Toggler.ToggleableComponent.MAIN_MENU); } - } Index: src/main/java/com/mg105/user_interface/SceneController.java ================================================================== --- src/main/java/com/mg105/user_interface/SceneController.java +++ src/main/java/com/mg105/user_interface/SceneController.java @@ -1,24 +1,36 @@ package com.mg105.user_interface; +import com.mg105.interface_adapters.InputControllable; import com.mg105.interface_adapters.Toggler; +import com.mg105.utils.UIConstants; +import javafx.animation.AnimationTimer; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; import javafx.stage.Stage; import org.jetbrains.annotations.NotNull; +import java.util.LinkedList; import java.util.Map; +import java.util.Queue; import java.util.Stack; /** * SceneController acts as the central coordination mechanism for the user interface. */ -public class SceneController implements Toggler { +public class SceneController implements Toggler, Alerter, InputControllable { private final @NotNull Stage primaryStage; + private final @NotNull StackPane layout; private final @NotNull Map components; private final @NotNull ToggleableComponent defaultComponent; private final @NotNull Stack activeComponents; + + private final @NotNull Queue alerts; /** * Create a new scene controller * * @param primaryStage the JavaFX stage to draw on. @@ -33,10 +45,22 @@ this.defaultComponent = defaultComponent; this.components = components; activeComponents = new Stack<>(); activeComponents.push(defaultComponent); + + layout = new StackPane(); + layout.setPrefSize(UIConstants.CANVAS_SIZE, UIConstants.CANVAS_SIZE); + layout.setMaxSize(UIConstants.CANVAS_SIZE, UIConstants.CANVAS_SIZE); + layout.getChildren().add(new Pane()); + + primaryStage.setScene(new Scene( + layout, + UIConstants.CANVAS_SIZE, UIConstants.CANVAS_SIZE + )); + + alerts = new LinkedList<>(); } /** * Toggle the component. i.e. switch it between invisible and visible. * @@ -55,11 +79,20 @@ if (activeComponents.isEmpty()) { activeComponents.push(defaultComponent); } components.get(activeComponents.peek()).toggle(true); - primaryStage.setScene(components.get(activeComponents.peek()).getScene()); + + Pane nextComponent = components.get(activeComponents.peek()).getScene(); + layout.getChildren().set(0, nextComponent); + + // force alignment to top left and try to force appropriate sizing + nextComponent.setTranslateX(-nextComponent.getLayoutX()); + nextComponent.setTranslateY(-nextComponent.getLayoutY()); + nextComponent.setPrefSize(UIConstants.CANVAS_SIZE, UIConstants.CANVAS_SIZE); + nextComponent.setMaxSize(UIConstants.CANVAS_SIZE, UIConstants.CANVAS_SIZE); + primaryStage.setTitle("Mountain Group 105: " + activeComponents.peek()); } /** * Get the current visible component. @@ -66,8 +99,52 @@ * * @return the current visible component. */ @Override public @NotNull ToggleableComponent getCurrentComponent() { - return activeComponents.peek(); + if (layout.getChildren().size() > 1) { + return ToggleableComponent.ALERT; + } else { + return activeComponents.peek(); + } + } + + /** + * Display the alert on top of the current screen. + * + * @param body the alert. + */ + @Override + public void displayAlert(@NotNull Parent body) { + alerts.add(body); + + if (layout.getChildren().size() == 1) { + layout.getChildren().add(alerts.remove()); + } + } + + /** + * Interpret a key. Used for the sole purpose of dismissing alerts. + * + * @param key the key this component should process. + */ + @Override + public void interpret(String key) { + layout.getChildren().remove(1); + + if (!alerts.isEmpty()) { + new AnimationTimer() { + private long last = 0; + + @Override + public void handle(long now) { + if (last == 0) { + last = now; + } else if (now - last > 170 * 1e6) { + layout.getChildren().add(alerts.remove()); + stop(); + } + } + }.start(); + } } } ADDED src/main/java/com/mg105/user_interface/StandardComponentFactory.java Index: src/main/java/com/mg105/user_interface/StandardComponentFactory.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/user_interface/StandardComponentFactory.java @@ -0,0 +1,205 @@ +package com.mg105.user_interface; + +import com.mg105.utils.ColorConstants; +import com.mg105.utils.UIConstants; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.effect.DropShadow; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import org.jetbrains.annotations.NotNull; + +/** + * The StandardComponentFactory is used to delegate creation of JavaFX UI components so that they all look coherent. + */ +public class StandardComponentFactory { + private final @NotNull Alerter alerter; + + /** + * Create a StandardComponentFactory. + * + * @param alerter the interface used to alert. + */ + public StandardComponentFactory(@NotNull Alerter alerter) { + this.alerter = alerter; + } + + /** + * Create a new background. + * + * @param isLight if the background should be light (otherwise it is dark). + * + * @return the new background. + */ + public @NotNull Background createBackground(boolean isLight) { + Color backgroundColor = (isLight ? ColorConstants.SNOW : ColorConstants.WALL); + return new Background(new BackgroundFill(backgroundColor, null, null)); + } + + /** + * Create a label. + * + * @param text the label text. + * @param isLight if the background is light (otherwise it is presumed dark). + * + * @return a new label. + */ + public @NotNull Label createLabel(@NotNull String text, boolean isLight) { + Label l = new Label(text); + l.setTextFill((isLight ? ColorConstants.WALL : ColorConstants.SNOW)); + l.setFont(UIConstants.FONT_NORMAL); + return l; + } + + /** + * Create an active label. + * + * @param text the text of the label. + * + * @return a new label. + */ + public @NotNull Label createActiveLabel(@NotNull String text) { + Label l = createLabel(text, true); + l.setTextFill(ColorConstants.ACTIVE); + return l; + } + + /** + * Create a title label. + * + * @param text the label text. + * @param isLight if the background is light (otherwise it is presumed dark). + * + * @return a new label. + */ + public @NotNull Label createTitleLabel(@NotNull String text, boolean isLight) { + Label l = createLabel(text, isLight); + l.setFont(UIConstants.TITLE_FONT); + return l; + } + + /** + * Create a label representing a number. + * + * @param num the label number. + * @param isLight if the background is light (otherwise it is presumed dark). + * + * @return a new label. + */ + public @NotNull Label createNumberedLabel(int num, boolean isLight) { + Label l = createLabel((num > 0 ? "+" : "") + Integer.toString(num), isLight); + if (num > 0) { + l.setTextFill(Color.GREEN); + } else if (num < 0) { + l.setTextFill(Color.RED); + } + return l; + } + + /** + * Create a subtitle label. + * + * @param text the label text. + * @param isLight if the background is light (otherwise it is presumed dark). + * + * @return a new label. + */ + public @NotNull Label createSubtitleLabel(@NotNull String text, boolean isLight) { + Label l = createLabel(text, isLight); + l.setFont(UIConstants.SUBTITLE_FONT); + return l; + } + + /** + * Create a new multiselect grid. + * + * @return a new multiselect grid. + */ + public @NotNull MultiselectGrid createMultiselectGrid() { + return new MultiselectGrid(this); + } + + /** + * Create a growing spacer. + * + * @return a new spacer + */ + public @NotNull Region createSpacer() { + Region r = new Region(); + r.setPrefSize(UIConstants.SINGLE_GAP, UIConstants.SINGLE_GAP); + VBox.setVgrow(r, Priority.ALWAYS); + HBox.setHgrow(r, Priority.ALWAYS); + return r; + } + + /** + * Create a larger spacer. + * + * @return a larger spacer. + */ + public @NotNull Region createFixedDoubleSpacer() { + Region r = new Region(); + r.setPrefSize(2 * UIConstants.SINGLE_GAP, 2 * UIConstants.SINGLE_GAP); + return r; + } + + /** + * Create a small spacer. + * + * @return a smaller spacer. + */ + public @NotNull Region createFixedHalfSpacer() { + Region r = new Region(); + r.setPrefSize((int)(UIConstants.SINGLE_GAP / 2), (int)(UIConstants.SINGLE_GAP / 2)); + return r; + } + + /** + * Create an alerting modal dialog. + * + * @param text the text of the alert. + * @param isLight if the background should be light. + */ + public void createAlert(@NotNull String text, boolean isLight) { + DropShadow shadow = new DropShadow(); + shadow.setOffsetY((int)(UIConstants.SINGLE_GAP / 2)); + shadow.setColor(Color.color(0.4, 0.4, 0.5)); + + TilePane pane = new TilePane(Orientation.VERTICAL); + pane.setBackground(createBackground(isLight)); + pane.setAlignment(Pos.CENTER); + pane.setPadding(createInsets()); + pane.setVgap(UIConstants.SINGLE_GAP); + pane.setMaxWidth((int)(UIConstants.CANVAS_SIZE / 2)); + pane.setMaxHeight((int)(UIConstants.CANVAS_SIZE / 2)); + pane.setEffect(shadow); + pane.setBorder(new Border(new BorderStroke( + (isLight ? ColorConstants.WALL : ColorConstants.SNOW), + BorderStrokeStyle.SOLID, + CornerRadii.EMPTY, + new BorderWidths((int)(UIConstants.SINGLE_GAP / 4)) + ))); + + Label message = createLabel(text, isLight); + message.setWrapText(true); + pane.getChildren().add(message); + + MultiselectGrid okButton = createMultiselectGrid(); + okButton.getGrid().setAlignment(Pos.CENTER); + okButton.addColumn(createLabel("Ok", isLight)); + pane.getChildren().add(okButton.getGrid()); + + alerter.displayAlert(pane); + } + + /** + * Create standard insets. + * + * @return standard insets. + */ + public @NotNull Insets createInsets() { + return new Insets(UIConstants.SINGLE_GAP); + } +} Index: src/main/java/com/mg105/user_interface/Toggleable.java ================================================================== --- src/main/java/com/mg105/user_interface/Toggleable.java +++ src/main/java/com/mg105/user_interface/Toggleable.java @@ -1,8 +1,8 @@ package com.mg105.user_interface; -import javafx.scene.Scene; +import javafx.scene.layout.Pane; import org.jetbrains.annotations.NotNull; /** * A user interface component that can be toggled on or off depending on its visibility. */ @@ -10,15 +10,15 @@ /** * Get the scene of this toggleable object. It is this scene that will be displayed. * * @return the scene to be displayed. */ - @NotNull Scene getScene(); + @NotNull Pane getScene(); /** * Set the visibility of this component. * * @param isVisible true if the Toggleable is now visible, false otherwise. If false the Toggleable is expected * to do nothing on ANY user inputs. */ void toggle(boolean isVisible); } DELETED src/main/java/com/mg105/user_interface/TutorialTextDisplay.java Index: src/main/java/com/mg105/user_interface/TutorialTextDisplay.java ================================================================== --- src/main/java/com/mg105/user_interface/TutorialTextDisplay.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.mg105.user_interface; - -import com.mg105.interface_adapters.tutorial.TutorialTextController; -import com.mg105.utils.TutorialTexts; -import javafx.scene.control.Label; -import javafx.scene.text.Font; - -/** - * Returns the actual text that is displayed to the player - */ -public class TutorialTextDisplay { - private final TutorialTextController tutorialControl = new TutorialTextController(false); - - /** - * Constructor for a new TutorialTextDisplay ui - */ - public TutorialTextDisplay() { - } - - /** - * Check if specific action has been performed - * - * @param displayedText the String representing the current phase - * @return the actual text displayed to the user - */ - public String showBottomText(String displayedText) { - String tutorialText; - int phase_idx = tutorialControl.allPhases().indexOf(displayedText); - tutorialText = TutorialTexts.PHASES_TEXT.get(phase_idx); - return tutorialText; - - } - - /** - * Return a label that will show at the bottom of the screen. - * - * @return the label - */ - public Label tutorialLabel() { - Label bottomText = new Label(); - bottomText.setFont(Font.font(TutorialTexts.TEXT_SIZE)); - return bottomText; - } - - /** - * Get a tutorial controller which is used to switch text - * - * @return instance of the controller - */ - public TutorialTextController getController() { - return this.tutorialControl; - } -} - - DELETED src/main/java/com/mg105/user_interface/TutorialTextWindow.java Index: src/main/java/com/mg105/user_interface/TutorialTextWindow.java ================================================================== --- src/main/java/com/mg105/user_interface/TutorialTextWindow.java +++ /dev/null @@ -1,130 +0,0 @@ -package com.mg105.user_interface; - -import com.mg105.interface_adapters.tutorial.TutorialTextController; -import com.mg105.utils.TutorialTexts; -import javafx.animation.AnimationTimer; -import javafx.geometry.Insets; -import javafx.scene.Scene; -import javafx.scene.control.Label; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.Pane; -import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import org.jetbrains.annotations.NotNull; - -/** - * Create a window for the tutorial scenes - */ -public class TutorialTextWindow implements Toggleable { - final TutorialTextDisplay tutorialDisplay; - final Pane tutorialPane = new Pane(); - final Label bottomText = new Label(); - final Label helpText; - final TutorialTextController textController; - final Scene tutorialScene = new Scene(tutorialPane, TutorialTexts.HELPER_PANE_X, TutorialTexts.HELPER_PANE_Y); - - /** - * Window for the tutorial - * - * @param textController the controller for the tutorial - * @param tutorialDisplay the ui that displays the tutorial - */ - public TutorialTextWindow(TutorialTextController textController, @NotNull TutorialTextDisplay tutorialDisplay) { - this.textController = textController; - this.tutorialDisplay = tutorialDisplay; - - bottomText.setFont(Font.font(TutorialTexts.TEXT_SIZE)); - - helpText = tutorialDisplay.tutorialLabel(); - - helpText.setFont(Font.font(TutorialTexts.TEXT_SIZE)); - helpText.setBackground(new Background(new BackgroundFill(Color.rgb(255, 215, 0, 1), - new CornerRadii(80.0), new Insets(-100.0)))); - - tutorialPane.getChildren().add(bottomText); - tutorialPane.getChildren().add(helpText); - - AnimationTimer timer = new TutorialTimer(); - timer.start(); - } - - /** - * Get the scene of this toggleable object. It is this scene that will be displayed. - * - * @return the scene to be displayed. - */ - @Override - public @NotNull Scene getScene() { - return tutorialScene; - } - - /** - * Set the visibility of this component. - * - * @param isVisible true if the Toggleable is now visible, false otherwise. If false the Toggleable is expected - * to do nothing on ANY user inputs. - */ - @Override - public void toggle(boolean isVisible) { - } - - private class TutorialTimer extends AnimationTimer { - private long prevTime = 0; - private double helpTimer = TutorialTexts.HELP_TIME; - - /** - * This method needs to be overridden by extending classes. It is going to - * be called in every frame while the {@code AnimationTimer} is active. - * - * @param now The timestamp of the current frame given in nanoseconds. This - * value will be the same for all {@code AnimationTimers} called - * during one frame. - */ - @Override - public void handle(long now) { - long timeChange = now - prevTime; - - // 1e9 is 1 second - if (timeChange > TutorialTexts.TEXT_DURATION1 * 1e9 & textController.changeText()) { - prevTime = now; - tutorialDisplay.getController().nextPhase(); - String tutorialText = tutorialDisplay.getController().bottomText(); - bottomText.setText(tutorialDisplay.showBottomText(tutorialText)); - } - - if (textController.getShowControls() & textController.isComplete()) { - helpText.setText(TutorialTexts.CONTROLS); - helpTimer--; - if (helpTimer < 1) { - textController.setShowControls(false); - helpTimer = TutorialTexts.HELP_TIME; - } - } else if (textController.getShowControls()) { - if (textController.getActionPerformed(TutorialTexts.MOVED)) { - helpText.setText(TutorialTexts.DID_NOT_MOVE); - } else if (textController.getActionPerformed(TutorialTexts.USED_ITEM)) { - helpText.setText(TutorialTexts.DID_NOT_OPEN_CHEST); - } else { - helpText.setText(TutorialTexts.DID_NOT_BATTLE); - } - - helpTimer--; - if (helpTimer < 1) { - textController.setShowControls(false); - helpTimer = TutorialTexts.HELP_TIME; - } - - } else { - helpText.setText(""); - } - - } - } -} - - - - - Index: src/main/java/com/mg105/user_interface/WalkingMenu.java ================================================================== --- src/main/java/com/mg105/user_interface/WalkingMenu.java +++ src/main/java/com/mg105/user_interface/WalkingMenu.java @@ -1,106 +1,67 @@ package com.mg105.user_interface; +import com.mg105.interface_adapters.InputControllable; +import com.mg105.interface_adapters.Toggler; import com.mg105.interface_adapters.WalkVisController; import com.mg105.utils.PartyConstants; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; +import com.mg105.utils.Textures; +import com.mg105.utils.UIConstants; +import javafx.geometry.Orientation; import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.RadioButton; -import javafx.scene.control.ToggleGroup; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.layout.TilePane; import org.jetbrains.annotations.NotNull; /** * This class uses JavaFX and is displayed when the user wants to change the walking character sprite. */ -public class WalkingMenu implements EventHandler, Toggleable { +public class WalkingMenu implements InputControllable, Toggleable { private final WalkVisController controller; - private final RadioButton radA; - private final RadioButton radB; - private final RadioButton radC; - private final RadioButton radD; - private final Button select; - private final Scene scene; + private final TilePane layout; + private final @NotNull ImageView nextSelected; + private final @NotNull MultiselectGrid options; + private final @NotNull Toggler toggler; /** * Creates a new WalkingMenu with reference to a controller class. * Sets up UI elements. * - * @param controller the WalkVisController to be referred to. - */ - public WalkingMenu(WalkVisController controller) { - this.controller = controller; - - StackPane layout = new StackPane(); - - ToggleGroup group = new ToggleGroup(); - - VBox buttons = new VBox(); - - radA = new RadioButton(PartyConstants.ALL_PARTY_MEMBER_NAMES[0]); - radA.setToggleGroup(group); - radA.setSelected(true); - - radB = new RadioButton(PartyConstants.ALL_PARTY_MEMBER_NAMES[1]); - radB.setToggleGroup(group); - radB.setSelected(false); - - radC = new RadioButton(PartyConstants.ALL_PARTY_MEMBER_NAMES[2]); - radC.setToggleGroup(group); - radC.setSelected(false); - - radD = new RadioButton(PartyConstants.ALL_PARTY_MEMBER_NAMES[3]); - radD.setToggleGroup(group); - radD.setSelected(false); - - select = new Button("Confirm"); - select.setOnAction(this); - - Label exitLbl = new Label("Press space bar to exit."); - - buttons.getChildren().addAll(radA, radB, radC, radD, select, exitLbl); - buttons.setAlignment(Pos.CENTER); - layout.getChildren().addAll(buttons); - - scene = new Scene(layout, 300, 300); - } - - /** - * Handles button events. - * Pressing the select button updates the WalkingCharacter to represent the desired walking character sprite. - * - * @param event the event which occurred. - */ - @Override - public void handle(ActionEvent event) { - Object source = event.getSource(); - if (source.equals(select)) { - if (radA.isSelected()) { - controller.changePlayerSprite("A"); - } else if (radB.isSelected()) { - controller.changePlayerSprite("B"); - } else if (radC.isSelected()) { - controller.changePlayerSprite("C"); - } else if (radD.isSelected()) { - controller.changePlayerSprite("D"); - } - } + * @param controller the WalkVisController to be referred to. + * @param toggler the toggler used to toggle this menu. + * @param componentFactory the component factory used to create UI components + */ + public WalkingMenu(@NotNull WalkVisController controller, @NotNull Toggler toggler, + @NotNull StandardComponentFactory componentFactory) { + this.controller = controller; + this.toggler = toggler; + + layout = new TilePane(Orientation.VERTICAL); + layout.setVgap(UIConstants.SINGLE_GAP); + layout.setAlignment(Pos.CENTER); + layout.setBackground(componentFactory.createBackground(true)); + + nextSelected = new ImageView(Textures.PLAYER_A); + layout.getChildren().add(nextSelected); + + options = componentFactory.createMultiselectGrid(); + options.addColumn(); + for (String name : PartyConstants.ALL_PARTY_MEMBER_NAMES) { + options.addToColumn(componentFactory.createLabel(name, true)); + } + layout.getChildren().add(options.getGrid()); } /** * Get the scene of this toggleable object. It is this scene that will be displayed. * * @return the scene to be displayed. */ @Override - public @NotNull Scene getScene() { - return this.scene; + public @NotNull Pane getScene() { + return layout; } /** * Set the visibility of this component. * @@ -109,6 +70,39 @@ */ @Override public void toggle(boolean isVisible) { //The menu should ideally remain the same. } + + @Override + public void interpret(String key) { + switch (key) { + case "w" -> { + options.activeUp(); + updateImage(options.getActive().y); + } + case "s" -> { + options.activeDown(); + updateImage(options.getActive().y); + } + case " " -> { + controller.changePlayerSprite(Character.toString('A' + options.getActive().y)); + toggler.toggle(Toggler.ToggleableComponent.WALK_MENU); + } + } + } + + /** + * Update the visual image. + * + * @param id the id of the new image. + */ + private void updateImage(int id) { + nextSelected.setImage(switch (id) { + case 0 -> Textures.PLAYER_A; + case 1 -> Textures.PLAYER_B; + case 2 -> Textures.PLAYER_C; + case 3 -> Textures.PLAYER_D; + default -> Textures.MISSING; + }); + } } Index: src/main/java/com/mg105/user_interface/WinMenu.java ================================================================== --- src/main/java/com/mg105/user_interface/WinMenu.java +++ src/main/java/com/mg105/user_interface/WinMenu.java @@ -1,58 +1,82 @@ package com.mg105.user_interface; -import com.mg105.utils.TutorialTexts; +import com.mg105.interface_adapters.InputControllable; +import com.mg105.utils.UIConstants; +import javafx.geometry.Orientation; import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; -import javafx.scene.text.Font; +import javafx.scene.layout.TilePane; import org.jetbrains.annotations.NotNull; /** * A menu that is shown to the player after they have reached the final room */ -public class WinMenu implements Toggleable { - private final @NotNull Scene scene; +public class WinMenu implements Toggleable, InputControllable { + private boolean isVisible = false; + private final @NotNull Runnable replayButton; + private final @NotNull TilePane layout; + private final @NotNull MainMenu mainMenu; + private final @NotNull Label message; /** * Shows a button that congratulates you and lets you replay the game * - * @param ReplayButton the button responsible for replaying the game + * @param replayButton the button responsible for replaying the game + * @param componentFactory the component factory used to create UI components + * @param mainMenu the main menu component. */ - public WinMenu(@NotNull ReplayGeneratorButton ReplayButton) { - Label winLbl = new Label(); - winLbl.setText("Congratulations on reaching the final room!!"); - winLbl.setFont(Font.font(TutorialTexts.TEXT_SIZE_LARGE)); - StackPane.setAlignment(winLbl, Pos.TOP_CENTER); - - Button generateMapButton = new Button("You Won! Replay The Game"); - generateMapButton.setOnAction(ReplayButton); - - @NotNull Pane layout = new StackPane(); - layout.getChildren().add(generateMapButton); - layout.getChildren().add(winLbl); - scene = new Scene(layout, 600, 600); + public WinMenu(@NotNull Runnable replayButton, @NotNull StandardComponentFactory componentFactory, + @NotNull MainMenu mainMenu) { + this.replayButton = replayButton; + this.mainMenu = mainMenu; + + layout = new TilePane(Orientation.VERTICAL); + layout.setBackground(componentFactory.createBackground(true)); + layout.setVgap(UIConstants.SINGLE_GAP); + layout.setAlignment(Pos.CENTER); + + message = componentFactory.createLabel("UNSET", true); + + layout.getChildren().addAll( + componentFactory.createTitleLabel("Summit Conquered", true), + message + ); + + MultiselectGrid options = componentFactory.createMultiselectGrid(); + options.addColumn(componentFactory.createLabel("Now what...", true)); + options.getGrid().setAlignment(Pos.CENTER); + layout.getChildren().add(options.getGrid()); } /** * Get scene of the WinMenu * * @return the scene that WinMenu is in */ @Override - public @NotNull Scene getScene() { - return scene; + public @NotNull Pane getScene() { + return layout; } /** * Toggle the WinMenu * * @param isVisible whether the menu is visible */ @Override public void toggle(boolean isVisible) { - // Does nothing, for now + this.isVisible = isVisible; + + if (isVisible) { + message.setText("The venerated Mountain Group " + mainMenu.getPlayCount() + " does it again!"); + } + } + + @Override + public void interpret(String key) { + if (isVisible && key.equals(" ")) { + replayButton.run(); + } } } ADDED src/main/java/com/mg105/utils/ColorConstants.java Index: src/main/java/com/mg105/utils/ColorConstants.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/utils/ColorConstants.java @@ -0,0 +1,26 @@ +package com.mg105.utils; + +import javafx.scene.paint.Color; +import org.jetbrains.annotations.NotNull; + +/** + * The colour palette of the game. + */ +public class ColorConstants { + /** + * The colour used for wall (out of bounds) areas. + */ + public static final @NotNull Color WALL = Color.rgb(38, 44, 68); + /** + * The colour used for snow. + */ + public static final @NotNull Color SNOW = Color.rgb(205, 220, 255); + /** + * The accent colour used for snow. + */ + public static final @NotNull Color SNOW_ALT = Color.rgb(135, 160, 227); + /** + * The colour used when something is active. + */ + public static final @NotNull Color ACTIVE = Color.rgb(200, 150, 61); +} ADDED src/main/java/com/mg105/utils/Textures.java Index: src/main/java/com/mg105/utils/Textures.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/utils/Textures.java @@ -0,0 +1,58 @@ +package com.mg105.utils; + +import javafx.scene.image.Image; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * The textures (images) that are used in the game. + */ +public class Textures { + /** + * The texture if none is found. + */ + public static @NotNull Image MISSING = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/missing.png"))); + + /** + * The floor image. + */ + public static @NotNull Image TILE_FLOOR = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/floor.png"))); + /** + * The wall image. + */ + public static @NotNull Image TILE_WALL = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/wall.png"))); + /** + * The wall's face image. + */ + public static @NotNull Image TILE_WALL_FACE = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/wall_face.png"))); + /** + * The tile used for a closed chest. + */ + public static @NotNull Image TILE_CHEST = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/chest.png"))); + /** + * The tile used for an open chest. + */ + public static @NotNull Image TILE_CHEST_OPEN = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/chest_open.png"))); + /** + * The tile used for an opponent set. + */ + public static @NotNull Image TILE_OPPONENT_SET = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/tiles/battle.png"))); + + /** + * The tile for player A. + */ + public static @NotNull Image PLAYER_A = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/sprites/A.png"))); + /** + * The tile for player B. + */ + public static @NotNull Image PLAYER_B = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/sprites/B.png"))); + /** + * The tile for player C. + */ + public static @NotNull Image PLAYER_C = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/sprites/C.png"))); + /** + * The tile for player D. + */ + public static @NotNull Image PLAYER_D = new Image(Objects.requireNonNull(Textures.class.getResourceAsStream("/sprites/D.png"))); +} DELETED src/main/java/com/mg105/utils/TutorialTexts.java Index: src/main/java/com/mg105/utils/TutorialTexts.java ================================================================== --- src/main/java/com/mg105/utils/TutorialTexts.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.mg105.utils; - -import java.util.Arrays; -import java.util.List; - -/** - * Constants for texts related to the tutorial - */ -public class TutorialTexts { - /** - * Constant string for move phase - */ - public static final String MOVED = "moved"; - /** - * Constant string for attack phase - */ - public static final String ATTACKED = "attacked"; - /** - * Constant string for using item phase - */ - public static final String USED_ITEM = "usedItem"; - /** - * Constant int for x size of helper pane - */ - public static final int HELPER_PANE_X = 420; - - /** - * Constant int for y size of helper pane - */ - public static final int HELPER_PANE_Y = 100; - - /** - * Constant int for text duration per phase - */ - public static final int TEXT_DURATION1 = 3; - - /** - * Constant int for size of the regular text - */ - public static final int TEXT_SIZE = 14; - - /** - * Constant int for size of the larger text - */ - public static final int TEXT_SIZE_LARGE = 20; - - /** - * Constant int for text duration of helper text (not in seconds) - */ - public static final int HELP_TIME = 250; - - /** - * Constant array listing all the phases - */ - public static final List PHASES - = Arrays.asList("...", "story", "tell move", "tell attack", "tell use item", "exit room", "hotkeys"); - /** - * Constant string for story text - */ - public static final String STORY = """ - Welcome to mountain climber. You must battle your way - to the top of the mountain. Inside various rooms - you will encounter enemies and find treasures."""; - /** - * Constant string for move text - */ - public static final String TELL_MOVE = "Move your character with the WASD keys."; - /** - * Constant string for attack text - */ - public static final String TELL_ATTACK = "Use the battle button [f] to attack the enemies."; - /** - * Constant string for open chest text - */ - public static final String TELL_USE_ITEM = "Open treasure chests using the [e] key.."; - /** - * Constant string for good luck text - */ - public static final String EXIT_ROOM = "Good luck on your journey!"; - /** - * Constant string for telling player the game controls - */ - public static final String CONTROLS = """ - Hotkeys: Press K for help. Press WASD to return to game.\s - Game Controls: Use WASD to move. Use battle key [f] to \s - initiate battles. Open chests using [e]."""; - /** - * Constant string for telling the player the hotkeys - */ - public static final String HOTKEYS = "Hint: Press [WASD] to return to the game. Press K for help!"; - /** - * Constant array containing the actual text shown to the player - */ - public static final List PHASES_TEXT = Arrays.asList("", - TutorialTexts.STORY, TutorialTexts.TELL_MOVE, TutorialTexts.TELL_ATTACK, TutorialTexts.TELL_USE_ITEM, TutorialTexts.EXIT_ROOM, TutorialTexts.HOTKEYS); - /** - * Constant string reminding player how to move - */ - public static final String DID_NOT_MOVE = """ - You haven't completed the tutorial yet ... - - You should return to the game and move you character with [WASD]! - (Pressing WASD in this window will return you to the game.) - """; - - /** - * Constant string for reminding player to open chest - */ - public static final String DID_NOT_OPEN_CHEST = """ - You haven't completed the tutorial yet ... - - You should go open a chest! Do this using the [e] key. - (Pressing WASD in this window will return you to the game.) - """; - - /** - * Constant string for reminding player to initiate a battle - */ - public static final String DID_NOT_BATTLE = """ - You haven't completed the tutorial yet ... - - You should go fight a battle! Use [f] key to initiate a battle. - (Pressing WASD in this window will return you to the game.) - """; -} ADDED src/main/java/com/mg105/utils/UIConstants.java Index: src/main/java/com/mg105/utils/UIConstants.java ================================================================== --- /dev/null +++ src/main/java/com/mg105/utils/UIConstants.java @@ -0,0 +1,37 @@ +package com.mg105.utils; + +import javafx.scene.text.Font; + +/** + * General user interface constants. + */ +public class UIConstants { + /** + * The width and height of the entire canvas. + */ + public static final int CANVAS_SIZE = MapConstants.TILE_SIZE * MapConstants.ROOM_SIZE; + /** + * The width of a line gap. + */ + public static final int SINGLE_GAP = CANVAS_SIZE / 32; + /** + * The standard font. + */ + public static final Font FONT_NORMAL = Font.loadFont(UIConstants.class.getResourceAsStream("/fonts/Strait-Regular.ttf"), 19); + /** + * The title font. + */ + public static final Font TITLE_FONT = Font.loadFont(UIConstants.class.getResourceAsStream("/fonts/Strait-Regular.ttf"), 30); + /** + * The title font used only for the number. + */ + public static final Font TITLE_FONT_ALT = Font.loadFont(UIConstants.class.getResourceAsStream("/fonts/HomemadeApple-Regular.ttf"), 35); + /** + * The subtitle font. + */ + public static final Font SUBTITLE_FONT = Font.loadFont(UIConstants.class.getResourceAsStream("/fonts/Strait-Regular.ttf"), 22); + /** + * The keyboard font. + */ + public static final Font FONT_MONO = Font.loadFont(UIConstants.class.getResourceAsStream("/fonts/IBMPlexMono-Medium.ttf"), 19); +} ADDED src/main/resources/fonts/HomemadeApple-Regular.ttf Index: src/main/resources/fonts/HomemadeApple-Regular.ttf ================================================================== --- /dev/null +++ src/main/resources/fonts/HomemadeApple-Regular.ttf cannot compute difference between binary files ADDED src/main/resources/fonts/IBMPlexMono-Medium.ttf Index: src/main/resources/fonts/IBMPlexMono-Medium.ttf ================================================================== --- /dev/null +++ src/main/resources/fonts/IBMPlexMono-Medium.ttf cannot compute difference between binary files ADDED src/main/resources/fonts/Strait-Regular.ttf Index: src/main/resources/fonts/Strait-Regular.ttf ================================================================== --- /dev/null +++ src/main/resources/fonts/Strait-Regular.ttf cannot compute difference between binary files Index: src/main/resources/tiles/battle.png ================================================================== --- src/main/resources/tiles/battle.png +++ src/main/resources/tiles/battle.png cannot compute difference between binary files Index: src/main/resources/tiles/chest.png ================================================================== --- src/main/resources/tiles/chest.png +++ src/main/resources/tiles/chest.png cannot compute difference between binary files Index: src/main/resources/tiles/chest_open.png ================================================================== --- src/main/resources/tiles/chest_open.png +++ src/main/resources/tiles/chest_open.png cannot compute difference between binary files DELETED src/main/resources/tiles/exit.png Index: src/main/resources/tiles/exit.png ================================================================== --- src/main/resources/tiles/exit.png +++ /dev/null cannot compute difference between binary files Index: src/main/resources/tiles/wall_face.png ================================================================== --- src/main/resources/tiles/wall_face.png +++ src/main/resources/tiles/wall_face.png cannot compute difference between binary files DELETED src/test/java/com/mg105/entities/GiveTutorialTest.java Index: src/test/java/com/mg105/entities/GiveTutorialTest.java ================================================================== --- src/test/java/com/mg105/entities/GiveTutorialTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.mg105.entities; - -import com.mg105.utils.TutorialTexts; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class GiveTutorialTest { - @Test - void testActionSetterGetter() { - GiveTutorial newTutorial = new GiveTutorial(false, false, false); - newTutorial.actionPerformedSetter(TutorialTexts.MOVED); - GiveTutorial newTutorial2 = new GiveTutorial(false, false, false); - newTutorial2.actionPerformedSetter(TutorialTexts.ATTACKED); - GiveTutorial newTutorial3 = new GiveTutorial(false, false, false); - newTutorial3.actionPerformedSetter(TutorialTexts.USED_ITEM); - - assertTrue(newTutorial.actionPerformedGetter(TutorialTexts.MOVED)); - assertFalse(newTutorial.actionPerformedGetter(TutorialTexts.ATTACKED)); - assertFalse(newTutorial.actionPerformedGetter(TutorialTexts.USED_ITEM)); - - assertFalse(newTutorial2.actionPerformedGetter(TutorialTexts.MOVED)); - assertTrue(newTutorial2.actionPerformedGetter(TutorialTexts.ATTACKED)); - assertFalse(newTutorial2.actionPerformedGetter(TutorialTexts.USED_ITEM)); - - assertFalse(newTutorial3.actionPerformedGetter(TutorialTexts.MOVED)); - assertFalse(newTutorial3.actionPerformedGetter(TutorialTexts.ATTACKED)); - assertTrue(newTutorial3.actionPerformedGetter(TutorialTexts.USED_ITEM)); - } -} DELETED src/test/java/com/mg105/interface_adapters/map/RoomInterpreterTest.java Index: src/test/java/com/mg105/interface_adapters/map/RoomInterpreterTest.java ================================================================== --- src/test/java/com/mg105/interface_adapters/map/RoomInterpreterTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.mg105.interface_adapters.map; - -import com.mg105.use_cases.map.RoomGetterInterface; -import com.mg105.use_cases.outputds.RoomLayout; -import com.mg105.utils.MapConstants; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; - -import java.awt.*; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class RoomInterpreterTest { - @Test - void getLayout() { - RoomTileType[][] expected = new RoomTileType[][]{ - {RoomTileType.WALL, RoomTileType.EXIT, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL}, - {RoomTileType.WALL_WITH_FACE, RoomTileType.OPPONENT_SET, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.WALL}, - {RoomTileType.EXIT, RoomTileType.CHEST_OPEN, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.WALL_WITH_FACE}, - {RoomTileType.WALL, RoomTileType.CHEST, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.EXIT}, - {RoomTileType.WALL, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.WALL}, - {RoomTileType.WALL, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.WALL}, - {RoomTileType.WALL, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.FLOOR, RoomTileType.WALL}, - {RoomTileType.WALL_WITH_FACE, RoomTileType.EXIT, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE, RoomTileType.WALL_WITH_FACE}, - }; - RoomInterpreter interpreter = new RoomInterpreter(new FakeRoomGetter()); - RoomTileType[][] actual = interpreter.getCurrentRoom(); - - for (int y = 0; y < MapConstants.ROOM_SIZE; y++) { - for (int x = 0; x < MapConstants.ROOM_SIZE; x++) { - assertEquals(expected[y][x], actual[y][x]); - } - } - } - - @Test - void getPlayer() { - RoomInterpreter interpreter = new RoomInterpreter(new FakeRoomGetter()); - assertEquals(new Point(4, 4), interpreter.getPlayer()); - } - - @Test - void getSprite() { - RoomInterpreter interpreter = new RoomInterpreter(new FakeRoomGetter()); - assertEquals("/sprites/B.png", interpreter.getCharacterSprite()); - } - - private static class FakeRoomGetter implements RoomGetterInterface { - @Override - public @NotNull RoomLayout getCurrentRoomLayout() { - List closedChests = new ArrayList<>(); - closedChests.add(new Point(1, 3)); - - List openChests = new ArrayList<>(); - openChests.add(new Point(1, 2)); - - List opponents = new ArrayList<>(); - opponents.add(new Point(1, 1)); - - List doorways = new ArrayList<>(); - doorways.add(new Point(0, 2)); - doorways.add(new Point(1, 0)); - doorways.add(new Point(MapConstants.ROOM_SIZE - 1, 3)); - doorways.add(new Point(1, MapConstants.ROOM_SIZE - 1)); - - return new RoomLayout(closedChests, openChests, opponents, doorways, new Point(4, 4)); - } - - @Override - public @NotNull String getWalkingSprite() { - return "/sprites/B.png"; - } - - @Override - public boolean isFinalRoom() { - return false; - } - } -} DELETED src/test/java/com/mg105/use_cases/tutorial/TutorialInteractorTest.java Index: src/test/java/com/mg105/use_cases/tutorial/TutorialInteractorTest.java ================================================================== --- src/test/java/com/mg105/use_cases/tutorial/TutorialInteractorTest.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.mg105.use_cases.tutorial; - -import com.mg105.use_cases.PlayerGetsTutorial; -import com.mg105.user_interface.TutorialTextDisplay; -import com.mg105.utils.TutorialTexts; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -public class TutorialInteractorTest { - @Test - void testAdvancePhase() { - PlayerGetsTutorial tutorialPlayer - = new PlayerGetsTutorial(TutorialTexts.PHASES, 0); - - assertEquals(tutorialPlayer.currentPhase(), 0); - tutorialPlayer.nextPhase(); - assertEquals(tutorialPlayer.currentPhase(), 1); - tutorialPlayer.nextPhase(); - assertEquals(tutorialPlayer.currentPhase(), 2); - tutorialPlayer.nextPhase(); - assertEquals(tutorialPlayer.currentPhase(), 3); - } - - @Test - void testTutorialComplete() { - PlayerGetsTutorial tutorialPlayer1 - = new PlayerGetsTutorial(TutorialTexts.PHASES, 0); - PlayerGetsTutorial tutorialPlayer2 - = new PlayerGetsTutorial(TutorialTexts.PHASES, 0); - tutorialPlayer2.setActionPerformed(TutorialTexts.MOVED); - tutorialPlayer2.setActionPerformed(TutorialTexts.ATTACKED); - tutorialPlayer2.setActionPerformed(TutorialTexts.USED_ITEM); - - assertFalse(tutorialPlayer1.getActionPerformed(TutorialTexts.MOVED)); - assertFalse(tutorialPlayer1.getActionPerformed(TutorialTexts.ATTACKED)); - assertFalse(tutorialPlayer1.getActionPerformed(TutorialTexts.USED_ITEM)); - assertFalse(tutorialPlayer1.isComplete()); - - assertTrue(tutorialPlayer2.getActionPerformed(TutorialTexts.MOVED)); - assertTrue(tutorialPlayer2.getActionPerformed(TutorialTexts.ATTACKED)); - assertTrue(tutorialPlayer2.getActionPerformed(TutorialTexts.USED_ITEM)); - assertTrue(tutorialPlayer2.isComplete()); - } - - @Test - void testTutorialBottomText() { - TutorialTextDisplay tutorialDisplay = new TutorialTextDisplay(); - int phase_num = tutorialDisplay.getController().getTutorial().currentPhase(); - String phase = tutorialDisplay.getController().getTutorial().allPhases().get(phase_num); - String tutorialText = tutorialDisplay.showBottomText(phase); - String expected = ""; - - TutorialTextDisplay tutorialDisplay2 = new TutorialTextDisplay(); - tutorialDisplay2.getController().nextPhase(); - int phase_num2 = tutorialDisplay2.getController().getTutorial().currentPhase(); - String phase2 = tutorialDisplay2.getController().getTutorial().allPhases().get(phase_num2); - String tutorialText2 = tutorialDisplay2.showBottomText(phase2); - - TutorialTextDisplay tutorialDisplay3 = new TutorialTextDisplay(); - tutorialDisplay3.getController().nextPhase(); - tutorialDisplay3.getController().nextPhase(); - int phase_num3 = tutorialDisplay3.getController().getTutorial().currentPhase(); - String phase3 = tutorialDisplay3.getController().getTutorial().allPhases().get(phase_num3); - String tutorialText3 = tutorialDisplay3.showBottomText(phase3); - - TutorialTextDisplay tutorialDisplay4 = new TutorialTextDisplay(); - tutorialDisplay4.getController().nextPhase(); - tutorialDisplay4.getController().nextPhase(); - tutorialDisplay4.getController().nextPhase(); - int phase_num4 = tutorialDisplay4.getController().getTutorial().currentPhase(); - String phase4 = tutorialDisplay4.getController().getTutorial().allPhases().get(phase_num4); - String tutorialText4 = tutorialDisplay4.showBottomText(phase4); - - TutorialTextDisplay tutorialDisplay5 = new TutorialTextDisplay(); - tutorialDisplay5.getController().nextPhase(); - tutorialDisplay5.getController().nextPhase(); - tutorialDisplay5.getController().nextPhase(); - tutorialDisplay5.getController().nextPhase(); - int phase_num5 = tutorialDisplay5.getController().getTutorial().currentPhase(); - String phase5 = tutorialDisplay5.getController().getTutorial().allPhases().get(phase_num5); - String tutorialText5 = tutorialDisplay5.showBottomText(phase5); - - assertEquals(tutorialText, expected); - assertEquals(tutorialText2, TutorialTexts.STORY); - assertEquals(tutorialText3, TutorialTexts.TELL_MOVE); - assertEquals(tutorialText4, TutorialTexts.TELL_ATTACK); - assertEquals(tutorialText5, TutorialTexts.TELL_USE_ITEM); - } -}