diff --git a/.github/linters/my_java_checkstyle.xml b/.github/linters/my_java_checkstyle.xml new file mode 100644 index 00000000..82def407 --- /dev/null +++ b/.github/linters/my_java_checkstyle.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000..ab9755d1 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,41 @@ +--- +name: Lint + +on: # yamllint disable-line rule:truthy + push: null + pull_request: null + +jobs: + build: + name: Lint + runs-on: ubuntu-latest + + permissions: + contents: read + packages: read + # To report GitHub Actions status checks + statuses: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # super-linter needs the full git history to get the + # list of files that changed across commits + fetch-depth: 0 + + - name: Super-linter + uses: super-linter/super-linter@v5.7.2 # x-release-please-version + env: + DEFAULT_BRANCH: main + # To report GitHub Actions status checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Exclude forked files from checks + FILTER_REGEX_INCLUDE: .*src/.* + # Ignore .gitignore files + IGNORE_GITIGNORED_FILES: true + + # Custom Java Checkstyle configuration + JAVA_FILE_NAME: my_java_checkstyle.xml + # Disable google-java-format + VALIDATE_GOOGLE_JAVA_FORMAT: false diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..3e006c60 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "overrides": [ + { + "files": "*.md", + "options": { + "tabWidth": 1 + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b315c3eb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "dotnet.defaultSolution": "RPG_Game/RPG_Game.sln", + + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter", + "editor.formatOnSave": true + } +} diff --git a/PYTHONREQUIREMENTS.md b/PYTHONREQUIREMENTS.md new file mode 100644 index 00000000..ef8bc12a --- /dev/null +++ b/PYTHONREQUIREMENTS.md @@ -0,0 +1,16 @@ +## To install all Python dependencies for any specific project: + +1. cd into the project folder. +2. Ensure that the folder contains a `requirements.txt` file. This file contains all the necessary dependencies for this project to function. +3. If this file is not found, look at `Creating a requirements.txt file for your project`. Else, go to step 4. +4. Run the command `pip3 install -r requirements.txt` to generate the dependency file. + +
+ +## Creating a requirements.txt file for your project: + +1. We will use two libraries to help us generate the `requirements.txt` file - pipreqs and pip-tools. +2. If not already installed, run `pip3 install pipreqs` and `pip3 install pip-tools`. +3. cd into the project folder. +4. Run the command `pipreqs --savepath=requirements.in && pip-compile`. +5. Both `requirements.in` and `requirements.txt` files should be created. \ No newline at end of file diff --git a/README.md b/README.md index 9f3119b7..28dd1082 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ To get started, simply fork this repo. Please refer to [CONTRIBUTING.md](CONTRIB ## C#: -- [Learn C# By Building a Simple RPG Game](http://scottlilly.com/learn-c-by-building-a-simple-rpg-index/) +- [Learn C# By Building a Simple RPG Game](http://scottlilly.com/learn-c-by-building-a-simple-rpg-index/) :white_check_mark: - [Create a Rogue-like game in C#](https://roguesharp.wordpress.com/) - [Create a Blank App with C# and Xamarin (work in progress)](https://www.intertech.com/Blog/xamarin-tutorial-part-1-create-a-blank-app/) - [Build iOS Photo Library App with Xamarin and Visual Studio](https://www.raywenderlich.com/134049/building-ios-apps-with-xamarin-and-visual-studio) @@ -371,7 +371,7 @@ To get started, simply fork this repo. Please refer to [CONTRIBUTING.md](CONTRIB - [Mining Twitter Data with Python](https://marcobonzanini.com/2015/03/02/mining-twitter-data-with-python-part-1/) - [Scrape a Website with Scrapy and MongoDB](https://realpython.com/blog/python/web-scraping-with-scrapy-and-mongodb/) - [How To Scrape With Python and Selenium WebDriver](http://www.byperth.com/2018/04/25/guide-web-scraping-101-what-you-need-to-know-and-how-to-scrape-with-python-selenium-webdriver/) -- [Which Movie Should I Watch using BeautifulSoup](https://medium.com/@nishantsahoo.in/which-movie-should-i-watch-5c83a3c0f5b1) +- [Which Movie Should I Watch using BeautifulSoup](https://medium.com/@nishantsahoo.in/which-movie-should-i-watch-5c83a3c0f5b1) :white_check_mark: ### Web Applications: @@ -401,7 +401,7 @@ To get started, simply fork this repo. Please refer to [CONTRIBUTING.md](CONTRIB - [How to Make a Reddit Bot - YouTube](https://www.youtube.com/watch?v=krTUf7BpTc0) (video) - [Build a Facebook Messenger Bot](https://blog.hartleybrody.com/fb-messenger-bot/) - [Making a Reddit + Facebook Messenger Bot](https://pythontips.com/2017/04/13/making-a-reddit-facebook-messenger-bot/) -- How To Create a Telegram Bot Using Python +- How To Create a Telegram Bot Using Python :white_check_mark: - [Part 1](https://khashtamov.com/en/how-to-create-a-telegram-bot-using-python/) - [Part 2](https://khashtamov.com/en/how-to-deploy-telegram-bot-django/) - [Create a Twitter Bot In Python](https://medium.freecodecamp.org/creating-a-twitter-bot-in-python-with-tweepy-ac524157a607) diff --git a/src/Crafting_Interpreters/.gitignore b/src/Crafting_Interpreters/.gitignore new file mode 100644 index 00000000..38105ec2 --- /dev/null +++ b/src/Crafting_Interpreters/.gitignore @@ -0,0 +1,24 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* \ No newline at end of file diff --git a/src/Crafting_Interpreters/Makefile b/src/Crafting_Interpreters/Makefile new file mode 100644 index 00000000..e4f3bc93 --- /dev/null +++ b/src/Crafting_Interpreters/Makefile @@ -0,0 +1,25 @@ +JFLAGS = -g +JC = javac + +.SUFFIXES: .java .class + +.java.class: + $(JC) $(JFLAGS) $*.java + +# This uses the line continuation character (\) for readability. +# Compile .java files in dependency order. +CLASSES = \ + lox/TokenType.java \ + lox/Token.java \ + lox/Scanner.java \ + lox/Lox.java \ + +default: classes + +classes: $(CLASSES:.java=.class) + +clean: + $(RM) lox/*.class + +run: + java lox/Lox \ No newline at end of file diff --git a/src/Crafting_Interpreters/README.md b/src/Crafting_Interpreters/README.md new file mode 100644 index 00000000..5f3a0d40 --- /dev/null +++ b/src/Crafting_Interpreters/README.md @@ -0,0 +1,15 @@ +# Build an Interpreter + +Date Started: 07/01/2024 + +Date Completed: + +This is a mini-project to learn about how compilers work by attempting to implement one in Java programming language. Here is the [link](https://www.craftinginterpreters.com/contents.html) to the guide that I followed. Stopped at 5.1.1. + +Dependencies: + +- + +References: + +- diff --git a/src/Crafting_Interpreters/lox/Lox.java b/src/Crafting_Interpreters/lox/Lox.java new file mode 100644 index 00000000..b19dc0f4 --- /dev/null +++ b/src/Crafting_Interpreters/lox/Lox.java @@ -0,0 +1,98 @@ +package lox; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public final class Lox { + /** + * A flag to indicate if an error occurred + * during the execution of the program. + */ + private static boolean hadError = false; + + /** + * The exit code to indicate a usage error. + */ + private static final int EX_USAGE = 64; + + /** + * The exit code to indicate a data error. + */ + private static final int EX_DATAERR = 65; + + + /** + * Private constructor to prevent instantiation of this class. + */ + private Lox() { } + + /** + * The entry point of the program. + * + * @param args The command line arguments. + * @throws IOException If an I/O error occurs. + */ + public static void main(String[] args) throws IOException { + if (args.length > 1) { + System.out.println("Usage: jlox [script]"); + System.exit(EX_USAGE); + } else if (args.length == 1) { + runFile(args[0]); + } else { + runPrompt(); + } + } + + private static void runFile(String path) throws IOException { + byte[] bytes = Files.readAllBytes(Paths.get(path)); + run(new String(bytes, Charset.defaultCharset())); + + // Indicate an error in the exit code. + if (hadError) System.exit(EX_DATAERR); + } + + private static void runPrompt() throws IOException { + InputStreamReader input = new InputStreamReader(System.in); + BufferedReader reader = new BufferedReader(input); + + for (;;) { + System.out.print("> "); + String line = reader.readLine(); + if (line == null) break; + run(line); + hadError = false; + } + } + + private static void run(String source) { + Scanner scanner = new Scanner(source); + List tokens = scanner.scanTokens(); + + // For now, just print the tokens. + for (Token token : tokens) { + System.out.println(token); + } + } + + /** + * Report an error. + * + * @param line The line number where the error occurred. + * @param message The error message. + */ + public static void error(int line, String message) { + report(line, "", message); + } + + private static void report(int line, String where, String message) { + String msg = String.format("[line %d] Error%s: %s", + line, where, message); + System.err.println(msg); + hadError = true; + } +} diff --git a/src/Crafting_Interpreters/lox/Scanner.java b/src/Crafting_Interpreters/lox/Scanner.java new file mode 100644 index 00000000..ccdc6e88 --- /dev/null +++ b/src/Crafting_Interpreters/lox/Scanner.java @@ -0,0 +1,236 @@ +package lox; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static lox.TokenType.*; + +class Scanner { + + /** + * A map of KEYWORDS to their respective token types. + */ + private static final Map KEYWORDS; + + static { + KEYWORDS = new HashMap<>(); + KEYWORDS.put("and", AND); + KEYWORDS.put("class", CLASS); + KEYWORDS.put("else", ELSE); + KEYWORDS.put("false", FALSE); + KEYWORDS.put("for", FOR); + KEYWORDS.put("fun", FUN); + KEYWORDS.put("if", IF); + KEYWORDS.put("nil", NIL); + KEYWORDS.put("or", OR); + KEYWORDS.put("print", PRINT); + KEYWORDS.put("return", RETURN); + KEYWORDS.put("super", SUPER); + KEYWORDS.put("this", THIS); + KEYWORDS.put("true", TRUE); + KEYWORDS.put("var", VAR); + KEYWORDS.put("while", WHILE); + } + + /** + * The source code to scan. + */ + private final String source; + + /** + * The list of tokens found in the source code. + */ + private final List tokens = new ArrayList<>(); + + /** + * The index of the first character in the current lexeme. + */ + private int start = 0; + + /** + * The index of the current character being considered. + */ + private int current = 0; + + /** + * The current line number in the source code. + */ + private int line = 1; + + + /** + * Create a new scanner for the specified source code. + * + * @param source The source code to scan. + */ + Scanner(String source) { + this.source = source; + } + + /** + * Scan the source code and return the list of tokens found. + * + * @return The list of tokens found in the source code. + */ + List scanTokens() { + while (!isAtEnd()) { + // We are at the beginning of the next lexeme. + start = current; + scanToken(); + } + + tokens.add(new Token(EOF, "", null, line)); + return tokens; + } + + private void scanToken() { + char c = advance(); + switch (c) { + case '(': addToken(LEFT_PAREN); break; + case ')': addToken(RIGHT_PAREN); break; + case '{': addToken(LEFT_BRACE); break; + case '}': addToken(RIGHT_BRACE); break; + case ',': addToken(COMMA); break; + case '.': addToken(DOT); break; + case '-': addToken(MINUS); break; + case '+': addToken(PLUS); break; + case ';': addToken(SEMICOLON); break; + case '*': addToken(STAR); break; + case '!': + addToken(match('=') ? BANG_EQUAL : BANG); + break; + case '=': + addToken(match('=') ? EQUAL_EQUAL : EQUAL); + break; + case '<': + addToken(match('=') ? LESS_EQUAL : LESS); + break; + case '>': + addToken(match('=') ? GREATER_EQUAL : GREATER); + break; + case '/': + if (match('/')) { + // A comment goes until the end of the line. + while (peek() != '\n' && !isAtEnd()) advance(); + } else { + addToken(SLASH); + } + break; + case ' ': + case '\r': + case '\t': + // Ignore whitespace. + break; + + case '\n': + line++; + break; + + case '"': string(); break; + + default: + if (isDigit(c)) { + number(); + } else if (isAlpha(c)) { + identifier(); + } else { + Lox.error(line, "Unexpected character."); + } + break; + } + } + + private void string() { + while (peek() != '"' && !isAtEnd()) { + if (peek() == '\n') line++; + advance(); + } + + // Unterminated string. + if (isAtEnd()) { + Lox.error(line, "Unterminated string."); + return; + } + + // The closing ". + advance(); + + // Trim the surrounding quotes. + String value = source.substring(start + 1, current - 1); + addToken(STRING, value); + } + + private void number() { + while (isDigit(peek())) advance(); + + // Look for a fractional part. + if (peek() == '.' && isDigit(peekNext())) { + // Consume the "." + advance(); + + while (isDigit(peek())) advance(); + } + + addToken(NUMBER, Double.parseDouble(source.substring(start, current))); + } + + private void identifier() { + while (isAlphaNumeric(peek())) advance(); + + String text = source.substring(start, current); + TokenType type = KEYWORDS.get(text); + if (type == null) type = IDENTIFIER; + addToken(type); + } + + private boolean match(char expected) { + if (isAtEnd()) return false; + if (source.charAt(current) != expected) return false; + + current++; + return true; + } + + private char peek() { + if (isAtEnd()) return '\0'; + return source.charAt(current); + } + + private char peekNext() { + if (current + 1 >= source.length()) return '\0'; + return source.charAt(current + 1); + } + + private boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private boolean isAlpha(char c) { + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '_'; + } + + private boolean isAlphaNumeric(char c) { + return isAlpha(c) || isDigit(c); + } + + private boolean isAtEnd() { + return current >= source.length(); + } + + private char advance() { + return source.charAt(current++); + } + + private void addToken(TokenType type) { + addToken(type, null); + } + + private void addToken(TokenType type, Object literal) { + String text = source.substring(start, current); + tokens.add(new Token(type, text, literal, line)); + } +} diff --git a/src/Crafting_Interpreters/lox/Token.java b/src/Crafting_Interpreters/lox/Token.java new file mode 100644 index 00000000..ed419500 --- /dev/null +++ b/src/Crafting_Interpreters/lox/Token.java @@ -0,0 +1,49 @@ +package lox; + +class Token { + /** + * The type of the token. + */ + private final TokenType type; + + /** + * The lexeme of the token. + */ + private final String lexeme; + + /** + * The literal value of the token. + */ + private final Object literal; + + /** + * The line number in the source code where the token appears. + */ + private final int line; + + + /** + * Create a new token. + * + * @param type The type of the token. + * @param lexeme The lexeme of the token. + * @param literal The literal value of the token. + * @param line The line number in the source code where the token appears. + */ + Token(TokenType type, String lexeme, Object literal, int line) { + this.type = type; + this.lexeme = lexeme; + this.literal = literal; + this.line = line; + } + + /** + * Return a string representation of the token. + * + * @return A string representation of the token. + */ + @Override + public String toString() { + return type + " " + lexeme + " " + literal; + } +} diff --git a/src/Crafting_Interpreters/lox/TokenType.java b/src/Crafting_Interpreters/lox/TokenType.java new file mode 100644 index 00000000..92dd3fcc --- /dev/null +++ b/src/Crafting_Interpreters/lox/TokenType.java @@ -0,0 +1,23 @@ +package lox; + +@SuppressWarnings("checkstyle:JavadocVariable") +enum TokenType { + // Single-character tokens. + LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, + COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR, + + // One or two character tokens. + BANG, BANG_EQUAL, + EQUAL, EQUAL_EQUAL, + GREATER, GREATER_EQUAL, + LESS, LESS_EQUAL, + + // Literals. + IDENTIFIER, STRING, NUMBER, + + // Keywords. + AND, CLASS, ELSE, FALSE, FUN, FOR, IF, NIL, OR, + PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE, + + EOF +} diff --git a/src/IMDB_Movie_Scraper/.gitignore b/src/IMDB_Movie_Scraper/.gitignore new file mode 100644 index 00000000..6769e21d --- /dev/null +++ b/src/IMDB_Movie_Scraper/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/src/IMDB_Movie_Scraper/README.md b/src/IMDB_Movie_Scraper/README.md new file mode 100644 index 00000000..528e1367 --- /dev/null +++ b/src/IMDB_Movie_Scraper/README.md @@ -0,0 +1,17 @@ +# Which Movie Should I Watch using BeautifulSoup + +Date Started: 22/05/2023 + +Date Completed: 28/05/2023 + +This is a mini-project to learn about web scraping using BeautifulSoup. Here is the [link](https://medium.com/@nishantsahoo/which-movie-should-i-watch-5c83a3c0f5b1) to the guide that I followed. + +Dependencies: + +- lxml 4.9.2 +- beautifulsoup4 4.12.2 +- Python 3.9.13 + +References: + +- [BeautifulSoup4 API](https://www.crummy.com/software/BeautifulSoup/bs4/doc/) diff --git a/src/IMDB_Movie_Scraper/requirements.in b/src/IMDB_Movie_Scraper/requirements.in new file mode 100644 index 00000000..68f64703 --- /dev/null +++ b/src/IMDB_Movie_Scraper/requirements.in @@ -0,0 +1 @@ +beautifulsoup4==4.12.2 diff --git a/src/IMDB_Movie_Scraper/requirements.txt b/src/IMDB_Movie_Scraper/requirements.txt new file mode 100644 index 00000000..93f8641c --- /dev/null +++ b/src/IMDB_Movie_Scraper/requirements.txt @@ -0,0 +1,10 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile +# +beautifulsoup4==4.12.2 + # via -r requirements.in +soupsieve==2.4.1 + # via beautifulsoup4 diff --git a/src/IMDB_Movie_Scraper/scraper.py b/src/IMDB_Movie_Scraper/scraper.py new file mode 100644 index 00000000..2c4d708b --- /dev/null +++ b/src/IMDB_Movie_Scraper/scraper.py @@ -0,0 +1,48 @@ +from urllib.request import urlopen + +from bs4 import BeautifulSoup + +BASE_URL = "https://www.imdb.com" +DEFAULT_YEAR = 2017 + +# Getting user input +while True: + year = input("Enter a year to search for: ") + + if year == "": + year = str(DEFAULT_YEAR) + print("Setting default year to {}...".format(year)) + break + + if len(year) == 4 and year.isdigit(): + break + + print("Year must be 4 digits long.") + + +# Sending a GET request to specified URL and getting the response as a HTTPResponse object +url = BASE_URL + "/search/title/?release_date=" + year +http_response = urlopen(url) +markup = http_response.read() + +# Parser +soup = BeautifulSoup(markup, "lxml") +movie_list = soup.find_all("div", class_="lister-item mode-advanced") + + +def get_movie_info(movie): + return { + "title": movie.find("h3", class_="lister-item-header").a.text, + "rating": movie.find( + "div", class_="inline-block ratings-imdb-rating" + ).text.strip() + if movie.find("div", class_="inline-block ratings-imdb-rating") + else "No rating", + "path": movie.find("h3", class_="lister-item-header").a["href"], + } + + +movie_list = list(map(get_movie_info, movie_list)) + +for idx, movie in enumerate(movie_list, 1): + print(f'{idx}. {movie["title"]}, {movie["rating"]}\n{BASE_URL + movie["path"]}') diff --git a/src/MH_Script/.env.example b/src/MH_Script/.env.example new file mode 100644 index 00000000..00853cb9 --- /dev/null +++ b/src/MH_Script/.env.example @@ -0,0 +1,2 @@ +MH_PASSWORD=my_secure_password +MH_USERNAME=my_username diff --git a/src/MH_Script/.gitignore b/src/MH_Script/.gitignore new file mode 100644 index 00000000..5e45bbd6 --- /dev/null +++ b/src/MH_Script/.gitignore @@ -0,0 +1,177 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Selenium env +selenium/ \ No newline at end of file diff --git a/src/MH_Script/README.md b/src/MH_Script/README.md new file mode 100644 index 00000000..47b59eea --- /dev/null +++ b/src/MH_Script/README.md @@ -0,0 +1,49 @@ +# MH Script + +Date Started: 08/03/2025 + +Date Completed: 10/03/2025 + +This is a project that attempts to create a Mousehunt script that automatically gifts 25 gifts and 20 raffle tickets to the top most active friends. + +I am running this script in _Windows command-line_ on _Google Chrome_. + +**NOTE: Currently linked to Windows Task Scheduler with properties:** + +- Run whether user is logged on or not +- Run task as soon as possible after a scheduled start is missed +- Trigger at _9:00am_ daily +- Starts the `run_mh_script.bat` program + +Dependencies: + +- selenium 4.29.0 +- webdriver-manager 4.0.2 +- selenium-stealth 1.0.6 + +References: + +- [https://stackoverflow.com/questions/68289474/selenium-headless-how-to-bypass-cloudflare-detection-using-selenium](https://stackoverflow.com/questions/68289474/selenium-headless-how-to-bypass-cloudflare-detection-using-selenium) + +## Setup Instructions + +From root directory, perform these commands: + +```bash +cd src/MH_Script + +python -m venv venv # OR +python3 -m venv venv + +venv\Scripts\Activate # Windows +source venv/bin/activate # Mac/Linux/bash + +pip install -r requirements.txt +``` + +Then, add `.env` file according to `.env.example` and run + +```bash +python main.py # OR +python3 main.py +``` diff --git a/src/MH_Script/helper.py b/src/MH_Script/helper.py new file mode 100644 index 00000000..da5c4744 --- /dev/null +++ b/src/MH_Script/helper.py @@ -0,0 +1,47 @@ +from logger import log, log_success +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.ui import WebDriverWait + + +def wait_for(driver: webdriver.Chrome, condition, locator=None): + """Generic wait function to handle different conditions.""" + return WebDriverWait(driver, 10).until(condition(locator) if locator else condition) + + +def wait_and_click(driver: webdriver.Chrome, locator): + """Waits for an element (or WebElement) to be clickable and clicks it.""" + button = wait_for(driver, EC.element_to_be_clickable, locator) + button.click() + + +def wait_and_find_elements(driver: webdriver.Chrome, locator): + """ + Waits for all elements matching the given locator to be present in the DOM. + """ + return wait_for(driver, EC.presence_of_all_elements_located, locator) + + +def wait_and_find_element(driver: webdriver.Chrome, locator): + """ + Waits for the first element matching the given locator to be present in the DOM. + """ + return wait_for(driver, EC.presence_of_element_located, locator) + + +def click_all_gift_and_ticket(driver: webdriver.Chrome): + """Clicks all enabled gift and ticket buttons.""" + buttons = { + "gift": (By.CLASS_NAME, "sendGift"), + "ticket": (By.CLASS_NAME, "sendTicket"), + } + + log("Clicking all gift and ticket buttons...") + + for key, locator in buttons.items(): + for button in wait_and_find_elements(driver, locator): + if "disabled" not in button.get_attribute("class"): + wait_and_click(driver, button) + + log_success("All gift and ticket buttons clicked.") diff --git a/src/MH_Script/logger.py b/src/MH_Script/logger.py new file mode 100644 index 00000000..e1d7b0eb --- /dev/null +++ b/src/MH_Script/logger.py @@ -0,0 +1,81 @@ +import datetime +import logging +import os + + +class Logger: + _instance = None + _initialized = False + + @classmethod + def get_instance(cls, log_dir="log"): + """ + Get or create the singleton logger instance. + """ + if cls._instance is None: + cls._instance = cls(log_dir) + return cls._instance + + def __init__(self, log_dir="log"): + """ + Initialize the logger only once. + """ + # Only initialize once, even if constructor is called multiple times + if not Logger._initialized: + # Create log directory if it doesn't exist + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # Generate a unique filename based on current timestamp + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + self.log_file = os.path.join(log_dir, f"app_{timestamp}.log") + + # Configure the logger + self.logger = logging.getLogger("my_logger") + self.logger.setLevel(logging.INFO) + self.logger.handlers = [] # Clear any existing handlers + + # Create file handler and set format + file_handler = logging.FileHandler(self.log_file) + formatter = logging.Formatter("%(message)s") + file_handler.setFormatter(formatter) + + # Add handler to logger + self.logger.addHandler(file_handler) + + # Log initial message + self.log_success(f"Logger initialized with log file: {self.log_file}") + + # Mark as initialized + Logger._initialized = True + + def log(self, status, message): + """Log a message with timestamp and status.""" + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"{timestamp} - {status} - {message}" + self.logger.info(log_entry) + + def log_success(self, message): + """Log a success message.""" + self.log("SUCCESS", message) + + def log_error(self, message): + """Log an error message.""" + self.log("ERROR", message) + + def log_info(self, message): + """Log an info message.""" + self.log("INFO", message) + + +# Convenience static methods for easy access +def log_success(message): + Logger.get_instance().log_success(message) + + +def log_error(message): + Logger.get_instance().log_error(message) + + +def log(func): + return Logger.get_instance().log_info(func) diff --git a/src/MH_Script/main.py b/src/MH_Script/main.py new file mode 100644 index 00000000..422a5a20 --- /dev/null +++ b/src/MH_Script/main.py @@ -0,0 +1,85 @@ +from helper import ( + click_all_gift_and_ticket, + wait_and_click, + wait_and_find_element, + wait_and_find_elements, +) +from logger import log, log_error, log_success +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.common.by import By +from selenium.webdriver.common.keys import Keys +from selenium_stealth import stealth +from util import get_password, get_username, init_options +from webdriver_manager.chrome import ChromeDriverManager + +# Set up Chrome options +options = Options() +init_options(options) + +# Initialize WebDriver with automatic driver management +driver = webdriver.Chrome( + service=ChromeService(ChromeDriverManager().install()), options=options +) + +stealth( + driver, + languages=["en-US", "en"], + vendor="Google Inc.", + platform="Win32", + webgl_vendor="Intel Inc.", + renderer="Intel Iris OpenGL Engine", + fix_hairline=True, +) + +log("Starting script...") + +try: + # Open a website + driver.get("https://www.mousehuntgame.com/") + + if wait_and_find_elements(driver, (By.CLASS_NAME, "loginFormContainer")): + log("Starting login process...") + + # If in first login menu, click on "START NEW GAME" + login_text = "START NEW GAME" + xpath_expr = ( + "//*[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', " + "'abcdefghijklmnopqrstuvwxyz'), '{}')]".format(login_text.lower()) + ) + if wait_and_find_elements(driver, (By.XPATH, xpath_expr)): + wait_and_click(driver, (By.CLASS_NAME, "loginPage-signInButton")) + + username_field = wait_and_find_element(driver, (By.NAME, "username")) + password_field = wait_and_find_element(driver, (By.NAME, "password")) + + username_field.send_keys(get_username()) + password_field.send_keys(get_password()) + + # Submit the form (Either by pressing Enter or clicking a login button) + password_field.send_keys(Keys.RETURN) # Press Enter + + log_success("Login process completed.") + + log("Starting main script...") + + wait_and_click(driver, (By.CLASS_NAME, "friends")) + + click_all_gift_and_ticket(driver) + + wait_and_click(driver, (By.CLASS_NAME, "pagerView-nextPageLink")) + + click_all_gift_and_ticket(driver) + + log_success("Main script completed.") + + log_success("Script completed successfully.") + +except Exception as e: + print(f"Error occurred: {e}") + log_error(f"Error occurred: {e}") + +finally: + # Close the browser window after actions are complete + driver.quit() diff --git a/src/MH_Script/requirements.txt b/src/MH_Script/requirements.txt new file mode 100644 index 00000000..3a1ec57d --- /dev/null +++ b/src/MH_Script/requirements.txt @@ -0,0 +1,24 @@ +attrs==25.1.0 +certifi==2025.1.31 +cffi==1.17.1 +charset-normalizer==3.4.1 +exceptiongroup==1.2.2 +h11==0.14.0 +idna==3.10 +outcome==1.3.0.post0 +packaging==24.2 +pycparser==2.22 +PySocks==1.7.1 +python-dotenv==1.0.1 +requests==2.32.3 +selenium==4.29.0 +selenium-stealth==1.0.6 +sniffio==1.3.1 +sortedcontainers==2.4.0 +trio==0.29.0 +trio-websocket==0.12.2 +typing_extensions==4.12.2 +urllib3==2.3.0 +webdriver-manager==4.0.2 +websocket-client==1.8.0 +wsproto==1.2.0 diff --git a/src/MH_Script/run_mh_script.bat b/src/MH_Script/run_mh_script.bat new file mode 100644 index 00000000..8b9029d7 --- /dev/null +++ b/src/MH_Script/run_mh_script.bat @@ -0,0 +1,14 @@ +@echo off +REM TO BE RUN BY TASKSCHEDULER +cd /d "%~dp0" + +python -m venv venv +call venv\Scripts\activate.bat +pip install -r requirements.txt +python main.py +call venv\Scripts\deactivate.bat + +REM OPEN LOG FOLDER IN EXPLORER +explorer.exe /e,"%~dp0log" + +exit \ No newline at end of file diff --git a/src/MH_Script/util.py b/src/MH_Script/util.py new file mode 100644 index 00000000..e95e1943 --- /dev/null +++ b/src/MH_Script/util.py @@ -0,0 +1,72 @@ +import os +import shutil +import subprocess + +from dotenv import load_dotenv +from logger import log, log_success +from selenium.webdriver.chrome.options import Options + +load_dotenv() + + +# Running on WSL +def get_windows_username(): + return ( + subprocess.check_output('cmd.exe /c "echo %USERNAME%"', shell=True) + .decode() + .strip() + ) + + +def get_windows_cwd(): + wsl_cwd = subprocess.check_output(["pwd"]).decode().strip() + win_cwd = subprocess.check_output(["wslpath", "-w", wsl_cwd]).decode().strip() + return win_cwd + + +def get_username(): + return os.getenv("MH_USERNAME") + + +def get_password(): + return os.getenv("MH_PASSWORD") + + +def init_options(options: Options): + log("Initializing Chrome options...") + + # Set the path to your Chrome user data directory + dir_path = os.path.join(os.getcwd(), "selenium") + profile_name = "Default" + + # Delete the existing directory if it exists + if os.path.exists(dir_path): + shutil.rmtree(dir_path) + + options.add_argument(f"user-data-dir={dir_path}") # Path to user data + options.add_argument( + f"--profile-directory={profile_name}" + ) # Specific profile folder + + # Add other options as needed + options.add_argument("--headless=new") # Run Chrome in headless mode (without GUI) + options.add_argument("--disable-gpu") # Recommended for some environments + options.add_argument( + "--window-size=1920,1080" + ) # Set a virtual screen size for consistency + options.add_argument("--start-maximized") # Start Chrome maximized + options.add_argument( + "disable-infobars" + ) # Disable the "Chrome is being controlled by automated software" notification + options.add_argument("--disable-extensions") # Disable all extensions + options.add_argument( + "--no-sandbox" + ) # Run Chrome without sandboxing (for some environments) + options.add_argument( + "--disable-dev-shm-usage" + ) # Disable shared memory usage (for some environments) + + # Specify the location of the Chrome binary (if needed) + options.binary_location = r"C:\Program Files\Google\Chrome\Application\chrome.exe" # Path to Chrome on Windows + + log_success("Chrome options initialized.") diff --git a/src/RPG_Game/.gitignore b/src/RPG_Game/.gitignore new file mode 100644 index 00000000..8dd4607a --- /dev/null +++ b/src/RPG_Game/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml \ No newline at end of file diff --git a/src/RPG_Game/README.md b/src/RPG_Game/README.md new file mode 100644 index 00000000..b5175286 --- /dev/null +++ b/src/RPG_Game/README.md @@ -0,0 +1,21 @@ +# Learn C# By Building a Simple RPG Game + +Date Started: 08/08/2023 + +Date Completed: 07/01/2024 + +This is a mini-project to learn about C# and also to create my own RPG Game. Here is the [link](https://soscsrpg.com/) to the guide that I followed. + +To launch the app: + +- cd to the `/RPG_Game_UI` directory +- run the command `dotnet build` to build the application +- then `dotnet run` to launch + +Dependencies: + +- dotnet 7.0.306 + +References: + + diff --git a/src/RPG_Game/RPG_Game.sln b/src/RPG_Game/RPG_Game.sln new file mode 100644 index 00000000..056c9163 --- /dev/null +++ b/src/RPG_Game/RPG_Game.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG_Game_Engine", "RPG_Game_Engine\RPG_Game_Engine.csproj", "{462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG_Game_UI", "RPG_Game_UI\RPG_Game_UI.csproj", "{D03C6C5C-EB70-440A-84FE-7F5A69AA5097}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {462D9413-3AB3-46A9-A7FF-CCFD5B124E0E}.Release|Any CPU.Build.0 = Release|Any CPU + {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D03C6C5C-EB70-440A-84FE-7F5A69AA5097}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A464F9CF-ECB4-439D-AB13-55C112AEFB61} + EndGlobalSection +EndGlobal diff --git a/src/RPG_Game/RPG_Game_Engine/BaseNotification.cs b/src/RPG_Game/RPG_Game_Engine/BaseNotification.cs new file mode 100644 index 00000000..99c63037 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/BaseNotification.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.ComponentModel; + +namespace Engine +{ + public class BaseNotification : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Controllers/GameSession.cs b/src/RPG_Game/RPG_Game_Engine/Controllers/GameSession.cs new file mode 100644 index 00000000..9572650f --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Controllers/GameSession.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Models; +using Engine.Models.Monsters; +using Engine.Factories; +using Engine.EventArgs; +using Engine.Models.Items.Misc; + +namespace Engine.Controllers +{ + public class GameSession : BaseNotification + { + #region Properties + + private Location? _currentLocation; + private Monster? _currentMonster; + public Player CurrentPlayer { get; private set; } + public Location? CurrentLocation + { + get { return _currentLocation; } + set + { + _currentLocation = value; + OnPropertyChanged(nameof(CurrentLocation)); + } + } + public Monster? CurrentMonster + { + get { return _currentMonster; } + set + { + _currentMonster = value; + OnPropertyChanged(nameof(CurrentMonster)); + OnPropertyChanged(nameof(HasMonster)); + OnPropertyChanged(nameof(IsInBattle)); + } + } + public static ShopKeeper ShopKeeper => ShopKeeper.GetInstance(); + public bool HasMonster => CurrentMonster != null; + public bool IsInBattle => HasMonster && CurrentMonster?.CurrentHitPoints > 0; + private World CurrentWorld { get; set; } + + #endregion + + public GameSession() + { + this.CurrentPlayer = new Player("Scott", "Fighter", 10, 0, 1, 100000); + this.CurrentWorld = WorldFactory.CreateWorld(); + + //temp + this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1001, 1)); + this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1001, 2)); + this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1002, 1)); + + if (!CurrentPlayer.Inventory.Weapons.Any()) + { + this.CurrentPlayer.Inventory.AddItem(ItemFactory.CreateGameItemGroup(1001, 1)); + } + } + + public void OnClick_Move() + { + this.CurrentLocation = this.CurrentWorld.GetRandomLocation(this.CurrentLocation); + this.CurrentMonster = MonsterFactory.GetRandomMonster(); + + GameMessage.RaiseMessage(this, $"You see a {this.CurrentMonster.Name} here!"); + } + + public void OnClick_Use() + { + if (this.CurrentPlayer.ItemInHand is null) + { + GameMessage.RaiseMessage(this, "You have nothing to use."); + return; + } + + this.CurrentPlayer.ItemInHand.Item.Use(this); + } + + public void OnClick_Sell(GameItemGroup item) + { + if (item.Item.Price is null) + { + return; + } + + this.CurrentPlayer.Inventory.RemoveItem(item); + this.CurrentPlayer.Gold += (int)item.Item.Price; + GameMessage.RaiseMessage(this, $"You sold 1 {item.Item.Name} for {item.Item.Price} gold."); + } + + public void OnClick_Buy(GameItemGroup item) + { + if (item.Item.Price is null) + { + return; + } + + if (this.CurrentPlayer.Gold < (int)item.Item.Price) + { + GameMessage.RaiseMessage(this, $"You do not have enough gold to buy {item.Item.Name}."); + return; + } + + this.CurrentPlayer.Inventory.AddItem(item); + this.CurrentPlayer.Gold -= (int)item.Item.Price; + GameMessage.RaiseMessage(this, $"You bought 1 {item.Item.Name} for {item.Item.Price} gold."); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessage.cs b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessage.cs new file mode 100644 index 00000000..2c7fb3f3 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessage.cs @@ -0,0 +1,14 @@ +using System; + +namespace Engine.EventArgs +{ + public static class GameMessage + { + public static event EventHandler? OnMessageRaised; + + internal static void RaiseMessage(object source, string message) + { + OnMessageRaised?.Invoke(source, new GameMessageEventArgs(message)); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessageEventArgs.cs b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessageEventArgs.cs new file mode 100644 index 00000000..acf0d481 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/EventArgs/GameMessageEventArgs.cs @@ -0,0 +1,12 @@ +namespace Engine.EventArgs +{ + public class GameMessageEventArgs : System.EventArgs + { + public string Message { get; private set; } + + public GameMessageEventArgs(string message) + { + this.Message = message; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Factories/ItemFactory.cs b/src/RPG_Game/RPG_Game_Engine/Factories/ItemFactory.cs new file mode 100644 index 00000000..443a7e11 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Factories/ItemFactory.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using Engine.Models.Items.Item; +using Engine.Models.Items.Misc; + +namespace Engine.Factories +{ + internal static class ItemFactory + { + private readonly static Dictionary _gameItems; + + static ItemFactory() + { + _gameItems = new Dictionary + { + {1001, new Weapon( + itemID: 1001, + name: "Pointy Stick", + minimumDamage: 1, + maximumDamage: 2 + )}, + {1002, new Weapon( + itemID: 1002, + name: "Rusty Sword", + minimumDamage: 2, + maximumDamage: 3, + price: 5 + )}, + {3001, new Consumable( + itemID: 3001, + name: "Healing Gauze", + price: 7 + )}, + {9001, new Craftable( + itemID: 9001, + name: "Snakeskin", + description: "The skin of a snake. Looks durable.", + price: 2 + )}, + {9002, new Craftable( + itemID: 9002, + name: "Snake Venom", + description: "One drop can kill.", + price: 10 + )}, + {9003, new Craftable( + itemID: 9003, + name: "Rat Tail", + description: "Looks gross.", + price: 1 + )}, + {9004, new Craftable( + itemID: 9004, + name: "Rat Meat", + description: "Edible, but not very tasty.", + price: 2 + )}, + {9005, new Craftable( + itemID: 9005, + name: "Spider Fang", + description: "Sharp and poisonous.", + price: 5 + )}, + {9006, new Craftable( + itemID: 9006, + name: "Spider Silk", + description: "Strong and flexible.", + price: 3 + )}, + }; + } + + internal static GameItem CreateGameItem(int itemID) + { + if (!_gameItems.ContainsKey(itemID)) + { + throw new ArgumentException(string.Format("Item ID {0} does not exist", itemID)); + } + + return _gameItems[itemID].Clone(); + } + + internal static GameItemGroup CreateGameItemGroup(int itemID, int quantity) + { + GameItem item = CreateGameItem(itemID); + return new GameItemGroup(item, quantity); + } + + internal static GameItemGroup CreateGameItemGroup(GameItem item, int quantity) + { + return new GameItemGroup(item, quantity); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Factories/MonsterFactory.cs b/src/RPG_Game/RPG_Game_Engine/Factories/MonsterFactory.cs new file mode 100644 index 00000000..f685c4aa --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Factories/MonsterFactory.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using Engine.Models.Monsters; +using Engine.Models.Items.Misc; +using Engine.Models; + +namespace Engine.Factories +{ + internal static class MonsterFactory + { + private readonly static Dictionary _monsters; + + static MonsterFactory() + { + _monsters = new Dictionary + { + {4001, new Monster( + monsterID: 4001, + name: "Snake", + imageName: "Snake", + maximumHitPoints: 8, + minimumDamage: 1, + maximumDamage: 2, + rewardExperiencePoints: 10, + rewardGold: 10, + lootTable: new(new() { + new DropChance(itemID: 9001, dropPercentage: 100, minimumQuantity: 1, maximumQuantity: 3), + new DropChance(itemID: 9002, dropPercentage: 25, minimumQuantity: 1, maximumQuantity: 1), + }) + )}, + {4002, new Monster( + monsterID: 4002, + name: "Rat", + imageName: "Rat", + maximumHitPoints: 5, + minimumDamage: 2, + maximumDamage: 2, + rewardExperiencePoints: 5, + rewardGold: 5, + lootTable: new(new() { + new DropChance(itemID: 9003, dropPercentage: 100, minimumQuantity: 1, maximumQuantity: 1), + new DropChance(itemID: 9004, dropPercentage: 75, minimumQuantity: 1, maximumQuantity: 1), + }) + )}, + {4003, new Monster( + monsterID: 4003, + name: "Spider", + imageName: "Spider", + maximumHitPoints: 10, + minimumDamage: 1, + maximumDamage: 5, + rewardExperiencePoints: 10, + rewardGold: 10, + lootTable: new(new() { + new DropChance(itemID: 9005, dropPercentage: 50, minimumQuantity: 1, maximumQuantity: 2), + new DropChance(itemID: 9006, dropPercentage: 100, minimumQuantity: 1, maximumQuantity: 1), + }) + )}, + }; + } + + internal static Monster CreateMonster(int monsterID) + { + if (!_monsters.ContainsKey(monsterID)) + { + throw new ArgumentException(string.Format("Monster ID {0} does not exist", monsterID)); + } + + return _monsters[monsterID].Clone(); + } + + internal static Monster GetRandomMonster() + { + int randomMonsterKeyID = RandomNumberGenerator.NumberBetweenExclusive(_monsters.Count); + + return CreateMonster(_monsters.Keys.ElementAt(randomMonsterKeyID)); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Factories/WorldFactory.cs b/src/RPG_Game/RPG_Game_Engine/Factories/WorldFactory.cs new file mode 100644 index 00000000..3b240759 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Factories/WorldFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Models; + +namespace Engine.Factories +{ + internal static class WorldFactory + { + internal static World CreateWorld() + { + World newWorld = new(); + + newWorld.AddLocation("Farmer's Field", + "There are rows of corn growing here, with giant rats hiding between them.", + "FarmFields"); + newWorld.AddLocation("Farmer's House", + "This is the house of your neighbor, Farmer Ted.", + "Farmhouse"); + newWorld.AddLocation("Home", + "This is your home", + "Home"); + newWorld.AddLocation("Town square", + "You see a fountain here.", + "TownSquare"); + newWorld.AddLocation("Town Gate", + "There is a gate here, protecting the town from giant spiders.", + "TownGate"); + newWorld.AddLocation("Spider Forest", + "The trees in this forest are covered with spider webs.", + "SpiderForest"); + newWorld.AddLocation("Herbalist's hut", + "You see a small hut, with plants drying from the roof.", + "HerbalistsHut"); + newWorld.AddLocation("Herbalist's garden", + "There are many plants here, with snakes hiding behind them.", + "HerbalistsGarden"); + + return newWorld; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/GameStatus.cs b/src/RPG_Game/RPG_Game_Engine/GameStatus.cs new file mode 100644 index 00000000..612ddde5 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/GameStatus.cs @@ -0,0 +1,23 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.ComponentModel; + +namespace Engine +{ + public static class GameStatus + { + #region Game Variables + + public enum GameVariables + { + ATTACK_SUCCESS, + ATTACK_FAILURE, + INSUFFICIENT_ITEM_QUANTITY, + ZERO_ITEM_QUANTITY + } + + #endregion + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/FarmFields.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/FarmFields.png new file mode 100644 index 00000000..0413abbd Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/FarmFields.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/Farmhouse.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Farmhouse.png new file mode 100644 index 00000000..d1c48543 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Farmhouse.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsGarden.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsGarden.png new file mode 100644 index 00000000..c9509ce4 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsGarden.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsHut.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsHut.png new file mode 100644 index 00000000..46e64a67 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/HerbalistsHut.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/Home.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Home.png new file mode 100644 index 00000000..835fda6c Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Home.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/SpiderForest.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/SpiderForest.png new file mode 100644 index 00000000..c036289a Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/SpiderForest.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownGate.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownGate.png new file mode 100644 index 00000000..90a599d1 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownGate.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownSquare.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownSquare.png new file mode 100644 index 00000000..c780585c Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/TownSquare.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Locations/Trader.png b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Trader.png new file mode 100644 index 00000000..bd45d62a Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Locations/Trader.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Rat.png b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Rat.png new file mode 100644 index 00000000..fe441254 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Rat.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Snake.png b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Snake.png new file mode 100644 index 00000000..41719af0 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Snake.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Spider.png b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Spider.png new file mode 100644 index 00000000..96677491 Binary files /dev/null and b/src/RPG_Game/RPG_Game_Engine/Images/Monsters/Spider.png differ diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Inventory.cs b/src/RPG_Game/RPG_Game_Engine/Models/Inventory.cs new file mode 100644 index 00000000..edc35997 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Inventory.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.ObjectModel; +using Engine.Models.Items.Item; +using Engine.Models.Items.Misc; +using System.Collections.Generic; +using System.Collections.Specialized; +using Engine.Factories; + +namespace Engine.Models +{ + public class Inventory : BaseNotification + { + private readonly ObservableCollection _items; + + public ObservableCollection Items { get { return _items; } } + + #region Display Properties + public List Weapons { get; private set; } + public List Craftables { get; private set; } + public List Consumables { get; private set; } + public List Usables { get; private set; } + + #endregion + + public Inventory() + { + _items = new ObservableCollection(); + _items.CollectionChanged += UpdateDisplayProperties; + } + + public void AddItem(GameItemGroup itemGroupToAdd) + { + GameItemGroup? itemGroup = _items.FirstOrDefault(item => item.Item.IsSameItem(itemGroupToAdd.Item)); + + if (itemGroup == null) + { + _items.Add(itemGroupToAdd); + } + else + { + _items[_items.IndexOf(itemGroup)].IncrementQuantity(itemGroupToAdd.Quantity); + } + } + + public void RemoveItem(GameItemGroup itemGroupToRemove) + { + GameItemGroup itemGroup = _items.FirstOrDefault(item => item.Item.IsSameItem(itemGroupToRemove.Item)) + ?? throw new ArgumentException(string.Format("Item {0} not found in inventory", itemGroupToRemove.Item.Name)); + + int newQuantity = _items[_items.IndexOf(itemGroup)].DecrementQuantity(itemGroupToRemove.Quantity); + + if (newQuantity == 0) + { + _items.Remove(itemGroup); + } + } + + private void UpdateDisplayProperties(object sender, NotifyCollectionChangedEventArgs e) + { + this.Weapons = new(_items.Where(item => item.Item is Weapon)); + this.Craftables = new(_items.Where(item => item.Item is Craftable)); + this.Consumables = new(_items.Where(item => item.Item is Consumable)); + this.Usables = new[] { this.Weapons, this.Consumables }.SelectMany(item => item).ToList(); + + OnPropertyChanged(nameof(Weapons)); + OnPropertyChanged(nameof(Craftables)); + OnPropertyChanged(nameof(Consumables)); + OnPropertyChanged(nameof(Usables)); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Consumable.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Consumable.cs new file mode 100644 index 00000000..f8374bbf --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Consumable.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Controllers; +using Engine.Factories; + +namespace Engine.Models.Items.Item +{ + public class Consumable : GameItem + { + public Consumable(int itemID, string name, int? price = null) : base(itemID, name, price) { } + + public override Consumable Clone() + { + return new Consumable(this.ItemID, this.Name, this.Price); + } + + private void Consume(Inventory inventory) + { + inventory.RemoveItem(ItemFactory.CreateGameItemGroup(this, 1)); + } + + public override void Use(GameSession gameSession) + { + gameSession.CurrentPlayer.CurrentHitPoints = Math.Min(gameSession.CurrentPlayer.CurrentHitPoints + 5, gameSession.CurrentPlayer.MaximumHitPoints); + this.Consume(gameSession.CurrentPlayer.Inventory); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Craftable.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Craftable.cs new file mode 100644 index 00000000..16a15ef5 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Craftable.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Controllers; + +namespace Engine.Models.Items.Item +{ + public class Craftable : GameItem + { + public string Description { get; set; } + + public Craftable(int itemID, string name, string description, int? price = null) : base(itemID, name, price) + { + this.Description = description; + } + + public override Craftable Clone() + { + return new Craftable(this.ItemID, this.Name, this.Description, this.Price); + } + + public override void Use(GameSession gameSession) + { + return; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/GameItem.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/GameItem.cs new file mode 100644 index 00000000..faf89aac --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/GameItem.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Controllers; + +namespace Engine.Models.Items.Item +{ + public abstract class GameItem : ICloneable + { + public int ItemID { get; set; } + public string Name { get; set; } + + // Untradable items will have a null price + public int? Price { get; set; } + + public GameItem(int itemID, string name, int? price = null) + { + this.ItemID = itemID; + this.Name = name; + this.Price = price; + } + + object ICloneable.Clone() + { + return Clone(); + } + + public abstract GameItem Clone(); + public abstract void Use(GameSession gameSession); + + public bool IsSameItem(GameItem otherItem) + { + return this.ItemID == otherItem.ItemID; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Weapon.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Weapon.cs new file mode 100644 index 00000000..899b9082 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Item/Weapon.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Controllers; +using Engine.Models.Monsters; +using Engine.EventArgs; + +namespace Engine.Models.Items.Item +{ + public class Weapon : GameItem + { + public int MinimumDamage { get; set; } + public int MaximumDamage { get; set; } + + public Weapon(int itemID, string name, int minimumDamage, int maximumDamage, int? price = null) : base(itemID, name, price) + { + this.MinimumDamage = minimumDamage; + this.MaximumDamage = maximumDamage; + } + + public override Weapon Clone() + { + return new Weapon(this.ItemID, this.Name, this.MinimumDamage, this.MaximumDamage, this.Price); + } + + public int RollDamage() + { + return RandomNumberGenerator.NumberBetweenInclusive(this.MinimumDamage, this.MaximumDamage); + } + + public override void Use(GameSession gameSession) + { + if (gameSession.CurrentPlayer.Attack(gameSession.CurrentMonster!) == GameStatus.GameVariables.ATTACK_FAILURE) + { + return; + } + + if (gameSession.CurrentMonster!.CurrentHitPoints <= 0) + { + GameMessage.RaiseMessage(this, ""); + GameMessage.RaiseMessage(this, $"You defeated the {gameSession.CurrentMonster.Name}!"); + + gameSession.CurrentMonster.ReleaseRewards(gameSession.CurrentPlayer); + gameSession.CurrentMonster = null; + + return; + } + + gameSession.CurrentMonster.Attack(gameSession.CurrentPlayer); + + if (gameSession.CurrentPlayer.CurrentHitPoints <= 0) + { + GameMessage.RaiseMessage(this, ""); + GameMessage.RaiseMessage(this, $"You were defeated by the {gameSession.CurrentMonster.Name}!"); + } + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/DropChance.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/DropChance.cs new file mode 100644 index 00000000..482dc026 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/DropChance.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Engine.Models.Items.Misc +{ + public class DropChance + { + public int ItemID { get; private set; } + + // Out of 100 + public int DropPercentage { get; private set; } + public int MinimumQuantity { get; private set; } + public int MaximumQuantity { get; private set; } + + public DropChance(int itemID, int dropPercentage, int minimumQuantity, int maximumQuantity) + { + this.ItemID = itemID; + this.DropPercentage = dropPercentage; + this.MinimumQuantity = minimumQuantity; + this.MaximumQuantity = maximumQuantity; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/GameItemGroup.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/GameItemGroup.cs new file mode 100644 index 00000000..ac4fe1fc --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/GameItemGroup.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Models.Items.Item; + +namespace Engine.Models.Items.Misc +{ + public class GameItemGroup : BaseNotification + { + private int _quantity; + public GameItem Item { get; set; } + public int Quantity + { + get { return _quantity; } + set + { + _quantity = value; + OnPropertyChanged(nameof(Quantity)); + } + } + + public GameItemGroup(GameItem item, int quantity) + { + this.Item = item; + this.Quantity = quantity; + } + + public int IncrementQuantity(int quantity) + { + this.Quantity += quantity; + return this.Quantity; + } + + public int DecrementQuantity(int quantity) + { + if (this.Quantity < quantity) + { + throw new ArgumentException(string.Format("Item {0} does not have enough quantity", this.Item.Name)); + } + + this.Quantity -= quantity; + return this.Quantity; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/LootTable.cs b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/LootTable.cs new file mode 100644 index 00000000..d238742d --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Items/Misc/LootTable.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; +using Engine.Factories; + +namespace Engine.Models.Items.Misc +{ + public class LootTable + { + private List DropChances { get; set; } + + public LootTable(List dropChances) + { + DropChances = dropChances; + } + + internal Inventory RollLoot() + { + Inventory inventory = new(); + + foreach (DropChance dropChance in DropChances) + { + if (RandomNumberGenerator.NumberBetweenInclusive(1, 100) <= dropChance.DropPercentage) + { + inventory.AddItem(ItemFactory.CreateGameItemGroup(dropChance.ItemID, RandomNumberGenerator.NumberBetweenInclusive(dropChance.MinimumQuantity, dropChance.MaximumQuantity))); + } + } + + return inventory; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Location.cs b/src/RPG_Game/RPG_Game_Engine/Models/Location.cs new file mode 100644 index 00000000..29e86bb5 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Location.cs @@ -0,0 +1,21 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Engine.Models +{ + public class Location + { + public string Name { get; private set; } + public string Description { get; private set; } + public string ImageName { get; private set; } + + public Location(string name, string description, string imageName) + { + this.Name = name; + this.Description = description; + this.ImageName = string.Format("pack://application:,,,/RPG_Game_Engine;component/Images/Locations/{0}.png", imageName); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Monsters/Monster.cs b/src/RPG_Game/RPG_Game_Engine/Models/Monsters/Monster.cs new file mode 100644 index 00000000..437db226 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Monsters/Monster.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; +using Engine.Models.Items.Misc; +using Engine.EventArgs; + +namespace Engine.Models.Monsters +{ + public class Monster : BaseNotification, ICloneable + { + private int _currentHitPoints; + public int MonsterID { get; private set; } + public string Name { get; private set; } + public string ImageName { get; private set; } + public int MaximumHitPoints { get; private set; } + public int CurrentHitPoints + { + get { return _currentHitPoints; } + set + { + _currentHitPoints = value; + OnPropertyChanged(nameof(CurrentHitPoints)); + } + } + public int MinimumDamage { get; private set; } + public int MaximumDamage { get; private set; } + public int RewardExperiencePoints { get; private set; } + public int RewardGold { get; private set; } + private LootTable LootTable { get; set; } + private Inventory? Inventory { get; set; } + + // Constructor for initializing a monster with a loot table + public Monster(int monsterID, string name, string imageName, int maximumHitPoints, int minimumDamage, int maximumDamage, int rewardExperiencePoints, int rewardGold, LootTable lootTable) + { + this.MonsterID = monsterID; + this.Name = name; + this.ImageName = string.Format("pack://application:,,,/RPG_Game_Engine;component/Images/Monsters/{0}.png", imageName); + this.MaximumHitPoints = maximumHitPoints; + this.CurrentHitPoints = maximumHitPoints; + this.MinimumDamage = minimumDamage; + this.MaximumDamage = maximumDamage; + this.RewardExperiencePoints = rewardExperiencePoints; + this.RewardGold = rewardGold; + this.LootTable = lootTable; + this.Inventory = null; + } + + // Constructor for cloning a monster + private Monster(int monsterID, string name, int maximumHitPoints, int minimumDamage, int maximumDamage, int rewardExperiencePoints, int rewardGold, string imageName, LootTable lootTable) + { + this.MonsterID = monsterID; + this.Name = name; + this.ImageName = imageName; + this.MaximumHitPoints = maximumHitPoints; + this.CurrentHitPoints = maximumHitPoints; + this.MinimumDamage = minimumDamage; + this.MaximumDamage = maximumDamage; + this.RewardExperiencePoints = rewardExperiencePoints; + this.RewardGold = rewardGold; + this.LootTable = lootTable; + this.Inventory = LootTable.RollLoot(); + } + + public Monster Clone() + { + return new Monster(this.MonsterID, this.Name, this.MaximumHitPoints, this.MinimumDamage, this.MaximumDamage, this.RewardExperiencePoints, this.RewardGold, this.ImageName, this.LootTable); + } + + object ICloneable.Clone() + { + return Clone(); + } + + internal void Attack(Player player) + { + int damageToPlayer = RandomNumberGenerator.NumberBetweenInclusive(this.MinimumDamage, this.MaximumDamage); + + player.TakeDamage(this, damageToPlayer); + } + + internal void TakeDamage(int damageToMonster) + { + if (damageToMonster == 0) + { + GameMessage.RaiseMessage(this, $"You missed the {this.Name}."); + } + else + { + this.CurrentHitPoints -= damageToMonster; + GameMessage.RaiseMessage(this, $"You hit the {this.Name} for {damageToMonster} points."); + } + } + + internal void ReleaseRewards(Player player) + { + player.ExperiencePoints += this.RewardExperiencePoints; + GameMessage.RaiseMessage(this, $"You receive {this.RewardExperiencePoints} experience points."); + + player.Gold += this.RewardGold; + GameMessage.RaiseMessage(this, $"You receive {this.RewardGold} gold."); + + if (this.Inventory == null) + { + return; + } + + foreach (GameItemGroup gameItemGroup in this.Inventory.Items) + { + player.Inventory.AddItem(gameItemGroup); + GameMessage.RaiseMessage(this, $"You receive {gameItemGroup.Quantity} {gameItemGroup.Item.Name}."); + } + + this.Inventory = null; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/Player.cs b/src/RPG_Game/RPG_Game_Engine/Models/Player.cs new file mode 100644 index 00000000..9e2e78b8 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/Player.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Controllers; +using Engine.EventArgs; +using Engine.Models.Items.Item; +using Engine.Models.Items.Misc; +using Engine.Models.Monsters; + +namespace Engine.Models +{ + public class Player : BaseNotification + { + private string _characterClass; + private int _currentHitPoints; + private int _maximumHitPoints; + private int _experiencePoints; + private int _level; + private int _gold; + + public string Name { get; private set; } + public string CharacterClass + { + get { return _characterClass; } + set + { + _characterClass = value; + OnPropertyChanged(nameof(CharacterClass)); + } + } + public int CurrentHitPoints + { + get { return _currentHitPoints; } + set + { + _currentHitPoints = value; + OnPropertyChanged(nameof(CurrentHitPoints)); + } + } + public int MaximumHitPoints + { + get { return _maximumHitPoints; } + set + { + _maximumHitPoints = value; + OnPropertyChanged(nameof(MaximumHitPoints)); + } + } + public int ExperiencePoints + { + get { return _experiencePoints; } + set + { + _experiencePoints = value; + OnPropertyChanged(nameof(ExperiencePoints)); + } + } + public int Level + { + get { return _level; } + set + { + _level = value; + OnPropertyChanged(nameof(Level)); + } + } + public int Gold + { + get { return _gold; } + set + { + _gold = value; + OnPropertyChanged(nameof(Gold)); + } + } + + public Inventory Inventory { get; private set; } + public GameItemGroup? ItemInHand { get; set; } + + public Player(string name, string characterClass, int hitPoints, int experiencePoints, int level, int gold) + { + this.Name = name; + this.CharacterClass = characterClass; + this.CurrentHitPoints = hitPoints; + this.MaximumHitPoints = hitPoints; + this.ExperiencePoints = experiencePoints; + this.Level = level; + this.Gold = gold; + this.Inventory = new Inventory(); + } + + internal GameStatus.GameVariables Attack(Monster monster) + { + if (this.ItemInHand == null || !(this.ItemInHand.Item is Weapon) || this.ItemInHand.Quantity == 0) + { + GameMessage.RaiseMessage(this, "You must select a weapon to attack."); + return GameStatus.GameVariables.ATTACK_FAILURE; + } + + Weapon Weapon = (Weapon)this.ItemInHand.Item; + + // Hit the monster + monster.TakeDamage(Weapon.RollDamage()); + + return GameStatus.GameVariables.ATTACK_SUCCESS; + } + + internal void TakeDamage(Monster monster, int damageToPlayer) + { + if (damageToPlayer == 0) + { + GameMessage.RaiseMessage(this, $"The {monster.Name} attacks, but misses you."); + } + else + { + this.CurrentHitPoints -= damageToPlayer; + GameMessage.RaiseMessage(this, $"The {monster.Name} hit you for {damageToPlayer} points."); + } + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/ShopKeeper.cs b/src/RPG_Game/RPG_Game_Engine/Models/ShopKeeper.cs new file mode 100644 index 00000000..b239e553 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/ShopKeeper.cs @@ -0,0 +1,37 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Engine.Factories; + +namespace Engine.Models +{ + public class ShopKeeper + { + #region Singleton Pattern + private static readonly ShopKeeper _shopKeeper = new(); + public static ShopKeeper GetInstance() + { + return _shopKeeper; + } + #endregion + + private readonly Inventory _inventory = new(); + private ShopKeeper() + { + _inventory.AddItem(ItemFactory.CreateGameItemGroup(3001, 1)); + _inventory.AddItem(ItemFactory.CreateGameItemGroup(9001, 1)); + _inventory.AddItem(ItemFactory.CreateGameItemGroup(9002, 1)); + _inventory.AddItem(ItemFactory.CreateGameItemGroup(9003, 1)); + _inventory.AddItem(ItemFactory.CreateGameItemGroup(9004, 1)); + _inventory.AddItem(ItemFactory.CreateGameItemGroup(9005, 1)); + _inventory.AddItem(ItemFactory.CreateGameItemGroup(9006, 1)); + } + + public Inventory Inventory + { + get { return _inventory; } + set { } + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/Models/World.cs b/src/RPG_Game/RPG_Game_Engine/Models/World.cs new file mode 100644 index 00000000..3a6ac4fb --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/Models/World.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Engine.Models +{ + internal class World + { + private readonly List _locations = new(); + private readonly Dictionary _special_locations = new(); + internal World() { } + + internal void AddLocation(string name, string description, string imageName) + { + _locations.Add(new Location(name, description, imageName)); + } + + internal void AddLocation(string key, string name, string description, string imageName) + { + _special_locations.Add(key, new Location(name, description, imageName)); + } + + internal Location GetRandomLocation(Location? prev) + { + while (true) + { + int index = RandomNumberGenerator.NumberBetweenExclusive(_locations.Count); + if (_locations[index] != prev) + { + return _locations[index]; + } + } + } + + internal Location GetSpecialLocation(string key) + { + if (!_special_locations.ContainsKey(key)) + { + throw new KeyNotFoundException(string.Format("Key {0} not found in {1} dictionary", key, nameof(_special_locations))); + } + + return _special_locations[key]; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_Engine/RPG_Game_Engine.csproj b/src/RPG_Game/RPG_Game_Engine/RPG_Game_Engine.csproj new file mode 100644 index 00000000..6d6c1689 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/RPG_Game_Engine.csproj @@ -0,0 +1,25 @@ + + + + net7.0-windows + enable + true + + + + + + + + + + + + + + + + + + + diff --git a/src/RPG_Game/RPG_Game_Engine/RandomNumberGenerator.cs b/src/RPG_Game/RPG_Game_Engine/RandomNumberGenerator.cs new file mode 100644 index 00000000..e1c9d332 --- /dev/null +++ b/src/RPG_Game/RPG_Game_Engine/RandomNumberGenerator.cs @@ -0,0 +1,29 @@ +using System; + +namespace Engine +{ + internal class RandomNumberGenerator + { + private static Random _generator = new(); + + public static int NumberBetweenInclusive(int minimumValue, int maximumValue) + { + return _generator.Next(minimumValue, maximumValue + 1); + } + + public static int NumberBetweenInclusive(int maximumValue) + { + return _generator.Next(0, maximumValue + 1); + } + + public static int NumberBetweenExclusive(int minimumValue, int maximumValue) + { + return _generator.Next(minimumValue, maximumValue); + } + + public static int NumberBetweenExclusive(int maximumValue) + { + return _generator.Next(0, maximumValue); + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_UI/App.xaml b/src/RPG_Game/RPG_Game_UI/App.xaml new file mode 100644 index 00000000..73f8ad7c --- /dev/null +++ b/src/RPG_Game/RPG_Game_UI/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/src/RPG_Game/RPG_Game_UI/App.xaml.cs b/src/RPG_Game/RPG_Game_UI/App.xaml.cs new file mode 100644 index 00000000..f8390c83 --- /dev/null +++ b/src/RPG_Game/RPG_Game_UI/App.xaml.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace RPG_Game_UI +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/src/RPG_Game/RPG_Game_UI/AssemblyInfo.cs b/src/RPG_Game/RPG_Game_UI/AssemblyInfo.cs new file mode 100644 index 00000000..8b5504ec --- /dev/null +++ b/src/RPG_Game/RPG_Game_UI/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/src/RPG_Game/RPG_Game_UI/Converters/InvBooleanToVisibilityConverter.cs b/src/RPG_Game/RPG_Game_UI/Converters/InvBooleanToVisibilityConverter.cs new file mode 100644 index 00000000..14fcd851 --- /dev/null +++ b/src/RPG_Game/RPG_Game_UI/Converters/InvBooleanToVisibilityConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace RPG_Game_UI.Converters +{ + // copied from https://www.dotnetmirror.com/articles/wpf/107/wpf-visibility-converter-example-using-ivalueconverter + public class InvBooleanToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool visibility = false; + + if (value is bool v) + { + visibility = v; + } + + return visibility ? Visibility.Hidden : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + Visibility visibility = (Visibility)value; + + return visibility == Visibility.Hidden; + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_UI/Converters/NullToVisibilityConverter.cs b/src/RPG_Game/RPG_Game_UI/Converters/NullToVisibilityConverter.cs new file mode 100644 index 00000000..a069479a --- /dev/null +++ b/src/RPG_Game/RPG_Game_UI/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace RPG_Game_UI.Converters +{ + // copied from https://stackoverflow.com/questions/21939667/nulltovisibilityconverter-make-visible-if-not-null + public class NullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value is null ? Visibility.Hidden : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); // not needed + } + } +} \ No newline at end of file diff --git a/src/RPG_Game/RPG_Game_UI/MainWindow.xaml b/src/RPG_Game/RPG_Game_UI/MainWindow.xaml new file mode 100644 index 00000000..a90b43c8 --- /dev/null +++ b/src/RPG_Game/RPG_Game_UI/MainWindow.xaml @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +