diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index c8c176fec8..0000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Bug Report -about: Found a bug that you wish to tell us about? Start here. [MCA 6.0.0+ ONLY] -title: '' -labels: needs review -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Steps to Reproduce** -Please include steps we can take to reproduce the bug: -1. Launch Minecraft -2. Create a World -3. etc... - -**Minecraft Information (please complete all questions entirely):** - - Minecraft Version: [e.g. 1.12.2] - - Forge Version: [e.g. 14.23.5.2838] - - MCA Version: [e.g. 6.0.0] **Any version below 6.0.0 is NOT supported. Do not submit an issue for these versions.** - - Launcher: [e.g. Standard, Twitch, MultiMC] - -**Modpack Information** -Complete this section if you are using a modpack. - - Modpack Name: [e.g. FTB Infinity] - - Modpack Version: [e.g 3.1.0] - -**Mods List** -Complete this section if you are NOT using a modpack but have other mods installed alongside MCA. - - Please list the other mods you have installed - -**Additional context** -Add any other context about the problem here that you feel is useful. Screenshots, world files, logs, etc. diff --git a/.github/ISSUE_TEMPLATE/feature-change-request.md b/.github/ISSUE_TEMPLATE/feature-change-request.md deleted file mode 100644 index 4277aabb8c..0000000000 --- a/.github/ISSUE_TEMPLATE/feature-change-request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature Change Request -about: Suggest your idea for MCA relating to a current problem in the mod. -title: "[REQUEST] " -labels: '' -assignees: '' - ---- - -**Please describe the problem you are experiencing and requesting a change for** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like to see** -A clear and concise description of what you ideally want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you have considered, if applicable. For example, if you're able to do what you want with a simple in-game workaround, include how to do that here. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000000..f2d54b4b7e --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,56 @@ +name: Java CI with Gradle + +on: [ push ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle - Forge + run: ./gradlew :forge:build + - name: Build with Gradle - Fabric + run: ./gradlew :fabric:build + - name: Upload Artifacts - Forge + uses: actions/upload-artifact@v2 + with: + name: forge_artifacts + path: ./forge/build/libs/ + - name: Upload Artifacts - Fabric + uses: actions/upload-artifact@v2 + with: + name: fabric_artifacts + path: ./fabric/build/libs/ + # - name: Release Build + # uses: softprops/action-gh-release@v1 + # if: startsWith(github.ref, 'refs/tags/') + # with: + # body_path: CHANGELOG.md + # files: "./build/libs/**" + # - name: Build Success + # uses: rjstone/discord-webhook-notify@v1 + # if: success() + # with: + # severity: info + # details: Build Succeeded! + # webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} + # - name: Build Failure + # uses: rjstone/discord-webhook-notify@v1 + # if: failure() + # with: + # severity: error + # details: Build Failed! + # webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} + # - name: Build Cancelled + # uses: rjstone/discord-webhook-notify@v1 + # if: cancelled() + # with: + # severity: warn + # details: Build Canceled! + # webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} + diff --git a/.github/workflows/pr_gradle.yml b/.github/workflows/pr_gradle.yml new file mode 100644 index 0000000000..0b16914eda --- /dev/null +++ b/.github/workflows/pr_gradle.yml @@ -0,0 +1,29 @@ +name: Java CI with Gradle + +on: [ pull_request ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle - Forge + run: ./gradlew :forge:build + - name: Build with Gradle - Fabric + run: ./gradlew :fabric:build + - name: Upload Artifacts - Forge + uses: actions/upload-artifact@v2 + with: + name: forge_artifacts + path: ./forge/build/libs/ + - name: Upload Artifacts - Fabric + uses: actions/upload-artifact@v2 + with: + name: fabric_artifacts + path: ./fabric/build/libs/ + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 560ff2f88d..c873576f62 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,17 @@ build # other eclipse run -classes +backup + +# Files from Forge MDK +forge*changelog.txt +/src/main/resources/assets/mca/skins/old +/src/main/original-java +/mapppings +/mcmodsrepo +/local.properties + +# Files from architectury +.architectury-transformer +/logs +/common/src/main/resources/assets/mca/textures/*.psd diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..58540ceccd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,61 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Minecraft Client (:fabric)", + "request": "launch", + "cwd": "${workspaceFolder}/run", + "console": "internalConsole", + "stopOnEntry": false, + "mainClass": "dev.architectury.transformer.TransformerRuntime", + "vmArgs": "-Dfabric.dli.config\u003dC:\\dev\\gradle@@00201.9\\minecraft-comes-alive\\fabric\\.gradle\\loom-cache\\launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.launch.knot.KnotClient -Darchitectury.main.class\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\fabric\\.gradle\\architectury\\.main_class\" -Darchitectury.runtime.transformer\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\fabric\\.gradle\\architectury\\.transforms\" -Darchitectury.properties\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\fabric\\.gradle\\architectury\\.properties\" -Djdk.attach.allowAttachSelf\u003dtrue -javaagent:\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\.gradle\\architectury\\architectury-transformer-agent.jar\"", + "args": "", + "env": {}, + "projectName": ":fabric" + }, + { + "type": "java", + "name": "Minecraft Server (:fabric)", + "request": "launch", + "cwd": "${workspaceFolder}/run", + "console": "internalConsole", + "stopOnEntry": false, + "mainClass": "dev.architectury.transformer.TransformerRuntime", + "vmArgs": "-Dfabric.dli.config\u003dC:\\dev\\gradle@@00201.9\\minecraft-comes-alive\\fabric\\.gradle\\loom-cache\\launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.launch.knot.KnotServer -Darchitectury.main.class\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\fabric\\.gradle\\architectury\\.main_class\" -Darchitectury.runtime.transformer\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\fabric\\.gradle\\architectury\\.transforms\" -Darchitectury.properties\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\fabric\\.gradle\\architectury\\.properties\" -Djdk.attach.allowAttachSelf\u003dtrue -javaagent:\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\.gradle\\architectury\\architectury-transformer-agent.jar\"", + "args": "nogui", + "env": {}, + "projectName": ":fabric" + }, + { + "type": "java", + "name": "Minecraft Client (:forge)", + "request": "launch", + "cwd": "${workspaceFolder}/run", + "console": "internalConsole", + "stopOnEntry": false, + "mainClass": "dev.architectury.transformer.TransformerRuntime", + "vmArgs": "-Dfabric.dli.config\u003dC:\\dev\\gradle@@00201.9\\minecraft-comes-alive\\forge\\.gradle\\loom-cache\\launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.minecraftforge.userdev.LaunchTesting -Darchitectury.main.class\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\.gradle\\architectury\\.main_class\" -Darchitectury.runtime.transformer\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\.gradle\\architectury\\.transforms\" -Darchitectury.properties\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\.gradle\\architectury\\.properties\" -Djdk.attach.allowAttachSelf\u003dtrue -javaagent:\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\.gradle\\architectury\\architectury-transformer-agent.jar\"", + "args": "", + "env": { + "MOD_CLASSES": "main_a95852f%%C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\build\\resources\\main;main_a95852f%%C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\build\\classes\\java\\main" + }, + "projectName": ":forge" + }, + { + "type": "java", + "name": "Minecraft Server (:forge)", + "request": "launch", + "cwd": "${workspaceFolder}/run", + "console": "internalConsole", + "stopOnEntry": false, + "mainClass": "dev.architectury.transformer.TransformerRuntime", + "vmArgs": "-Dfabric.dli.config\u003dC:\\dev\\gradle@@00201.9\\minecraft-comes-alive\\forge\\.gradle\\loom-cache\\launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.minecraftforge.userdev.LaunchTesting -Darchitectury.main.class\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\.gradle\\architectury\\.main_class\" -Darchitectury.runtime.transformer\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\.gradle\\architectury\\.transforms\" -Darchitectury.properties\u003d\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\.gradle\\architectury\\.properties\" -Djdk.attach.allowAttachSelf\u003dtrue -javaagent:\"C:\\dev\\gradle 1.9\\minecraft-comes-alive\\.gradle\\architectury\\architectury-transformer-agent.jar\"", + "args": "nogui", + "env": { + "MOD_CLASSES": "main_b3f550b%%C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\build\\resources\\main;main_b3f550b%%C:\\dev\\gradle 1.9\\minecraft-comes-alive\\forge\\build\\classes\\java\\main" + }, + "projectName": ":forge" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 2c16a2a147..0000000000 --- a/CHANGELOG +++ /dev/null @@ -1,67 +0,0 @@ -6.0.1 -Feature: Whistle to call your family has been added back into the game. -Feature: Special skins for naming your children certain names (secret!) -Feature: Improved the villager editor with the ability to specify desired careers and textures. -Feature: Villager health is now configurable. - -Fixed: Guards not properly defending villagers when injured. -Fixed: Potential crash client-side when exiting the game. -Fixed: Career names and IDs were not matching in the Trade GUI. -Fixed: Inability to interact with villagers when using some other mods. - -6.0.0 -Feature: In-game notifications for updates. -Feature: Modified female bodies have been added back. -Feature: Player children now take a profession on growing to an adult. -Feature: Rose Gold generation can now be disabled. - -Changed: "Romantic" interaction constraint changed to "Adults". - -Fixed: You are now notified of your child's death. -Fixed: Interaction fatigue was not applying properly. -Fixed: Crashes resulting from using the Villager Spawner block. -Fixed: Inability to interact with some villagers. -Fixed: Guards now behave more like normal villagers. -Fixed: Villagers from eggs will no longer spawn with the Nitwit or Child profession. -Fixed: Negative gifts will no longer be taken from the player. -Fixed: Engagement ring now works properly. -Fixed: Automatic crash reporting wasn't working properly. -Fixed: Inability to procreate with Guards. -Fixed: Crash when a villager dies of fall damage. -Fixed: Villagers wandering away from their homes. -Fixed: The villager editor will no longer produce Pillagers. -Fixed: The force child growth command now works properly. -Fixed: Romantic actions appearing on non-adults. -Fixed: Crashes with other mods referencing onPlaySoundAtEntityEvent. -Fixed: Overspawn of guards in villagers. Now limited to 10 guards at a time. -Fixed: Missing message for riding horses on spouses. - -6.0.0-beta -This version of MCA has been fully rewritten from the ground up to be more user, server, and mod friendly with a streamlined and robust codebase. Compatibility with previous saves should not be expected. - -Major: RadixCore is no longer a required dependency. - -Feature: Villagers performing a chore or movement action show their status below their name. -Feature: Player marriage has been shifted to the /mca command. -Feature: Added /mca-admin commands for operators and server administrators. -Feature: GUI and API elements have been externalized and may be customized as you please with a constraints system. -Feature: More configuration options for server administrators. -Feature: Children now grow in stages - baby, toddler, child, teen, adult. -Feature: Added Guard careers: Hero, Archer, Warrior. -Feature: All skins are now 64x64 and additional skins of this size are now supported. - -Changed: Chores are now smarter, require no configuration before running, and should play more nicely with modded items. -Changed: Mining is now called Prospecting. -Changed: Villager personalities and moods no longer affect interactions. -Changed: The crystal ball and setup menu is no longer required. -Changed: Reaper battle has been tweaked for fairness. -Changed: Reviving villagers no longer requires a memorial item. -Changed: Engagement ring now allows marriage with only 50% of the required hearts. - -Fixed: Random crashing on servers due to java.lang.ClassCastException when approaching villages. -Fixed: Villagers all have their proper vanilla trades based on their profession. -Fixed: Married to "?" will no longer occur on LAN. -Fixed: Health display issues have been fixed. -Fixed: Lost/forgotten player histories on servers. -Fixed: Villagers should trend towards their home points and not wander too far away. -Fixed: Crashes related to null item stacks. \ No newline at end of file diff --git a/README.md b/README.md index 705ee634ed..4160284338 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,37 @@ -Minecraft Comes Alive +Minecraft Comes Alive Reborn ===================== Minecraft Comes Alive (MCA) is a Minecraft mod that replaces Minecraft's villagers with normal player-like NPCs. It works in single player, LAN, and SMP. -Villagers can be interacted with - you can talk to them, ask them to follow you, set their home, give them gifts, etc. Interacting with people builds relationships. Get your relationship high enough and you will be able to marry someone. +Villagers can be interacted with - you can talk to them, ask them to follow you, set their home, give them gifts, etc. Interacting with people builds relationships. Get your relationship high enough, and you will be able to marry someone. After getting married, you will be able to have children who will do many chores for you such as: Farming, Fishing, Woodcutting, Hunting, and Mining. Children will eventually grow up into adults. Adults can get married and have children of their own, and this cycle can repeat indefinitely! +MCA Reborn is a rewrite of MCA for Minecraft 1.16.5 and upwards, featuring extended village management, villager genetics and various enhancements. + +## CurseForge +Build versions will be uploaded here: +https://www.curseforge.com/minecraft/mc-mods/minecraft-comes-alive-reborn + ## Dependencies -At runtime, MCA has no external dependencies other than Minecraft Forge. +MCA has no external dependencies other than Minecraft Forge or Fabric. + +## Compatibilities +MCA is usually compatible with every mod, except when it comes to recognising items (e.g. gifting). -For development, MCA depends on Lombok. It will be installed automatically when you set up your development environment. Install the Lombok plugin for your relevant IDE if you have trouble building in your IDE. +Following mods have the required resourcepacks included and are therefore fully compatible: +- Farmer's Delight ## Contributing Any contributions to are welcome. Simply clone into your workspace, set it up, make your changes, and submit a pull request for review. +Alternatively you can help translating MCA into your language on [Crowdin](https://crowdin.com/project/minecraft-comes-alive-2)! + +Or you can Join the [Discord Community](https://discord.gg/MDcv8kmYHP) for questions, suggestions or social interactions. + ## Credits -These individuals made substantial contributions to MCA - without them, continued progress may have been impossible. +These individuals made substantial contributions to (vanilla) MCA - without them, continued progress may have been impossible. - SheWolfDeadly - ntzrmtthihu777 - ko2fan diff --git a/build.gradle b/build.gradle index 896b9d40eb..459218f8a3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,82 +1,46 @@ -buildscript { - repositories { - mavenCentral() - jcenter() - maven { url = "http://files.minecraftforge.net/maven" } - } - dependencies { - classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT' - } +plugins { + id 'java-library' + id 'architectury-plugin' version '3.2-SNAPSHOT' + id 'dev.architectury.loom' version '0.7.2-SNAPSHOT' apply false + id 'org.ajoberstar.reckon' version '0.13.0' } -apply plugin: 'net.minecraftforge.gradle.forge' -//Only edit below this line, the above code adds and enables the necessary things for Forge to be setup. -version = "6.0.1" -group = "com.minecraftcomesalive" // http://maven.apache.org/guides/mini/guide-naming-conventions.html -archivesBaseName = "MCA-1.12.2" +targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_1_8 -sourceCompatibility = targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. -compileJava { - sourceCompatibility = targetCompatibility = '1.8' +architectury { + minecraft = rootProject.minecraft_version } -minecraft { - version = "1.12.2-14.23.5.2768" - runDir = "run" - - // the mappings can be changed at any time, and must be in the following format. - // snapshot_YYYYMMDD snapshot are built nightly. - // stable_# stables are built at the discretion of the MCP team. - // Use non-default mappings at your own risk. they may not always work. - // simply re-run your setup task after changing the mappings to update your workspace. - mappings = "snapshot_20171003" - // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable. +reckon { + scopeFromProp() + stageFromProp('beta', 'rc', 'final') } -dependencies { - provided 'org.projectlombok:lombok:1.16.4' - // you may put jars on which you depend on in ./libs - // or you may define them like so.. - //compile "some.group:artifact:version:classifier" - //compile "some.group:artifact:version" - - // real examples - //compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env - //compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env +subprojects { + apply plugin: 'dev.architectury.loom' - // the 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime. - //provided 'com.mod-buildcraft:buildcraft:6.0.8:dev' + loom { + mixinConfig "mca.mixin.json" + } - // the deobf configurations: 'deobfCompile' and 'deobfProvided' are the same as the normal compile and provided, - // except that these dependencies get remapped to your current MCP mappings - //deobfCompile 'com.mod-buildcraft:buildcraft:6.0.8:dev' - //deobfProvided 'com.mod-buildcraft:buildcraft:6.0.8:dev' + dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + } +} - // for more info... - // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html - // http://www.gradle.org/docs/current/userguide/dependency_management.html +allprojects { + apply plugin: "architectury-plugin" -} + targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_1_8 -processResources { - // this will ensure that this task is redone when the versions change. - inputs.property "version", project.version - inputs.property "mcversion", project.minecraft.version + group = rootProject.group + description = rootProject.displayname + archivesBaseName = rootProject.name - // replace stuff in mcmod.info, nothing else - from(sourceSets.main.resources.srcDirs) { - include 'mcmod.info' - - // replace version and mcversion - expand 'version':project.version, 'mcversion':project.minecraft.version - } - - // copy everything else except the mcmod.info - from(sourceSets.main.resources.srcDirs) { - exclude 'mcmod.info' - } + tasks.withType(JavaCompile) { + options.encoding = "UTF-8" + } } - -jar { - classifier = 'universal' -} \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000000..ff30d2e9a0 --- /dev/null +++ b/changelog.md @@ -0,0 +1,195 @@ +# 7.0.0 + +* Giant initial update. This list may have missing parts. +* Added mca villager and zombie villager +* Added genetics, personality, traits and mood +* Added dialogue engine + * Ported classic interactions + * Added adoption + * Added divorce and divorce papers +* Added enhanced gifting + * Has a saturation Queue + * Respects villagers specific needs +* Added wedding ring and engagement ring +* Added Grim Reaper +* Added graves, resurrection, Staff of Life and the Scythe +* Added guards and archers +* Added blueprint + * Added village management + * Added automatic building and village recognition + * Added initial building types to extend village functions + * Added rank, task system +* Added taxes +* Added chores +* Added book with enhanced visuals +* Added Advancements +* Added Architecture to support Fabric and Forge +* Added voice acting +* Added initial translations + +# 7.0.1 + +* Fixed traits syncing issues and chance math +* Fixed translation keys + +# 7.0.2 + +* Fixed Server crash +* Fixed crash when setting clothes or haircut when playing on a server +* Added config flag to disable voice acting +* Fixed scythe loosing its charge on non-tombstones +* Fixed staff of life charges +* You can no longer adopt adults +* Fixed grown-up message appearing after world join +* Fixed building detection on certain coordinates +* Fixed tall villagers being too tall to live +* Fixed phrases not being translated on dedicated servers +* Synced Translations + +# 7.0.3 + +* Attempting to talk to a zombie won't prevent you from performing an action +* Fixed interaction fatigue reset +* Added Interaction and gift analysis +* Overhauled gift desaturation. + * Hearts reward will decrease, but won't drop below 0. + * Desaturation uses a configurable exponential curve, slightly favoring awesome stuff. + * Once a day by default, the villager forgets about the latest gift in the queue +* Fixed "datapack" crash +* Building tasks are now required to advance in ranks +* Removed bed reserving, beds are searched on demand +* Fixed villager-keep-following-you problem +* Fixed greeting AI +* Increase percentage of adult villagers +* Fixed changing clothes of unemployed villagers +* Increased frequency of marriage, births and guard spawns + +# 7.0.4 + +* Fixed widow icon +* Player and villager marriage symbol now swapped +* Taxes are initially set to 0% +* Whistle recipe now requires gold instead of rose gold +* Rings are no longer usable as gold ingots +* Fixed a crash related to building detection +* Integrated community re-shaded dna icon +* Added Vegetarian trait +* Fixed missing meat gift phrase +* Replaced names by accurate database of babies born in the US in 2010 +* Fixed graves text for formatted names +* Fixed reviving for villager died by height or void +* When adopting, your spouse also becomes your children's mother +* Decreased villager knockback +* Fixed incorrect amount of bounty hunters +* Added two more headstones +* Fixed crash caused by zombie villagers on dedicated servers +* Only player with merchant rank or higher will receive tax notifications +* mca-admin commands no require op permission +* Fixed smaller issues with building recognition +* Automatic building scanning can now be disabled +* Next to Buildings, you can now add more restrictive "rooms" instead in case your build is not recognized otherwise +* Buildings can no longer intersect +* If adding a building fails, a proper error message is now shown +* Updating existing, intersected buildings work now +* Fixed some villagers being confused on where they live +* Fixed outdated translation variables +* Setting the workplace makes them jobless for now, effectively causing them to look for a new job +* You use both matchmaker rings now +* Gifting cake works on every adult married villager +* Buildings can now be marked as restricted, preventing villagers from moving in +* Voice acting is now disabled by default +* Fixed guards on duty randomly looking into the sky when talking to +* Fixed at least one teleporting-away-while-following bug + +# 7.0.5 + +* Fixed issue with natural breeding +* Blueprint will now better display vertically stacked buildings +* Villager preview in the editor is now animated +* Fixed wasting charges on already reviving villagers +* Fixed a crash +* Fixed opposite gender bug +* Fixed villager marrying relatives +* Guards now attack mca zombie villagers +* No more sliding baby zombie villagers +* Slightly enhanced village boundary determination +* Fixed uninitialized zombie villager babies +* Fixed flower pots with flowers not being recognized +* Lost babies can now be retrieved by the spouse +* Fixed crash on dedicated server when using randomized baby name +* Village will now interact with each other +* Iron golems will now slap the villager when hit accidentally and then chill +* Guards will now support their citizen and have a custom dialogue when the player is the attacker +* Improved archer AI +* Fixed villager getting stuck in doors +* Guards no longer panic when a raid happens +* A wiped-out village will only send a last, bigger bountyhunter wave +* Added all items to recipe book +* Reworked female villager model +* Fixed a bunch of marriage issues caused on death +* Spouse and parents can now be modified in the villager editor +* Fixed guard spam +* Rank Mayor can now make villagers guards or archers manually +* If the Grim Reaper summoning fails, feedback on why is given +* Villager are now silent by default, configurable +* Villages can now be renamed +* Unlocked King rank + +# 7.0.6 + +* Fixed guards aggression towards mobs +* Fixed profession change not always switching clothes +* Added Family Tree item to search +* Fixed crash +* Fixed reaper summoning on some server + +# 7.0.7 + +* Experienced villagers no longer become guards +* The king can assign archers and guards at will +* Fixed king rank +* Can no longer pickup teens +* Fixed curing zombie villagers +* Added missing translations +* Added book of supporter +* Fixed gift desaturation not working +* Improved teleportation, especially when following the player +* Fixed the pixel gap of headstones +* Fixed sleeping villagers not waking up when moved around +* Added letter of condolence +* Fixed dimension issues with player and villager data +* Added mail system, used to notify the player about the death of family members +* Glass roofs are now supported +* Added more jobless skins +* Updated translations and fixed wrong variable syntax +* Added some admin commands +* Temporary disabled baby tracker +* You can now trade with family +* Fixed inventory duplication bug +* Fixed deadlock in relation with reaper spawner +* Villager marriages now respect player hearts +* Fixed gifting golden apple not reducing by 1 +* Fixed crash when hovering over unmarried villagers marriage-symbol +* Villagers will also update baby time +* Fixed datapack crash on some system locales +* Hopefully fixed stuck-at-sleeping issues after loading world +* Adding a building will also look for graveyards to decrease player confusion + +# 7.0.8 + +* Readded blacksmith functionality +* Fixed scaling-flickering with iguana tweaks +* Added text when trying to assign to invalid buildings +* Improved interaction layout +* Staff of Life can no longer be enchanted +* Fixed chores phrase names +* Command kill no longer counts as murder +* Added config flag to disable name tags +* Fixed log spam regarding invalid bounding boxes +* Fixed issues when assigning family in editor +* Buildings now support modded chests +* Villagers will now use your editor name +* Fixed letter author and creative mode usage +* Strengthened Grim Reaper +* Added mod support for atmospheric, autumity, berry good, buzzier bees, environmental, neopolitan, and upgrade aquatic +* Villager now recognize and estimate the value of every (modded) armor, tool, sword, bow and food as a gift (accuracy not guaranteed) diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000000..9a986ea203 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,16 @@ +dependencies { + modImplementation "net.fabricmc:fabric-loader:${rootProject.loader_version}" +} + +architectury { + common() +} + +minecraft { + refmapName = 'mca.mixin.refmap.json' // remove to make it forge runClient compatible + accessWidener 'src/main/resources/mca.aw' +} + +java { + withSourcesJar() +} diff --git a/common/src/main/java/mca/ClientProxy.java b/common/src/main/java/mca/ClientProxy.java new file mode 100644 index 0000000000..cc59d4dea7 --- /dev/null +++ b/common/src/main/java/mca/ClientProxy.java @@ -0,0 +1,37 @@ +package mca; + +import org.jetbrains.annotations.Nullable; + +import mca.network.client.ClientInteractionManager; +import net.minecraft.entity.player.PlayerEntity; + +/** + * Workaround for Forge's BS + */ +public class ClientProxy { + + private static Impl INSTANCE = new Impl(); + + @Nullable + public static PlayerEntity getClientPlayer() { + return INSTANCE.getClientPlayer(); + } + + public static ClientInteractionManager getNetworkHandler() { + return INSTANCE.getNetworkHandler(); + } + + public static class Impl { + protected Impl() { + INSTANCE = this; + } + + public PlayerEntity getClientPlayer() { + return null; + } + + public ClientInteractionManager getNetworkHandler() { + return null; + } + } +} diff --git a/common/src/main/java/mca/ClientProxyAbstractImpl.java b/common/src/main/java/mca/ClientProxyAbstractImpl.java new file mode 100644 index 0000000000..09dcfb7430 --- /dev/null +++ b/common/src/main/java/mca/ClientProxyAbstractImpl.java @@ -0,0 +1,17 @@ +package mca; + +import mca.network.client.ClientInteractionManager; +import mca.network.client.ClientInteractionManagerImpl; + +/** + * Workaround for Forge's BS + */ +public abstract class ClientProxyAbstractImpl extends ClientProxy.Impl { + + private final ClientInteractionManager networkHandler = new ClientInteractionManagerImpl(); + + @Override + public final ClientInteractionManager getNetworkHandler() { + return networkHandler; + } +} diff --git a/common/src/main/java/mca/Config.java b/common/src/main/java/mca/Config.java new file mode 100644 index 0000000000..64a77afd4d --- /dev/null +++ b/common/src/main/java/mca/Config.java @@ -0,0 +1,94 @@ +package mca; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Serializable; + +public final class Config implements Serializable { + private static final long serialVersionUID = 956221997003825933L; + + private static final Config INSTANCE = loadOrCreate(); + + public static Config getInstance() { + return INSTANCE; + } + + public static final int VERSION = 1; + + public int version = 0; + public boolean overwriteOriginalVillagers = true; + public boolean overwriteOriginalZombieVillagers = true; + public boolean enableInfection = true; + public int infectionChance = 5; + public boolean allowGrimReaper = true; + public int guardSpawnRate = 6; + public int chanceToHaveTwins = 2; + public float marriageHeartsRequirement = 100; + public int babyGrowUpTime = 20; + public int villagerMaxAgeTime = 192000; + public int villagerMaxHealth = 20; + public String villagerChatPrefix = ""; + public boolean allowPlayerMarriage = true; + public int roseGoldSpawnWeight = 6; + public boolean allowRoseGoldGeneration = true; + public int marriageChance = 5; + public int childrenChance = 5; + public int giftDesaturationQueueLength = 16; + public float giftDesaturationFactor = 0.5f; + public double giftDesaturationExponent = 0.85; + public double giftSatisfactionFactor = 0.33; + public int baseGiftMoodEffect = 2; + public int giftDesaturationReset = 24000; + public int greetHeartsThreshold = 75; + public int greetAfterDays = 1; + public int childInitialHearts = 100; + public int immigrantChance = 20; + public int bountyHunterInterval = 24000; + public int bountyHunterThreshold = -5; + public float traitChance = 0.25f; + public float traitInheritChance = 0.5f; + public float villagerHeight = 0.9f; + public boolean canHurtBabies = true; + public boolean useVoices = false; + public boolean useVanillaVoices = false; + public float interactionFatigue = 0.05f; + public int interactionFatigueCooldown = 4800; + public float taxesFactor = 0.5f; + public boolean enterVillageNotification = true; + public boolean showNameTags = true; + + public static File getConfigFile() { + return new File("./config/mca.json"); + } + + public void save() { + try (FileWriter writer = new FileWriter(getConfigFile())) { + version = VERSION; + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(this, writer); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static Config loadOrCreate() { + try (FileReader reader = new FileReader(getConfigFile())) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + Config config = gson.fromJson(reader, Config.class); + if (config.version != VERSION) { + config = new Config(); + } + config.save(); + return config; + } catch (IOException e) { + //e.printStackTrace(); + } + Config config = new Config(); + config.save(); + return config; + } +} diff --git a/common/src/main/java/mca/MCA.java b/common/src/main/java/mca/MCA.java new file mode 100644 index 0000000000..9e81b5da84 --- /dev/null +++ b/common/src/main/java/mca/MCA.java @@ -0,0 +1,9 @@ +package mca; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class MCA { + public static final String MOD_ID = "mca"; + public static final Logger LOGGER = LogManager.getLogger(); +} diff --git a/common/src/main/java/mca/ParticleTypesMCA.java b/common/src/main/java/mca/ParticleTypesMCA.java new file mode 100644 index 0000000000..37a6832fea --- /dev/null +++ b/common/src/main/java/mca/ParticleTypesMCA.java @@ -0,0 +1,18 @@ +package mca; + +import mca.cobalt.registration.Registration; +import net.minecraft.particle.DefaultParticleType; +import net.minecraft.particle.ParticleType; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +public interface ParticleTypesMCA { + DefaultParticleType POS_INTERACTION = register("pos_interaction", Registration.ObjectBuilders.Particles.simpleParticle()); + DefaultParticleType NEG_INTERACTION = register("neg_interaction", Registration.ObjectBuilders.Particles.simpleParticle()); + + static void bootstrap() { } + + static > T register(String name, T type) { + return Registration.register(Registry.PARTICLE_TYPE, new Identifier(MCA.MOD_ID, name), type); + } +} diff --git a/common/src/main/java/mca/SoundsMCA.java b/common/src/main/java/mca/SoundsMCA.java new file mode 100644 index 0000000000..924a3ff18f --- /dev/null +++ b/common/src/main/java/mca/SoundsMCA.java @@ -0,0 +1,47 @@ +package mca; + +import mca.cobalt.registration.Registration; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; +import net.minecraft.util.registry.Registry; + +public interface SoundsMCA { + SoundEvent reaper_scythe_out = register("reaper.scythe_out"); + SoundEvent reaper_scythe_swing = register("reaper.scythe_swing"); + SoundEvent reaper_idle = register("reaper.idle"); + SoundEvent reaper_death = register("reaper.death"); + SoundEvent reaper_block = register("reaper.block"); + SoundEvent reaper_summon = register("reaper.summon"); + + SoundEvent working_anvil = register("working.anvil"); + SoundEvent working_page = register("working.page"); + SoundEvent working_saw = register("working.saw"); + SoundEvent working_sharpen = register("working.sharpen"); + + SoundEvent VILLAGER_BABY_LAUGH = register("villager_baby_laugh");//TODO: + + SoundEvent VILLAGER_MALE_SCREAM = register("villager_male_scream"); + SoundEvent VILLAGER_FEMALE_SCREAM = register("villager_female_scream"); + + SoundEvent VILLAGER_MALE_LAUGH = register("villager_male_laugh"); + SoundEvent VILLAGER_FEMALE_LAUGH = register("villager_female_laugh");//TODO: + + SoundEvent VILLAGER_MALE_CRY = register("villager_male_cry"); + SoundEvent VILLAGER_FEMALE_CRY = register("villager_female_cry");//TODO: + + SoundEvent VILLAGER_MALE_ANGRY = register("villager_male_angry");//TODO: + SoundEvent VILLAGER_FEMALE_ANGRY = register("villager_female_angry");//TODO: + + SoundEvent VILLAGER_MALE_GREET = register("villager_male_greet"); + SoundEvent VILLAGER_FEMALE_GREET = register("villager_female_greet");//TODO: + + SoundEvent VILLAGER_MALE_SURPRISE = register("villager_male_surprise"); + SoundEvent VILLAGER_FEMALE_SURPRISE = register("villager_female_surprise");//TODO: + + static void bootstrap() { } + + static SoundEvent register(String sound) { + Identifier id = new Identifier(MCA.MOD_ID, sound); + return Registration.register(Registry.SOUND_EVENT, id, new SoundEvent(id)); + } +} \ No newline at end of file diff --git a/common/src/main/java/mca/TagsMCA.java b/common/src/main/java/mca/TagsMCA.java new file mode 100644 index 0000000000..f27151daa0 --- /dev/null +++ b/common/src/main/java/mca/TagsMCA.java @@ -0,0 +1,32 @@ +package mca; + +import mca.cobalt.registration.Registration; +import net.minecraft.block.Block; +import net.minecraft.item.Item; +import net.minecraft.tag.Tag; +import net.minecraft.util.Identifier; + +public interface TagsMCA { + interface Blocks { + Tag TOMBSTONES = register("tombstones"); + + static void bootstrap() {} + + static Tag register(String path) { + return Registration.ObjectBuilders.Tags.block(new Identifier(MCA.MOD_ID, path)); + } + } + + interface Items { + Tag VILLAGER_EGGS = register("villager_eggs"); + Tag ZOMBIE_EGGS = register("zombie_eggs"); + + Tag BABIES = register("babies"); + + static void bootstrap() {} + + static Tag register(String path) { + return Registration.ObjectBuilders.Tags.item(new Identifier(MCA.MOD_ID, path)); + } + } +} diff --git a/common/src/main/java/mca/advancement/criterion/BabyCriterion.java b/common/src/main/java/mca/advancement/criterion/BabyCriterion.java new file mode 100644 index 0000000000..e510f8c209 --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/BabyCriterion.java @@ -0,0 +1,50 @@ +package mca.advancement.criterion; + +import com.google.gson.JsonObject; +import net.minecraft.advancement.criterion.AbstractCriterion; +import net.minecraft.advancement.criterion.AbstractCriterionConditions; +import net.minecraft.predicate.NumberRange; +import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer; +import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer; +import net.minecraft.predicate.entity.EntityPredicate.Extended; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +public class BabyCriterion extends AbstractCriterion { + private static final Identifier ID = new Identifier("mca:baby"); + + public BabyCriterion() { + } + + public Identifier getId() { + return ID; + } + + public Conditions conditionsFromJson(JsonObject json, Extended player, AdvancementEntityPredicateDeserializer deserializer) { + NumberRange.IntRange c = NumberRange.IntRange.fromJson(json.get("count")); + return new Conditions(player, c); + } + + public void trigger(ServerPlayerEntity player, int c) { + this.test(player, (conditions) -> conditions.test(c)); + } + + public static class Conditions extends AbstractCriterionConditions { + private final NumberRange.IntRange count; + + public Conditions(Extended player, NumberRange.IntRange count) { + super(BabyCriterion.ID, player); + this.count = count; + } + + public boolean test(int c) { + return count.test(c); + } + + public JsonObject toJson(AdvancementEntityPredicateSerializer serializer) { + JsonObject json = super.toJson(serializer); + json.add("count", count.toJson()); + return json; + } + } +} diff --git a/common/src/main/java/mca/advancement/criterion/ChildAgeStateChangeCriterion.java b/common/src/main/java/mca/advancement/criterion/ChildAgeStateChangeCriterion.java new file mode 100644 index 0000000000..b57813414c --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/ChildAgeStateChangeCriterion.java @@ -0,0 +1,50 @@ +package mca.advancement.criterion; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.advancement.criterion.AbstractCriterion; +import net.minecraft.advancement.criterion.AbstractCriterionConditions; +import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer; +import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer; +import net.minecraft.predicate.entity.EntityPredicate.Extended; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +public class ChildAgeStateChangeCriterion extends AbstractCriterion { + private static final Identifier ID = new Identifier("mca:child_age_state_change"); + + public ChildAgeStateChangeCriterion() { + } + + public Identifier getId() { + return ID; + } + + public Conditions conditionsFromJson(JsonObject json, Extended player, AdvancementEntityPredicateDeserializer deserializer) { + String event = json.has("state") ? json.get("state").getAsString() : ""; + return new Conditions(player, event); + } + + public void trigger(ServerPlayerEntity player, String event) { + this.test(player, (conditions) -> conditions.test(event)); + } + + public static class Conditions extends AbstractCriterionConditions { + private final String event; + + public Conditions(Extended player, String event) { + super(ChildAgeStateChangeCriterion.ID, player); + this.event = event; + } + + public boolean test(String event) { + return this.event.equals(event); + } + + public JsonObject toJson(AdvancementEntityPredicateSerializer serializer) { + JsonObject json = super.toJson(serializer); + json.add("state", new JsonPrimitive(event)); + return json; + } + } +} diff --git a/common/src/main/java/mca/advancement/criterion/CriterionMCA.java b/common/src/main/java/mca/advancement/criterion/CriterionMCA.java new file mode 100644 index 0000000000..632288b9cf --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/CriterionMCA.java @@ -0,0 +1,20 @@ +package mca.advancement.criterion; + +import net.minecraft.advancement.criterion.Criterion; + +import mca.mixin.MixinCriteria; + +public interface CriterionMCA { + BabyCriterion BABY_CRITERION = register(new BabyCriterion()); + HeartsCriterion HEARTS_CRITERION = register(new HeartsCriterion()); + GenericEventCriterion GENERIC_EVENT_CRITERION = register(new GenericEventCriterion()); + ChildAgeStateChangeCriterion CHILD_AGE_STATE_CHANGE = register(new ChildAgeStateChangeCriterion()); + FamilyCriterion FAMILY = register(new FamilyCriterion()); + RankCriterion RANK = register(new RankCriterion()); + + static > T register(T obj) { + return MixinCriteria.register(obj); + } + + static void bootstrap() { } +} diff --git a/common/src/main/java/mca/advancement/criterion/FamilyCriterion.java b/common/src/main/java/mca/advancement/criterion/FamilyCriterion.java new file mode 100644 index 0000000000..d057d9d340 --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/FamilyCriterion.java @@ -0,0 +1,65 @@ +package mca.advancement.criterion; + +import com.google.gson.JsonObject; + +import mca.entity.ai.relationship.family.FamilyTree; +import mca.entity.ai.relationship.family.FamilyTreeNode; +import net.minecraft.advancement.criterion.AbstractCriterion; +import net.minecraft.advancement.criterion.AbstractCriterionConditions; +import net.minecraft.predicate.NumberRange; +import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer; +import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer; +import net.minecraft.predicate.entity.EntityPredicate.Extended; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; + +public class FamilyCriterion extends AbstractCriterion { + private static final Identifier ID = new Identifier("mca:family"); + + public FamilyCriterion() { } + + @Override + public Identifier getId() { + return ID; + } + + @Override + public Conditions conditionsFromJson(JsonObject json, Extended player, AdvancementEntityPredicateDeserializer deserializer) { + // quite limited, but I do not assume any more use cases + NumberRange.IntRange c = NumberRange.IntRange.fromJson(json.get("children")); + NumberRange.IntRange gc = NumberRange.IntRange.fromJson(json.get("grandchildren")); + return new Conditions(player, c, gc); + } + + public void trigger(ServerPlayerEntity player) { + FamilyTreeNode familyTree = FamilyTree.get((ServerWorld) player.world).getOrCreate(player); + long c = familyTree.getRelatives(0, 1).count(); + long gc = familyTree.getRelatives(0, 2).count() - c; + + test(player, condition -> condition.test((int)c, (int)gc)); + } + + public static class Conditions extends AbstractCriterionConditions { + private final NumberRange.IntRange children; + private final NumberRange.IntRange grandchildren; + + public Conditions(Extended player, NumberRange.IntRange children, NumberRange.IntRange grandchildren) { + super(FamilyCriterion.ID, player); + this.children = children; + this.grandchildren = grandchildren; + } + + public boolean test(int c, int gc) { + return children.test(c) && grandchildren.test(gc); + } + + @Override + public JsonObject toJson(AdvancementEntityPredicateSerializer serializer) { + JsonObject json = super.toJson(serializer); + json.add("children", children.toJson()); + json.add("grandchildren", grandchildren.toJson()); + return json; + } + } +} diff --git a/common/src/main/java/mca/advancement/criterion/GenericEventCriterion.java b/common/src/main/java/mca/advancement/criterion/GenericEventCriterion.java new file mode 100644 index 0000000000..430442dcc3 --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/GenericEventCriterion.java @@ -0,0 +1,50 @@ +package mca.advancement.criterion; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.advancement.criterion.AbstractCriterion; +import net.minecraft.advancement.criterion.AbstractCriterionConditions; +import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer; +import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer; +import net.minecraft.predicate.entity.EntityPredicate.Extended; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +public class GenericEventCriterion extends AbstractCriterion { + private static final Identifier ID = new Identifier("mca:generic_event"); + + public GenericEventCriterion() { + } + + public Identifier getId() { + return ID; + } + + public Conditions conditionsFromJson(JsonObject json, Extended player, AdvancementEntityPredicateDeserializer deserializer) { + String event = json.has("event") ? json.get("event").getAsString() : ""; + return new Conditions(player, event); + } + + public void trigger(ServerPlayerEntity player, String event) { + this.test(player, (conditions) -> conditions.test(event)); + } + + public static class Conditions extends AbstractCriterionConditions { + private final String event; + + public Conditions(Extended player, String event) { + super(GenericEventCriterion.ID, player); + this.event = event; + } + + public boolean test(String event) { + return this.event.equals(event); + } + + public JsonObject toJson(AdvancementEntityPredicateSerializer serializer) { + JsonObject json = super.toJson(serializer); + json.add("event", new JsonPrimitive(event)); + return json; + } + } +} diff --git a/common/src/main/java/mca/advancement/criterion/HeartsCriterion.java b/common/src/main/java/mca/advancement/criterion/HeartsCriterion.java new file mode 100644 index 0000000000..348441face --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/HeartsCriterion.java @@ -0,0 +1,60 @@ +package mca.advancement.criterion; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.advancement.criterion.AbstractCriterion; +import net.minecraft.advancement.criterion.AbstractCriterionConditions; +import net.minecraft.predicate.NumberRange; +import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer; +import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer; +import net.minecraft.predicate.entity.EntityPredicate.Extended; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +public class HeartsCriterion extends AbstractCriterion { + private static final Identifier ID = new Identifier("mca:hearts"); + + public HeartsCriterion() { + } + + public Identifier getId() { + return ID; + } + + public Conditions conditionsFromJson(JsonObject json, Extended player, AdvancementEntityPredicateDeserializer deserializer) { + NumberRange.IntRange hearts = NumberRange.IntRange.fromJson(json.get("hearts")); + NumberRange.IntRange increase = NumberRange.IntRange.fromJson(json.get("increase")); + String source = json.has("source") ? json.get("source").getAsString() : ""; + return new Conditions(player, hearts, increase, source); + } + + public void trigger(ServerPlayerEntity player, int hearts, int increase, String source) { + this.test(player, (conditions) -> conditions.test(hearts, increase, source)); + } + + public static class Conditions extends AbstractCriterionConditions { + private final NumberRange.IntRange hearts; + private final NumberRange.IntRange increase; + private final String source; + + public Conditions(Extended player, NumberRange.IntRange hearts, NumberRange.IntRange increase, String source) { + super(HeartsCriterion.ID, player); + this.hearts = hearts; + this.increase = increase; + this.source = source; + } + + public boolean test(int hearts, int increase, String source) { + return this.hearts.test(hearts) && this.increase.test(increase) + && (this.source.isEmpty() || this.source.equals(source)); + } + + public JsonObject toJson(AdvancementEntityPredicateSerializer serializer) { + JsonObject json = super.toJson(serializer); + json.add("hearts", hearts.toJson()); + json.add("increase", increase.toJson()); + json.add("source", new JsonPrimitive(source)); + return json; + } + } +} diff --git a/common/src/main/java/mca/advancement/criterion/RankCriterion.java b/common/src/main/java/mca/advancement/criterion/RankCriterion.java new file mode 100644 index 0000000000..3ce5602105 --- /dev/null +++ b/common/src/main/java/mca/advancement/criterion/RankCriterion.java @@ -0,0 +1,52 @@ +package mca.advancement.criterion; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import mca.resources.Rank; +import net.minecraft.advancement.criterion.AbstractCriterion; +import net.minecraft.advancement.criterion.AbstractCriterionConditions; +import net.minecraft.predicate.entity.AdvancementEntityPredicateDeserializer; +import net.minecraft.predicate.entity.AdvancementEntityPredicateSerializer; +import net.minecraft.predicate.entity.EntityPredicate.Extended; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; + +public class RankCriterion extends AbstractCriterion { + private static final Identifier ID = new Identifier("mca:rank"); + + public RankCriterion() { + + } + + public Identifier getId() { + return ID; + } + + public Conditions conditionsFromJson(JsonObject json, Extended player, AdvancementEntityPredicateDeserializer deserializer) { + Rank rank = Rank.fromName(json.get("rank").getAsString()); + return new Conditions(player, rank); + } + + public void trigger(ServerPlayerEntity player, Rank rank) { + this.test(player, (conditions) -> conditions.test(rank)); + } + + public static class Conditions extends AbstractCriterionConditions { + private final Rank rank; + + public Conditions(Extended player, Rank rank) { + super(RankCriterion.ID, player); + this.rank = rank; + } + + public boolean test(Rank rank) { + return this.rank == rank; + } + + public JsonObject toJson(AdvancementEntityPredicateSerializer serializer) { + JsonObject json = super.toJson(serializer); + json.add("rank", new JsonPrimitive(rank.name())); + return json; + } + } +} diff --git a/common/src/main/java/mca/block/BlockEntityTypesMCA.java b/common/src/main/java/mca/block/BlockEntityTypesMCA.java new file mode 100644 index 0000000000..a04c240d25 --- /dev/null +++ b/common/src/main/java/mca/block/BlockEntityTypesMCA.java @@ -0,0 +1,23 @@ +package mca.block; + +import mca.MCA; +import mca.cobalt.registration.Registration; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.datafixer.TypeReferences; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.util.registry.Registry; + +public interface BlockEntityTypesMCA { + + BlockEntityType TOMBSTONE = register("tombstone", BlockEntityType.Builder.create(TombstoneBlock.Data.constructor, BlocksMCA.GRAVELLING_HEADSTONE, BlocksMCA.UPRIGHT_HEADSTONE, BlocksMCA.SLANTED_HEADSTONE, BlocksMCA.CROSS_HEADSTONE, BlocksMCA.WALL_HEADSTONE)); + + static void bootstrap() { + } + + static BlockEntityType register(String name, BlockEntityType.Builder builder) { + Identifier id = new Identifier(MCA.MOD_ID, name); + return Registration.register(Registry.BLOCK_ENTITY_TYPE, id, builder.build(Util.getChoiceType(TypeReferences.BLOCK_ENTITY, id.toString()))); + } +} diff --git a/common/src/main/java/mca/block/BlocksMCA.java b/common/src/main/java/mca/block/BlocksMCA.java new file mode 100644 index 0000000000..5584f6e9a0 --- /dev/null +++ b/common/src/main/java/mca/block/BlocksMCA.java @@ -0,0 +1,34 @@ +package mca.block; + +import mca.MCA; +import mca.TagsMCA; +import mca.cobalt.registration.Registration; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.block.OreBlock; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.registry.Registry; + +public interface BlocksMCA { + Block ROSE_GOLD_BLOCK = register("rose_gold_block", new Block(Block.Settings.copy(Blocks.GOLD_BLOCK))); + Block ROSE_GOLD_ORE = register("rose_gold_ore", new OreBlock(Block.Settings.copy(Blocks.GOLD_ORE))); + + Block JEWELER_WORKBENCH = register("jeweler_workbench", new JewelerWorkbench(Block.Settings.copy(Blocks.OAK_WOOD).nonOpaque())); + Block INFERNAL_FLAME = register("infernal_flame", new InfernalFlameBlock(Block.Settings.copy(Blocks.SOUL_FIRE))); + + Block GRAVELLING_HEADSTONE = register("gravelling_headstone", new TombstoneBlock(Block.Settings.copy(Blocks.STONE).nonOpaque(), 100, 50, new Vec3d(0, -25, 40), -90.0f,true, TombstoneBlock.GRAVELLING_SHAPE)); + Block UPRIGHT_HEADSTONE = register("upright_headstone", new TombstoneBlock(Block.Settings.copy(Blocks.STONE).nonOpaque(), 90, 50, new Vec3d(0, -55, 23),0.0f, true, TombstoneBlock.UPRIGHT_SHAPE)); + Block SLANTED_HEADSTONE = register("slanted_headstone", new TombstoneBlock(Block.Settings.copy(Blocks.STONE).nonOpaque(), 100, 15, new Vec3d(0, -25, 10), -45.0f,true, TombstoneBlock.SLANTED_SHAPE)); + Block CROSS_HEADSTONE = register("cross_headstone", new TombstoneBlock(Block.Settings.copy(Blocks.STONE).nonOpaque(), 80, 15, new Vec3d(0, -13, 15),-45.0f, true, TombstoneBlock.CROSS_SHAPE)); + Block WALL_HEADSTONE = register("wall_headstone", new TombstoneBlock(Block.Settings.copy(Blocks.STONE).nonOpaque(), 100, 15, new Vec3d(0, -25, 40),0.0f, false, TombstoneBlock.WALL_SHAPE)); + + static void bootstrap() { + TagsMCA.Blocks.bootstrap(); + BlockEntityTypesMCA.bootstrap(); + } + + static T register(String name, T block) { + return Registration.register(Registry.BLOCK, new Identifier(MCA.MOD_ID, name), block); + } +} diff --git a/common/src/main/java/mca/block/InfernalFlameBlock.java b/common/src/main/java/mca/block/InfernalFlameBlock.java new file mode 100644 index 0000000000..f9def26de2 --- /dev/null +++ b/common/src/main/java/mca/block/InfernalFlameBlock.java @@ -0,0 +1,16 @@ +package mca.block; + +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.AbstractFireBlock; +import net.minecraft.block.BlockState; + +public class InfernalFlameBlock extends AbstractFireBlock { + public InfernalFlameBlock(AbstractBlock.Settings settings) { + super(settings, 2.0F); + } + + @Override + protected boolean isFlammable(BlockState state) { + return true; + } +} diff --git a/common/src/main/java/mca/block/JewelerWorkbench.java b/common/src/main/java/mca/block/JewelerWorkbench.java new file mode 100644 index 0000000000..7cf19c4ccc --- /dev/null +++ b/common/src/main/java/mca/block/JewelerWorkbench.java @@ -0,0 +1,115 @@ +package mca.block; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.HorizontalFacingBlock; +import net.minecraft.block.ShapeContext; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.client.item.TooltipContext; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.inventory.Inventory; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.DirectionProperty; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.ActionResult; +import net.minecraft.util.BlockMirror; +import net.minecraft.util.BlockRotation; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.ItemScatterer; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import mca.MCA; + +public class JewelerWorkbench extends Block/* implements BlockEntityProvider*/ { + public static final DirectionProperty FACING = HorizontalFacingBlock.FACING; + protected static final VoxelShape SHAPE = Block.createCuboidShape(1.0D, 0.1D, 1.0D, 15.0D, 24.0D, 15.0D); + + public JewelerWorkbench(Settings properties) { + super(properties); + } + + /* + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return return new JewelerWorkbenchTileEntity(); + } + */ + + @Override + public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult rayTrace) { + if (world.isClient) { + return ActionResult.SUCCESS; + } + //this.interactWith(world, pos, player); + return ActionResult.CONSUME; + } + + /* + private void interactWith(World world, BlockPos pos, PlayerEntity player) { + BlockEntity tileEntity = world.getBlockEntity(pos); + // TODO: Implement this + } + */ + + @Override + protected void appendProperties(StateManager.Builder builder) { + builder.add(FACING); + } + + @Override + public VoxelShape getOutlineShape(BlockState state, BlockView worldIn, BlockPos pos, ShapeContext context) { + return SHAPE; + } + + public VoxelShape getRayTraceShape(BlockState state, BlockView reader, BlockPos pos, ShapeContext context) { + return SHAPE; + } + + public void addInformation(ItemStack item, @Nullable BlockView iBlock, List tooltip, TooltipContext iTooltipFlag) { + tooltip.add(new LiteralText("Workbench allows you to buy rings from Jeweler").formatted(Formatting.GRAY)); + tooltip.add(new TranslatableText(String.format("tooltip.%s.block.statue.line1", MCA.MOD_ID)).formatted(Formatting.GRAY)); + tooltip.add(new TranslatableText(String.format("tooltip.%s.block.statue.line2", MCA.MOD_ID)).formatted(Formatting.GRAY)); + } + + @Nullable + @Override + public BlockState getPlacementState(ItemPlacementContext context) { + return this.getDefaultState().with(FACING, context.getPlayerFacing().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, BlockRotation rot) { + return state.with(FACING, rot.rotate(state.get(FACING))); + } + + @Override + public BlockState mirror(BlockState state, BlockMirror mirrorIn) { + return state.rotate(mirrorIn.getRotation(state.get(FACING))); + } + + @Override + public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean isMoving) { + if (!state.isOf(newState.getBlock())) { + BlockEntity tileEntity = world.getBlockEntity(pos); + if (tileEntity instanceof Inventory) { + ItemScatterer.spawn(world, pos, (Inventory) tileEntity); + world.updateComparators(pos, this); + } + super.onStateReplaced(state, world, pos, newState, isMoving); + } + } +} diff --git a/common/src/main/java/mca/block/TombstoneBlock.java b/common/src/main/java/mca/block/TombstoneBlock.java new file mode 100644 index 0000000000..1256d66f7f --- /dev/null +++ b/common/src/main/java/mca/block/TombstoneBlock.java @@ -0,0 +1,541 @@ +package mca.block; + +import mca.entity.Infectable; +import mca.entity.ai.relationship.CompassionateEntity; +import mca.entity.ai.relationship.EntityRelationship; +import mca.entity.ai.relationship.Gender; +import mca.server.world.data.GraveyardManager; +import mca.server.world.data.GraveyardManager.TombstoneState; +import mca.util.NbtElementCompat; +import mca.util.NbtHelper; +import mca.util.VoxelShapeUtil; +import mca.util.compat.ItemStackCompat; +import mca.util.compat.WorldEventsCompat; +import mca.util.localization.FlowingText; +import net.minecraft.block.*; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.LightningEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.ZombieVillagerEntity; +import net.minecraft.entity.passive.PassiveEntity; +import net.minecraft.fluid.FluidState; +import net.minecraft.fluid.Fluids; +import net.minecraft.item.ItemPlacementContext; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameters; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtList; +import net.minecraft.nbt.NbtString; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvents; +import net.minecraft.state.StateManager; +import net.minecraft.state.property.Properties; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.BlockMirror; +import net.minecraft.util.BlockRotation; +import net.minecraft.util.Tickable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Direction.Axis; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import net.minecraft.world.WorldAccess; +import net.minecraft.world.WorldView; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class TombstoneBlock extends BlockWithEntity implements Waterloggable { + public static final VoxelShape GRAVELLING_SHAPE = Block.createCuboidShape(1, 0, 1, 15, 1, 15); + public static final VoxelShape UPRIGHT_SHAPE = VoxelShapes.union( + Block.createCuboidShape(1, 0, 2, 15, 18, 4), + Block.createCuboidShape(2, 18, 2, 14, 19, 4), + Block.createCuboidShape(3, 19, 2, 13, 20, 4) + ); + public static final VoxelShape CROSS_SHAPE = VoxelShapes.union( + Block.createCuboidShape(6, 0, 2, 10, 28, 4), + Block.createCuboidShape(-1, 18, 2, 17, 21, 4) + ); + public static final VoxelShape SLANTED_SHAPE = Block.createCuboidShape(0, 0, 1, 16, 10, 10); + public static final VoxelShape WALL_SHAPE = Block.createCuboidShape(1, 1, 0, 15, 15, 1); + + private final Map shapes; + + private final int lineWidth; + private final int maxNameHeight; + private final Vec3d nameplateOffset; + private final boolean requiresSolid; + private final float rotation; + + public TombstoneBlock(Settings properties, int lineWidth, int maxNameHeight, Vec3d nameplateOffset, float rotation, boolean requiresSolid, VoxelShape baseShape) { + super(properties); + setDefaultState(getDefaultState().with(Properties.WATERLOGGED, false)); + + this.lineWidth = lineWidth; + this.maxNameHeight = maxNameHeight; + this.nameplateOffset = nameplateOffset; + this.rotation = rotation; + this.requiresSolid = requiresSolid; + shapes = Arrays.stream(Direction.values()) + .filter(d -> d.getAxis() != Axis.Y) + .collect(Collectors.toMap( + Function.identity(), + VoxelShapeUtil.rotator(baseShape)) + ); + } + + public int getLineWidth() { + return lineWidth; + } + + public int getMaxNameHeight() { + return maxNameHeight; + } + + public Vec3d getNameplateOffset() { + return nameplateOffset; + } + + public float getRotation() { + return rotation; + } + + @Override + public boolean hasSidedTransparency(BlockState state) { + return true; + } + + @Override + public boolean canMobSpawnInside() { + return true; + } + + @Override + @Deprecated + public VoxelShape getOutlineShape(BlockState state, BlockView view, BlockPos pos, ShapeContext ePos) { + if (this == BlocksMCA.SLANTED_HEADSTONE) { + for (Direction i : shapes.keySet()) { + shapes.put(i, VoxelShapeUtil.rotator(SLANTED_SHAPE).apply(i)); + } + } + + return shapes.getOrDefault(state.get(Properties.HORIZONTAL_FACING), VoxelShapes.fullCube()); + } + + @Override + public BlockRenderType getRenderType(BlockState state) { + return BlockRenderType.MODEL; + } + + @Override + public void onBlockAdded(BlockState state, World world, BlockPos pos, BlockState oldState, boolean notify) { + updateTombstoneState(world, pos); + } + + @Override + public void onPlaced(World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { + Data.of(world.getBlockEntity(pos)).ifPresent(data -> data.readFromStack(stack)); + updateTombstoneState(world, pos); + } + + private void updateTombstoneState(World world, BlockPos pos) { + if (!world.isClient) { + GraveyardManager.get((ServerWorld) world).setTombstoneState(pos, + hasEntity(world, pos) ? TombstoneState.FILLED : TombstoneState.EMPTY + ); + } + } + + @Override + public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { + super.onStateReplaced(state, world, pos, newState, moved); + if (!world.isClient && !state.isOf(newState.getBlock())) { + updateNeighbors(state, world, pos); + GraveyardManager.get((ServerWorld) world).removeTombstoneState(pos); + } + } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockView world) { + return BlockEntityTypesMCA.TOMBSTONE.instantiate(); + } + + @Override + protected void appendProperties(StateManager.Builder builder) { + builder.add(Properties.WATERLOGGED).add(Properties.HORIZONTAL_FACING); + } + + @Override + public BlockState getStateForNeighborUpdate(BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos) { + if (state.get(Properties.WATERLOGGED)) { + world.getFluidTickScheduler().schedule(pos, Fluids.WATER, Fluids.WATER.getTickRate(world)); + } + + if (direction == Direction.DOWN && !canPlaceAt(state, world, pos)) { + return Blocks.AIR.getDefaultState(); + } + + return super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos); + } + + @Override + public boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) { + if (requiresSolid) { + pos = pos.down(); + return world.getBlockState(pos).isSideSolid(world, pos, Direction.UP, SideShapeType.FULL); + } else { + return true; + } + } + + @Override + public FluidState getFluidState(BlockState state) { + return state.get(Properties.WATERLOGGED) ? Fluids.WATER.getStill(false) : super.getFluidState(state); + } + + private void updateNeighbors(BlockState state, World world, BlockPos pos) { + world.updateNeighborsAlways(pos, this); + world.updateNeighborsAlways(pos.offset(state.get(Properties.HORIZONTAL_FACING)), this); + } + + @Nullable + @Override + public BlockState getPlacementState(ItemPlacementContext context) { + return getDefaultState().with(Properties.HORIZONTAL_FACING, context.getPlayerFacing().getOpposite()); + } + + @Override + public BlockState rotate(BlockState state, BlockRotation rot) { + return state.with(Properties.HORIZONTAL_FACING, rot.rotate(state.get(Properties.HORIZONTAL_FACING))); + } + + @Override + public BlockState mirror(BlockState state, BlockMirror mirror) { + return state.rotate(mirror.getRotation(state.get(Properties.HORIZONTAL_FACING))); + } + + @Override + public int getWeakRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) { + return state.getStrongRedstonePower(world, pos, direction); + } + + @Override + public int getStrongRedstonePower(BlockState state, BlockView world, BlockPos pos, Direction direction) { + return direction == state.get(Properties.HORIZONTAL_FACING) && hasEntity(world, pos) ? 15 : 0; + } + + protected boolean hasEntity(BlockView world, BlockPos pos) { + return Data.of(world.getBlockEntity(pos)).map(Data::hasEntity).orElse(false); + } + + @Override + public boolean emitsRedstonePower(BlockState state) { + return true; + } + + @Override + public void randomDisplayTick(BlockState state, World world, BlockPos pos, Random random) { + Data.of(world.getBlockEntity(pos)).filter(Data::isResurrecting).ifPresent(data -> { + for (int i = 0; i < random.nextInt(8) + 1; ++i) { + world.addParticle(random.nextBoolean() ? ParticleTypes.LARGE_SMOKE : ParticleTypes.SMOKE, + pos.getX() + random.nextFloat(), + pos.getY() + random.nextFloat(), + pos.getZ() + random.nextFloat(), + (random.nextFloat() - 0.5) / 10F, + 0, + (random.nextFloat() - 0.5) / 10F + ); + } + }); + + + } + + @Override + public List getDroppedStacks(BlockState state, LootContext.Builder builder) { + List stacks = super.getDroppedStacks(state, builder); + + Optional data = Data.of(builder.get(LootContextParameters.BLOCK_ENTITY)).filter(Data::hasEntity); + + data + .flatMap(Data::getEntityName) + .ifPresent(name -> { + stacks.stream().filter(TombstoneBlock::isRemains).forEach(stack -> { + stack.removeCustomName(); + stack.setCustomName(new TranslatableText("block.mca.tombstone.remains", stack.getName(), name)); + }); + + }); + data.ifPresent(be -> { + stacks.stream().filter(s -> s.getItem() == asItem()).findFirst().ifPresent(be::writeToStack); + }); + + return stacks; + } + + static boolean isRemains(ItemStack stack) { + return stack.getItem() == Items.BONE || stack.getItem() == Items.SKELETON_SKULL; + } + + public static class Data extends BlockEntity implements Tickable { + public static Supplier constructor = Data::new; + + private Optional entityData = Optional.empty(); + + @Nullable + private FlowingText computedName; + + private int resurrectionProgress; + private boolean cure; + + protected Data() { + super(BlockEntityTypesMCA.TOMBSTONE); + } + + public boolean isResurrecting() { + return resurrectionProgress > 0; + } + + public void startResurrecting(boolean cure) { + resurrectionProgress = 1; + this.cure = cure; + generateLightning(); + markDirty(); + sync(); + } + + @Override + public void tick() { + if (world.isClient) { + return; + } + + if (hasEntity() && resurrectionProgress > 0) { + resurrectionProgress++; + markDirty(); + sync(); + + if (resurrectionProgress % 30 == 0) { + world.playSound(null, pos.getX(), pos.getY(), pos.getZ(), cure ? SoundEvents.BLOCK_BELL_USE : SoundEvents.ENTITY_POLAR_BEAR_AMBIENT, SoundCategory.BLOCKS, 1, 1); + world.syncWorldEvent(WorldEventsCompat.BLOCK_BROKEN, pos, Block.getRawIdFromState(getCachedState())); + } + + if (world.random.nextInt(10) > 5 && resurrectionProgress % 20 == 0) { + generateLightning(); + } + + if (resurrectionProgress > 500) { + resurrectionProgress = 0; + + createEntity(world, true).ifPresent(entity -> { + generateLightning(); + entity.setPosition(pos.getX() + 0.5F, pos.getY() + 0.5F, pos.getZ() + 0.5F); + if (entity instanceof LivingEntity) { + LivingEntity l = (LivingEntity) entity; + l.setHealth(l.getMaxHealth()); + l.clearStatusEffects(); + l.fallDistance = 0.0f; + l.removed = false; + l.deathTime = 0; + } + + //enforcing a dimension update + if (entity instanceof PassiveEntity) { + PassiveEntity mob = (PassiveEntity) entity; + mob.setBreedingAge(mob.getBreedingAge()); + } + + if (cure && (entity instanceof ZombieVillagerEntity)) { + entity = ((ZombieVillagerEntity) entity).method_29243(EntityType.VILLAGER, true); + } + + if (entity instanceof CompassionateEntity) { + ((CompassionateEntity)entity).getRelationships().getFamilyEntry().setDeceased(false); + } + + if (entity instanceof Infectable) { + ((Infectable) entity).setInfectionProgress(cure + ? Infectable.MIN_INFECTION + : Math.max(MathHelper.lerp(world.random.nextFloat(), Infectable.FEVER_THRESHOLD, Infectable.BABBLING_THRESHOLD), ((Infectable) entity).getInfectionProgress()) + ); + } + + world.spawnEntity(entity); + }); + } + } + } + + private void generateLightning() { + world.setLightningTicksLeft(10); + LightningEntity bolt = EntityType.LIGHTNING_BOLT.create(world); + bolt.setCosmetic(true); + bolt.resetPosition(pos.getX() + 0.5F, pos.getY(), pos.getZ() + 0.5F); + world.spawnEntity(bolt); + } + + public void setEntity(@Nullable Entity entity) { + entityData = Optional.ofNullable(entity).map(e -> new EntityData( + writeEntityToNbt(e), + e.getDisplayName(), + EntityRelationship.of(e).map(EntityRelationship::getGender).orElse(Gender.MALE) + )); + computedName = null; + markDirty(); + + if (hasWorld()) { + world.playSound(null, pos.getX(), pos.getY(), pos.getZ(), SoundEvents.BLOCK_BELL_USE, SoundCategory.BLOCKS, 1, 1); + world.syncWorldEvent(WorldEventsCompat.BLOCK_BROKEN, pos, Block.getRawIdFromState(getCachedState())); + // TODO: 1.17 + // world.emitGameEvent(GameEvent.BLOCK_CHANGE, pos); + ((TombstoneBlock) getCachedState().getBlock()).updateNeighbors(getCachedState(), world, pos); + + if (!world.isClient) { + GraveyardManager.get((ServerWorld) world).setTombstoneState(pos, + hasEntity() ? GraveyardManager.TombstoneState.FILLED : GraveyardManager.TombstoneState.EMPTY + ); + sync(); + } + } + } + + public boolean hasEntity() { + return entityData.isPresent(); + } + + public Gender getGender() { + return entityData.map(e -> e.gender).orElse(Gender.MALE); + } + + public Optional getEntityName() { + return entityData.map(e -> e.name); + } + + public FlowingText getOrCreateEntityName(Function factory) { + if (computedName == null) { + computedName = factory.apply(getEntityName().orElse(LiteralText.EMPTY)); + } + return computedName; + } + + public Optional createEntity(World world, boolean remove) { + try { + return entityData.flatMap(data -> EntityType.getEntityFromNbt(data.nbt, world)); + } finally { + if (remove) { + setEntity(null); + } + } + } + + private NbtCompound writeEntityToNbt(Entity entity) { + NbtCompound nbt = new NbtCompound(); + entity.writeNbt(nbt); + nbt.putString("id", EntityType.getId(entity.getType()).toString()); + return nbt; + } + + protected void sync() { + world.updateListeners(this.getPos(), this.getCachedState(), this.getCachedState(), 3); + } + + //@Override + public void fromClientTag(NbtCompound tag) { + entityData = tag.contains("entityData", NbtElementCompat.COMPOUND_TYPE) ? Optional.of(new EntityData(tag)) : Optional.empty(); + resurrectionProgress = tag.getInt("resurrectionProgress"); + cure = tag.getBoolean("cure"); + } + + //@Override + public NbtCompound toClientTag(NbtCompound tag) { + return writeNbt(tag); + } + + @Override + public void fromTag(BlockState state, NbtCompound nbt) { + super.fromTag(state, nbt); + fromClientTag(nbt); + } + + @Override + public NbtCompound toInitialChunkDataNbt() { + return this.writeNbt(new NbtCompound()); + } + + @Override + public BlockEntityUpdateS2CPacket toUpdatePacket() { + return new BlockEntityUpdateS2CPacket(this.pos, 127, this.toInitialChunkDataNbt()); + } + + @Override + public NbtCompound writeNbt(NbtCompound nbt) { + entityData.ifPresent(data -> data.writeNbt(nbt)); + nbt.putInt("resurrectionProgress", resurrectionProgress); + nbt.putBoolean("cure", cure); + return super.writeNbt(nbt); + } + + public void readFromStack(ItemStack stack) { + entityData = Optional.ofNullable(stack).map(s -> s.getSubTag("entityData")).map(EntityData::new); + } + + public void writeToStack(ItemStack stack) { + entityData.ifPresent(data -> { + data.writeNbt(stack.getOrCreateSubTag("entityData")); + getEntityName().ifPresent(name -> { + NbtHelper.computeIfAbsent( + stack.getOrCreateSubTag(ItemStackCompat.DISPLAY_KEY), + ItemStackCompat.LORE_KEY, NbtElementCompat.LIST_TYPE, NbtList::new) + .add(0, NbtString.of(name.asString())); + }); + }); + } + + public static Optional of(@Nullable BlockEntity be) { + return Optional.ofNullable(be).filter(p -> p instanceof Data).map(Data.class::cast); + } + + static final class EntityData { + private final NbtCompound nbt; + private final Text name; + private final Gender gender; + + public EntityData(NbtCompound nbt, Text name, Gender gender) { + this.nbt = nbt; + this.name = name == null ? LiteralText.EMPTY : new LiteralText(name.asString()); + this.gender = gender; + } + + EntityData(NbtCompound nbt) { + this( + nbt.getCompound("entityData"), + Text.Serializer.fromJson(nbt.getString("entityName")), + Gender.byId(nbt.getInt("entityGender")) + ); + } + + void writeNbt(NbtCompound nbt) { + nbt.put("entityData", this.nbt); + nbt.putString("entityName", Text.Serializer.toJson(name)); + nbt.putInt("entityGender", gender.ordinal()); + } + } + } +} diff --git a/common/src/main/java/mca/client/book/Book.java b/common/src/main/java/mca/client/book/Book.java new file mode 100644 index 0000000000..172c613429 --- /dev/null +++ b/common/src/main/java/mca/client/book/Book.java @@ -0,0 +1,117 @@ +package mca.client.book; + +import java.util.LinkedList; +import java.util.List; +import mca.client.book.pages.EmptyPage; +import mca.client.book.pages.Page; +import mca.client.book.pages.TextPage; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +public class Book { + private final String bookName; + private final Text bookAuthor; + private final List pages = new LinkedList<>(); + private Identifier background = new Identifier("textures/gui/book.png"); + private Formatting textFormatting = Formatting.BLACK; + private boolean pageTurnSound = true; + + public Book(String bookName) { + this(bookName, new TranslatableText(String.format("mca.books.%s.author", bookName)).formatted(Formatting.GRAY)); + } + + public Book(String bookName, Text bookAuthor) { + this.bookName = bookName; + this.bookAuthor = bookAuthor; + } + + public Book setBackground(Identifier background) { + this.background = background; + return this; + } + + public Book setTextFormatting(Formatting textFormatting) { + this.textFormatting = textFormatting; + return this; + } + + public Book setPageTurnSound(boolean pageTurnSound) { + this.pageTurnSound = pageTurnSound; + return this; + } + + public Book addPage(Page page) { + pages.add(page); + return this; + } + + private Book addPages(List pages) { + for (Page p : pages) { + addPage(p); + } + return this; + } + + public Book addSimplePages(int n) { + return addSimplePages(n, 0); + } + + public Book addSimplePages(int n, int start) { + for (int i = 0; i < n; i++) { + addPage(new TextPage(getBookName(), start + i)); + } + return this; + } + + public int getPageCount() { + return pages.size(); + } + + public String getBookName() { + return bookName; + } + + @Nullable + public Text getBookAuthor() { + return bookAuthor; + } + + public List getPages() { + return pages; + } + + public Identifier getBackground() { + return background; + } + + public Formatting getTextFormatting() { + return textFormatting; + } + + public boolean hasPageTurnSound() { + return pageTurnSound; + } + + public Page getPage(int index) { + return index < pages.size() ? pages.get(index) : new EmptyPage(); + } + + public void open() { + + } + + public void setPage(int i, boolean back) { + getPage(i).open(back); + } + + public Book copy() { + return new Book(getBookName()) + .setBackground(getBackground()) + .setTextFormatting(getTextFormatting()) + .setPageTurnSound(hasPageTurnSound()) + .addPages(pages); + } +} diff --git a/common/src/main/java/mca/client/book/pages/DynamicListPage.java b/common/src/main/java/mca/client/book/pages/DynamicListPage.java new file mode 100644 index 0000000000..9273b8d027 --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/DynamicListPage.java @@ -0,0 +1,30 @@ +package mca.client.book.pages; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; +import net.minecraft.text.Text; + +public class DynamicListPage extends ListPage { + private final Function> generator; + + public DynamicListPage(String title, Function> generator) { + super(title, new LinkedList<>()); + + this.generator = generator; + } + + public DynamicListPage(Text title, Function> generator) { + super(title, new LinkedList<>()); + + this.generator = generator; + } + + @Override + public void open(boolean back) { + text.clear(); + text.addAll(generator.apply(this)); + + super.open(back); + } +} diff --git a/common/src/main/java/mca/client/book/pages/EmptyPage.java b/common/src/main/java/mca/client/book/pages/EmptyPage.java new file mode 100644 index 0000000000..bc15375c7d --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/EmptyPage.java @@ -0,0 +1,11 @@ +package mca.client.book.pages; + +import mca.client.gui.ExtendedBookScreen; +import net.minecraft.client.util.math.MatrixStack; + +public class EmptyPage extends Page { + @Override + public void render(ExtendedBookScreen screen, MatrixStack matrices, int mouseX, int mouseY, float delta) { + + } +} diff --git a/common/src/main/java/mca/client/book/pages/ListPage.java b/common/src/main/java/mca/client/book/pages/ListPage.java new file mode 100644 index 0000000000..fc2beaa2c0 --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/ListPage.java @@ -0,0 +1,69 @@ +package mca.client.book.pages; + +import java.util.List; +import mca.client.gui.ExtendedBookScreen; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; + +public class ListPage extends Page { + final Text title; + final List text; + + int page; + + public static int entriesPerPage = 11; + + public ListPage(Text title, List text) { + this.title = title; + this.text = text; + } + + public ListPage(String title, List text) { + this(new TranslatableText(title).formatted(Formatting.BLACK).formatted(Formatting.BOLD), text); + } + + private static void drawCenteredText(MatrixStack matrices, TextRenderer textRenderer, Text text, int centerX, int y, int color) { + OrderedText orderedText = text.asOrderedText(); + textRenderer.draw(matrices, orderedText, (float)(centerX - textRenderer.getWidth(orderedText) / 2), (float)y, color); + } + + @Override + public void render(ExtendedBookScreen screen, MatrixStack matrices, int mouseX, int mouseY, float delta) { + drawCenteredText(matrices, screen.getTextRenderer(), title, screen.width / 2, 35, 0xFFFFFFFF); + + int y = 48; + for (int i = page * entriesPerPage; i < Math.min(text.size(), (page + 1) * entriesPerPage); i++) { + drawCenteredText(matrices, screen.getTextRenderer(), text.get(i), screen.width / 2, y, 0xFFFFFFFF); + y += 10; + } + } + + @Override + public void open(boolean back) { + page = back ? (text.size() - 1) / entriesPerPage : 0; + } + + @Override + public boolean previousPage() { + if (page > 0) { + page--; + return false; + } else { + return true; + } + } + + @Override + public boolean nextPage() { + if (page < (text.size() - 1) / entriesPerPage) { + page++; + return false; + } else { + return true; + } + } +} diff --git a/common/src/main/java/mca/client/book/pages/Page.java b/common/src/main/java/mca/client/book/pages/Page.java new file mode 100644 index 0000000000..a888d3565d --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/Page.java @@ -0,0 +1,21 @@ +package mca.client.book.pages; + +import mca.client.book.Book; +import mca.client.gui.ExtendedBookScreen; +import net.minecraft.client.util.math.MatrixStack; + +public abstract class Page { + public abstract void render(ExtendedBookScreen screen, MatrixStack matrices, int mouseX, int mouseY, float delta); + + public void open(boolean back) { + + } + + public boolean previousPage() { + return true; + } + + public boolean nextPage() { + return true; + } +} diff --git a/common/src/main/java/mca/client/book/pages/ScribbleTextPage.java b/common/src/main/java/mca/client/book/pages/ScribbleTextPage.java new file mode 100644 index 0000000000..0a32d50da0 --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/ScribbleTextPage.java @@ -0,0 +1,32 @@ +package mca.client.book.pages; + +import com.mojang.blaze3d.systems.RenderSystem; +import mca.client.gui.ExtendedBookScreen; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.Identifier; + +public class ScribbleTextPage extends TextPage { + Identifier scribble; + + public ScribbleTextPage(Identifier scribble, String name, int page) { + super(name, page); + this.scribble = scribble; + } + + public ScribbleTextPage(Identifier scribble, String text) { + super(text); + this.scribble = scribble; + } + + public void render(ExtendedBookScreen screen, MatrixStack matrices, int mouseX, int mouseY, float delta) { + // scribble + int i = (screen.width - 192) / 2; + RenderSystem.enableBlend(); + screen.bindTexture(scribble); + DrawableHelper.drawTexture(matrices, i + 28, 32, 0, 0, 128, 128, 128, 128); + RenderSystem.disableBlend(); + + super.render(screen, matrices, mouseX, mouseY, delta); + } +} diff --git a/common/src/main/java/mca/client/book/pages/TextPage.java b/common/src/main/java/mca/client/book/pages/TextPage.java new file mode 100644 index 0000000000..1fa91789c3 --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/TextPage.java @@ -0,0 +1,45 @@ +package mca.client.book.pages; + +import java.util.List; +import mca.client.gui.ExtendedBookScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.OrderedText; +import net.minecraft.text.StringVisitable; +import net.minecraft.text.Text; + +public class TextPage extends Page { + protected String content; + private List cachedPage; + + public TextPage(String name, int page) { + content = String.format("{ \"translate\": \"mca.books.%s.%d\" }", name, page); + } + + public TextPage(String content) { + this.content = content; + } + + public void render(ExtendedBookScreen screen, MatrixStack matrices, int mouseX, int mouseY, float delta) { + //prepare page + if (content != null) { + if (cachedPage == null) { + StringVisitable stringVisitable = StringVisitable.plain(content); + try { + stringVisitable = Text.Serializer.fromJson(content); + } catch (Exception ignored) { + } + + cachedPage = screen.getTextRenderer().wrapLines(stringVisitable, 114); + } + + // text + int l = Math.min(128 / 9, cachedPage.size()); + int i = (screen.width - 192) / 2; + for (int m = 0; m < l; ++m) { + OrderedText orderedText = cachedPage.get(m); + float x = i + 36; + screen.getTextRenderer().draw(matrices, orderedText, x, (32.0f + m * 9.0f), 0); + } + } + } +} diff --git a/common/src/main/java/mca/client/book/pages/TitlePage.java b/common/src/main/java/mca/client/book/pages/TitlePage.java new file mode 100644 index 0000000000..00ab7fb826 --- /dev/null +++ b/common/src/main/java/mca/client/book/pages/TitlePage.java @@ -0,0 +1,58 @@ +package mca.client.book.pages; + +import java.util.List; +import mca.client.gui.ExtendedBookScreen; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; + +public class TitlePage extends Page { + Text title; + Text subtitle; + + public TitlePage(String book) { + this(book, Formatting.BLACK); + } + + public TitlePage(String book, Formatting color) { + this("item.mca.book_" + book, "mca.books." + book + ".author", color); + } + + public TitlePage(String title, String subtitle) { + this(title, subtitle, Formatting.BLACK); + } + + public TitlePage(String title, String subtitle, Formatting color) { + this(new TranslatableText(title).formatted(color).formatted(Formatting.BOLD), + new TranslatableText(subtitle).formatted(color).formatted(Formatting.ITALIC)); + } + + public TitlePage(Text title, Text subtitle) { + this.title = title; + this.subtitle = subtitle; + } + + private static void drawCenteredText(MatrixStack matrices, TextRenderer textRenderer, Text text, int centerX, int y, int color) { + OrderedText orderedText = text.asOrderedText(); + drawCenteredText(matrices, textRenderer, orderedText, centerX, y, color); + } + + private static void drawCenteredText(MatrixStack matrices, TextRenderer textRenderer, OrderedText text, int centerX, int y, int color) { + textRenderer.draw(matrices, text, (float)(centerX - textRenderer.getWidth(text) / 2), (float)y, color); + } + + @Override + public void render(ExtendedBookScreen screen, MatrixStack matrices, int mouseX, int mouseY, float delta) { + List texts = screen.getTextRenderer().wrapLines(title, 114); + int y = 80 - 5 * texts.size(); + for (OrderedText t : texts) { + drawCenteredText(matrices, screen.getTextRenderer(), t, screen.width / 2 - 2, y, 0xFFFFFF); + y += 10; + } + y = 82 + 5 * texts.size(); + drawCenteredText(matrices, screen.getTextRenderer(), subtitle, screen.width / 2 - 2, y, 0xFFFFFF); + } +} diff --git a/common/src/main/java/mca/client/gui/AbstractDynamicScreen.java b/common/src/main/java/mca/client/gui/AbstractDynamicScreen.java new file mode 100644 index 0000000000..23e28c22f3 --- /dev/null +++ b/common/src/main/java/mca/client/gui/AbstractDynamicScreen.java @@ -0,0 +1,153 @@ +package mca.client.gui; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import mca.client.resources.Icon; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; + +public abstract class AbstractDynamicScreen extends Screen { + protected static final float iconScale = 1.5f; + + // Tracks which page we're on in the GUI for sending button events + private String activeScreen = "main"; + + private int mouseX; + private int mouseY; + + private Set constraints = new HashSet<>(); + + protected AbstractDynamicScreen(Text title) { + super(title); + } + + public String getActiveScreen() { + return activeScreen; + } + + public Set getConstraints() { + return constraints; + } + + public void setConstraints(Set constraints) { + this.constraints = constraints; + setLayout(activeScreen); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + super.render(matrices, mouseX, mouseY, delta); + this.mouseX = mouseX; + this.mouseY = mouseY; + } + + protected abstract void buttonPressed(Button button); + + protected void disableButton(String id) { + children().forEach(b -> { + if (b instanceof ButtonEx) { + if (((ButtonEx) b).getApiButton().identifier().equals(id)) { + ((ButtonEx)b).active = false; + } + } + }); + } + + protected void enableAllButtons() { + children().forEach(b -> { + if (b instanceof ClickableWidget) { + ((ClickableWidget)b).active = true; + } + }); + } + + protected void disableAllButtons() { + this.children().forEach(b -> { + if (b instanceof ClickableWidget) { + if (b instanceof ButtonEx) { + if (!((ButtonEx) b).getApiButton().identifier().equals("gui.button.backarrow")) { + ((ClickableWidget)b).active = false; + } + } else { + ((ClickableWidget)b).active = false; + } + } + }); + } + + /** + * Adds API buttons to the GUI screen provided. + * + * @param guiKey String key for the GUI's buttons + */ + public void setLayout(String guiKey) { + activeScreen = guiKey; + + children.clear(); + buttons.clear(); + MCAScreens.getInstance().getScreen(guiKey).ifPresent(buttons -> { + for (Button b : buttons) { + addButton(new ButtonEx(b, this)); + } + }); + } + + protected void drawIcon(MatrixStack transform, String key) { + Icon icon = MCAScreens.getInstance().getIcon(key); + this.drawTexture(transform, (int) (icon.x() / iconScale), (int) (icon.y() / iconScale), icon.u(), icon.v(), 16, 16); + } + + protected void drawHoveringIconText(MatrixStack transform, Text text, String key) { + Icon icon = MCAScreens.getInstance().getIcon(key); + renderTooltip(transform, text, icon.x() + 16, icon.y() + 20); + } + + protected void drawHoveringIconText(MatrixStack transform, List text, String key) { + Icon icon = MCAScreens.getInstance().getIcon(key); + renderTooltip(transform, text, icon.x() + 16, icon.y() + 20); + } + + //checks if the mouse hovers over a specified button + protected boolean hoveringOverIcon(String key) { + Icon icon = MCAScreens.getInstance().getIcon(key); + return hoveringOver(icon.x(), icon.y(), (int) (16 * iconScale), (int) (16 * iconScale)); + } + + //checks if the mouse hovers over a rectangle + protected boolean hoveringOver(int x, int y, int w, int h) { + return mouseX > x && mouseX < x + w && mouseY > y && mouseY < y + h; + } + + private static class ButtonEx extends ButtonWidget { + private final Button apiButton; + + public ButtonEx(Button apiButton, AbstractDynamicScreen screen) { + super((screen.width / 2) + apiButton.x(), + (screen.height / 2) + apiButton.y(), + apiButton.width(), + apiButton.height(), + new TranslatableText(apiButton.identifier()), + a -> screen.buttonPressed(apiButton)); + this.apiButton = apiButton; + + // Remove the button if we specify it should not be present on constraint failure + // Otherwise we just mark the button as disabled. + if (!apiButton.isValidForConstraint(screen.getConstraints())) { + if (apiButton.hideOnFail()) { + visible = false; + } + active = false; + } + } + + public Button getApiButton() { + return apiButton; + } + } +} diff --git a/common/src/main/java/mca/client/gui/BlueprintScreen.java b/common/src/main/java/mca/client/gui/BlueprintScreen.java new file mode 100644 index 0000000000..c0e3636689 --- /dev/null +++ b/common/src/main/java/mca/client/gui/BlueprintScreen.java @@ -0,0 +1,622 @@ +package mca.client.gui; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import mca.client.gui.widget.RectangleWidget; +import mca.cobalt.network.NetworkHandler; +import mca.network.GetVillageRequest; +import mca.network.RenameVillageMessage; +import mca.network.ReportBuildingMessage; +import mca.network.SaveVillageMessage; +import mca.resources.Rank; +import mca.resources.data.BuildingType; +import mca.resources.data.tasks.Task; +import mca.server.world.data.Building; +import mca.server.world.data.Village; +import mca.util.compat.RenderSystemCompat; +import mca.util.localization.FlowingText; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.gui.widget.TexturedButtonWidget; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3i; +import net.minecraft.util.registry.Registry; + +public class BlueprintScreen extends ExtendedScreen { + //gui element Y positions + private final int positionTaxes = -60; + private final int positionBirth = -10; + private final int positionMarriage = 40; + private Village village; + private int reputation; + private Rank rank; + private Set completedTasks; + private String page; + private ButtonWidget[] buttonTaxes; + private ButtonWidget[] buttonBirths; + private ButtonWidget[] buttonMarriage; + private ButtonWidget buttonPage; + private int pageNumber = 0; + private final List catalogButtons = new LinkedList<>(); + + private static final Identifier ICON_TEXTURES = new Identifier("mca:textures/buildings.png"); + private BuildingType selectedBuilding; + private UUID selectedVillager; + + private int mouseX; + private int mouseY; + + private Map> tasks; + private Map buildingTypes; + + public BlueprintScreen() { + super(new LiteralText("Blueprint")); + } + + private void saveVillage() { + NetworkHandler.sendToServer(new SaveVillageMessage(village)); + } + + private void changeTaxes(int d) { + village.setTaxes(Math.max(0, Math.min(100, village.getTaxes() + d))); + saveVillage(); + } + + private void changePopulationThreshold(int d) { + village.setPopulationThreshold(Math.max(0, Math.min(100, village.getPopulationThreshold() + d))); + saveVillage(); + } + + private void changeMarriageThreshold(int d) { + village.setMarriageThreshold(Math.max(0, Math.min(100, village.getMarriageThreshold() + d))); + saveVillage(); + } + + private ButtonWidget[] createValueChanger(int x, int y, int w, int h, Consumer onPress) { + ButtonWidget[] buttons = new ButtonWidget[3]; + + buttons[1] = addButton(new ButtonWidget(x - w / 2, y, w / 4, h, + new LiteralText("<<"), (b) -> onPress.accept(false))); + + buttons[2] = addButton(new ButtonWidget(x + w / 4, y, w / 4, h, + new LiteralText(">>"), (b) -> onPress.accept(true))); + + buttons[0] = addButton(new ButtonWidget(x - w / 4, y, w / 2, h, + new LiteralText(""), (b) -> { + })); + + return buttons; + } + + protected void drawBuildingIcon(MatrixStack transform, int x, int y, int u, int v) { + transform.push(); + transform.translate(x - 6.6, y - 6.6, 0); + transform.scale(0.66f, 0.66f, 0.66f); + this.drawTexture(transform, 0, 0, u, v, 20, 20); + transform.pop(); + } + + @Override + public void init() { + NetworkHandler.sendToServer(new GetVillageRequest()); + setPage("waiting"); + } + + private void setPage(String page) { + if (page.equals("close")) { + MinecraftClient.getInstance().openScreen(null); + return; + } + + this.page = page; + + buttons.clear(); + this.children.clear(); + + //page selection + int bx = width / 2 - 180; + int by = height / 2 - 56; + if (!page.equals("rename")) { + if (!page.equals("empty") && !page.equals("waiting")) { + for (String p : new String[] {"map", "rank", "catalog", "villagers", "rules", "close"}) { + ButtonWidget widget = new ButtonWidget(bx, by, 80, 20, new TranslatableText("gui.blueprint." + p), (b) -> setPage(p)); + addButton(widget); + if (page.equals(p)) { + widget.active = false; + } + by += 22; + } + } + } + + switch (page) { + case "advanced": + //add building + bx = width / 2 + 180 - 64 - 16; + by = height / 2 - 56; + TranslatableText text = new TranslatableText("gui.blueprint.autoScan"); + if (village.isAutoScan()) { + text.formatted(Formatting.GREEN); + } else { + text.formatted(Formatting.GRAY).formatted(Formatting.STRIKETHROUGH); + } + addButton(new ButtonWidget(bx, by, 96, 20, text, (b) -> { + NetworkHandler.sendToServer(new ReportBuildingMessage(ReportBuildingMessage.Action.AUTO_SCAN)); + NetworkHandler.sendToServer(new GetVillageRequest()); + village.toggleAutoScan(); + setPage(page); + })); + by += 22; + + //restrict access + addButton(new ButtonWidget(bx, by, 96, 20, new TranslatableText("gui.blueprint.restrictAccess"), (b) -> { + NetworkHandler.sendToServer(new ReportBuildingMessage(ReportBuildingMessage.Action.RESTRICT)); + NetworkHandler.sendToServer(new GetVillageRequest()); + })); + by += 22; + + //add room + addButton(new ButtonWidget(bx, by, 96, 20, new TranslatableText("gui.blueprint.addRoom"), (b) -> { + NetworkHandler.sendToServer(new ReportBuildingMessage(ReportBuildingMessage.Action.ADD_ROOM)); + NetworkHandler.sendToServer(new GetVillageRequest()); + })); + by += 22 * 3; + + //rename village + addButton(new ButtonWidget(bx, by, 96, 20, new TranslatableText("gui.blueprint.renameVillage"), (b) -> { + setPage("rename"); + })); + by += 22; + case "map": + //add building + bx = width / 2 + 180 - 64 - 16; + by = height / 2 - 56 + 22 * 3; + addButton(new ButtonWidget(bx, by, 96, 20, new TranslatableText("gui.blueprint.addBuilding"), (b) -> { + NetworkHandler.sendToServer(new ReportBuildingMessage(ReportBuildingMessage.Action.ADD)); + NetworkHandler.sendToServer(new GetVillageRequest()); + })); + by += 22; + + //remove building + addButton(new ButtonWidget(bx, by, 96, 20, new TranslatableText("gui.blueprint.removeBuilding"), (b) -> { + NetworkHandler.sendToServer(new ReportBuildingMessage(ReportBuildingMessage.Action.REMOVE)); + NetworkHandler.sendToServer(new GetVillageRequest()); + })); + by += 22; + + //advanced + if (!page.equals("advanced")) { + addButton(new ButtonWidget(bx, by, 96, 20, new TranslatableText("gui.blueprint.advanced"), (b) -> { + setPage("advanced"); + })); + } + + break; + case "rank": + break; + case "catalog": + //list catalog button + int row = 0; + int col = 0; + int size = 21; + int x = width / 2 - 4 * size - 8; + int y = (int)(height / 2 - 2.0 * size); + catalogButtons.clear(); + for (BuildingType bt : buildingTypes.values()) { + if (bt.visible()) { + TexturedButtonWidget widget = new TexturedButtonWidget( + row * size + x + 10, col * size + y - 10, 20, 20, bt.iconU(), bt.iconV() + 20, 20, ICON_TEXTURES, 256, 256, button -> { + selectBuilding(bt); + button.active = false; + catalogButtons.forEach(b -> b.active = true); + }, new TranslatableText("buildingType." + bt.name())); + catalogButtons.add(addButton(widget)); + + row++; + if (row > 4) { + row = 0; + col++; + } + } + } + break; + case "villagers": + addButton(new ButtonWidget(width / 2 - 24 - 20, height / 2 + 54, 20, 20, new LiteralText("<"), (b) -> { + if (pageNumber > 0) { + pageNumber--; + } + })); + addButton(new ButtonWidget(width / 2 + 24, height / 2 + 54, 20, 20, new LiteralText(">"), (b) -> { + if (pageNumber < Math.ceil(village.getPopulation() / 9.0) - 1) { + pageNumber++; + } + })); + buttonPage = addButton(new ButtonWidget(width / 2 - 24, height / 2 + 54, 48, 20, new LiteralText("0/0)"), (b) -> { + })); + break; + case "rules": + //taxes + buttonTaxes = createValueChanger(width / 2, height / 2 + positionTaxes + 10, 80, 20, (b) -> changeTaxes(b ? 10 : -10)); + toggleButtons(buttonTaxes, false); + + //birth threshold + buttonBirths = createValueChanger(width / 2, height / 2 + positionBirth + 10, 80, 20, (b) -> changePopulationThreshold(b ? 10 : -10)); + toggleButtons(buttonBirths, false); + + //marriage threshold + buttonMarriage = createValueChanger(width / 2, height / 2 + positionMarriage + 10, 80, 20, (b) -> changeMarriageThreshold(b ? 10 : -10)); + toggleButtons(buttonMarriage, false); + break; + case "rename": + TextFieldWidget field = addButton(new TextFieldWidget(textRenderer, width / 2 - 65, height / 2 - 16, 130, 20, new TranslatableText("gui.blueprint.renameVillage"))); + field.setMaxLength(32); + field.setText(village.getName()); + + addButton(new ButtonWidget(width / 2 - 66, height / 2 + 8, 64, 20, new TranslatableText("gui.blueprint.cancel"), (b) -> { + setPage("map"); + })); + addButton(new ButtonWidget(width / 2 + 2, height / 2 + 8, 64, 20, new TranslatableText("gui.blueprint.rename"), (b) -> { + NetworkHandler.sendToServer(new RenameVillageMessage(village.getId(), field.getText())); + village.setName(field.getText()); + setPage("map"); + })); + break; + } + } + + private void selectBuilding(BuildingType b) { + selectedBuilding = b; + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void render(MatrixStack transform, int sizeX, int sizeY, float offset) { + renderBackground(transform); + + this.mouseX = (int)(client.mouse.getX() * width / client.getWindow().getFramebufferWidth()); + this.mouseY = (int)(client.mouse.getY() * height / client.getWindow().getFramebufferHeight()); + + switch (page) { + case "waiting": + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.waiting"), width / 2, height / 2, 0xffaaaaaa); + break; + case "empty": + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.empty"), width / 2, height / 2, 0xffaaaaaa); + break; + case "map": + renderStats(transform); + case "advanced": + renderName(transform); + renderMap(transform); + break; + case "rank": + renderTasks(transform); + renderStats(transform); + break; + case "catalog": + renderCatalog(transform); + break; + case "villagers": + renderVillagers(transform); + break; + case "rules": + renderRules(transform); + break; + } + + super.render(transform, sizeX, sizeY, offset); + } + + private void renderName(MatrixStack transform) { + //name + transform.push(); + transform.scale(2.0f, 2.0f, 2.0f); + drawCenteredText(transform, textRenderer, village.getName(), width / 4, height / 4 - 48, 0xffffffff); + transform.pop(); + } + + private void renderStats(MatrixStack transform) { + int x = width / 2 + (page.equals("rank") ? -70 : 105); + int y = height / 2 - 50; + + //rank + Text rankStr = new TranslatableText("gui.village.rank." + rank.ordinal()); + int rankColor = rank.ordinal() == 0 ? 0xffff0000 : 0xffffff00; + + textRenderer.drawWithShadow(transform, new TranslatableText("gui.blueprint.currentRank", rankStr), x, y, rankColor); + textRenderer.drawWithShadow(transform, new TranslatableText("gui.blueprint.reputation", String.valueOf(reputation)), x, y + 11, rank.ordinal() == 0 ? 0xffff0000 : 0xffffffff); + textRenderer.drawWithShadow(transform, new TranslatableText("gui.blueprint.buildings", village.getBuildings().size()), x, y + 22, 0xffffffff); + textRenderer.drawWithShadow(transform, new TranslatableText("gui.blueprint.population", village.getPopulation(), village.getMaxPopulation()), x, y + 33, 0xffffffff); + } + + private void renderMap(MatrixStack transform) { + int mapSize = 75; + int y = height / 2 + 8; + RectangleWidget.drawRectangle(transform, width / 2 - mapSize, y - mapSize, width / 2 + mapSize, y + mapSize, 0xffffff88); + + transform.push(); + + RenderSystemCompat.setShaderTexture(0, ICON_TEXTURES); + + //center and scale the map + float sc = Math.min((float)mapSize / (village.getBox().getMaxBlockCount() + 3) * 2, 2.0f); + int mouseLocalX = (int)((mouseX - width / 2.0) / sc + village.getCenter().getX()); + int mouseLocalY = (int)((mouseY - y) / sc + village.getCenter().getZ()); + transform.translate(width / 2.0, y, 0); + transform.scale(sc, sc, 0.0f); + transform.translate(-village.getCenter().getX(), -village.getCenter().getZ(), 0); + + //show the players location + ClientPlayerEntity player = client.player; + if (player != null) { + RectangleWidget.drawRectangle(transform, (int)player.getX() - 1, (int)player.getZ() - 1, (int)player.getX() + 1, (int)player.getZ() + 1, 0xffff00ff); + } + + //buildings + List hoverBuildings = new LinkedList(); + for (Building building : village.getBuildings().values()) { + BuildingType bt = buildingTypes.get(building.getType()); + + if (bt.isIcon()) { + BlockPos c = building.getCenter(); + drawBuildingIcon(transform, c.getX(), c.getZ(), bt.iconU(), bt.iconV()); + + //tooltip + int margin = 6; + if (c.getSquaredDistance(new Vec3i(mouseLocalX, c.getY(), mouseLocalY)) < margin * margin) { + hoverBuildings.add(building); + } + } else { + BlockPos p0 = building.getPos0(); + BlockPos p1 = building.getPos1(); + RectangleWidget.drawRectangle(transform, p0.getX(), p0.getZ(), p1.getX(), p1.getZ(), bt.getColor()); + + //icon + if (bt.visible()) { + BlockPos c = building.getCenter(); + drawBuildingIcon(transform, c.getX(), c.getZ(), bt.iconU(), bt.iconV()); + } + + //tooltip + int margin = 1; + if (mouseLocalX >= p0.getX() - margin && mouseLocalX <= p1.getX() + margin && mouseLocalY >= p0.getZ() - margin && mouseLocalY <= p1.getZ() + margin) { + hoverBuildings.add(building); + } + } + } + + transform.pop(); + + //sort vertically + hoverBuildings.sort((a, b) -> b.getCenter().getY() - a.getCenter().getY()); + + //get tooltips + List> tooltips = new LinkedList<>(); + for (Building b : hoverBuildings) { + tooltips.add(getBuildingTooltip(b)); + } + + //get height + int h = 0; + for (List b : tooltips) { + h += getTooltipHeight(b) + 9; + } + + //render + int py = mouseY - h / 2 + 12; + for (List b : tooltips) { + renderTooltip(transform, b, mouseX, py); + py += getTooltipHeight(b) + 9; + } + } + + private List getBuildingTooltip(Building hoverBuilding) { + List lines = new LinkedList<>(); + + //name + BuildingType bt = buildingTypes.get(hoverBuilding.getType()); + lines.add(new TranslatableText("buildingType." + bt.name())); + + //size + if (!bt.grouped()) { + lines.add(new TranslatableText("gui.blueprint.size", String.valueOf(hoverBuilding.getSize()))); + } + + //residents + for (String name : hoverBuilding.getResidents().values()) { + lines.add(new LiteralText(name)); + } + + //pois + if (hoverBuilding.getPois().size() > 0) { + lines.add(new TranslatableText("gui.blueprint.pois", hoverBuilding.getPois().size()).formatted(Formatting.GRAY)); + } + + //present blocks + for (Map.Entry block : hoverBuilding.getBlocks().entrySet()) { + lines.add(new LiteralText(block.getValue() + " x ").append(getBlockName(block.getKey())).formatted(Formatting.GRAY)); + } + + return lines; + } + + private void renderTasks(MatrixStack transform) { + if (rank == null) { + return; + } + + int y = height / 2 + 5; + int x = width / 2 - 70; + + //tasks + for (Task task : tasks.get(rank.promote())) { + boolean completed = completedTasks.contains(task.getId()); + Text t = task.getTranslatable().formatted(completed ? Formatting.STRIKETHROUGH : Formatting.RESET); + textRenderer.drawWithShadow(transform, t, x, y, completed ? 0xff88ff88 : 0xffff5555); + y += 11; + } + } + + private void renderCatalog(MatrixStack transform) { + //title + transform.push(); + transform.scale(2.0f, 2.0f, 2.0f); + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.catalogFull"), width / 4, height / 4 - 52, 0xffffffff); + transform.pop(); + + //explanation + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.catalogHint").formatted(Formatting.GRAY), width / 2, height / 2 - 82, 0xffffffff); + + //building + int x = width / 2 + 35; + int y = height / 2 - 50; + if (selectedBuilding != null) { + //name + textRenderer.drawWithShadow(transform, new TranslatableText("buildingType." + selectedBuilding.name()), x, y, selectedBuilding.getColor()); + + //description + List wrap = FlowingText.wrap(new TranslatableText("buildingType." + selectedBuilding.name() + ".description").formatted(Formatting.GRAY).formatted(Formatting.ITALIC), 150); + for (Text t : wrap) { + textRenderer.drawWithShadow(transform, t, x, y + 12, 0xffffffff); + y += 10; + } + + //size + Text size = selectedBuilding.size() == 0 ? new TranslatableText("gui.blueprint.anySize") : new TranslatableText("gui.blueprint.size", String.valueOf(selectedBuilding.size())); + textRenderer.drawWithShadow(transform, size, x, y + 20, 0xffdddddd); + + //required blocks + for (Map.Entry b : selectedBuilding.blockIds().entrySet()) { + textRenderer.drawWithShadow(transform, new LiteralText(b.getValue() + " x ").append(getBlockName(b.getKey())), x, y + 32, 0xffffffff); + y += 10; + } + } else { + //help + List wrap = FlowingText.wrap(new TranslatableText("gui.blueprint.buildingTypes").formatted(Formatting.GRAY).formatted(Formatting.ITALIC), 150); + for (Text t : wrap) { + textRenderer.drawWithShadow(transform, t, x, y, 0xffffffff); + y += 10; + } + } + } + + private void renderVillagers(MatrixStack transform) { + int maxPages = (int)Math.ceil(village.getPopulation() / 9.0); + buttonPage.setMessage(new LiteralText((pageNumber + 1) + "/" + maxPages)); + + List> villager = village.getBuildings().values().stream() + .flatMap(b -> b.getResidents().entrySet().stream()) + .sorted(Map.Entry.comparingByValue()) + .collect(Collectors.toList()); + + selectedVillager = null; + for (int i = 0; i < 9; i++) { + int index = i + pageNumber * 9; + if (index < villager.size()) { + int y = height / 2 - 51 + i * 11; + boolean hover = isMouseWithin(width / 2 - 50, y - 1, 100, 11); + drawCenteredText(transform, textRenderer, new LiteralText(villager.get(index).getValue()), width / 2, y, hover ? 0xFFD7D784 : 0xFFFFFFFF); + if (hover) { + selectedVillager = villager.get(index).getKey(); + } + } else { + break; + } + } + } + + private void renderRules(MatrixStack transform) { + buttonTaxes[0].setMessage(new LiteralText(village.getTaxes() + "%")); + buttonMarriage[0].setMessage(new LiteralText(village.getMarriageThreshold() + "%")); + buttonBirths[0].setMessage(new LiteralText(village.getPopulationThreshold() + "%")); + + //taxes + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.taxes"), width / 2, height / 2 + positionTaxes, 0xffffffff); + if (!rank.isAtLeast(Rank.MERCHANT)) { + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.rankTooLow"), width / 2, height / 2 + positionTaxes + 15, 0xffffffff); + toggleButtons(buttonTaxes, false); + } else { + toggleButtons(buttonTaxes, true); + } + + //births + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.birth"), width / 2, height / 2 + positionBirth, 0xffffffff); + if (!rank.isAtLeast(Rank.NOBLE)) { + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.rankTooLow"), width / 2, height / 2 + positionBirth + 15, 0xffffffff); + toggleButtons(buttonBirths, false); + } else { + toggleButtons(buttonBirths, true); + } + + //marriages + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.marriage"), width / 2, height / 2 + positionMarriage, 0xffffffff); + if (!rank.isAtLeast(Rank.MAYOR)) { + drawCenteredText(transform, textRenderer, new TranslatableText("gui.blueprint.rankTooLow"), width / 2, height / 2 + positionMarriage + 15, 0xffffffff); + toggleButtons(buttonMarriage, false); + } else { + toggleButtons(buttonMarriage, true); + } + } + + private Text getBlockName(Identifier id) { + if (Registry.BLOCK.containsId(id)) { + return new TranslatableText(Registry.BLOCK.get(id).getTranslationKey()); + } else { + return new TranslatableText("tag." + id.toString()); + } + } + + private void toggleButtons(ButtonWidget[] buttons, boolean active) { + for (ButtonWidget b : buttons) { + b.active = active; + b.visible = active; + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (page.equals("villagers") && selectedVillager != null) { + MinecraftClient.getInstance().openScreen(new FamilyTreeScreen(selectedVillager)); + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + protected boolean isMouseWithin(int x, int y, int w, int h) { + return mouseX >= x && mouseX < x + w && mouseY >= y && mouseY < y + h; + } + + public void setVillage(Village village) { + this.village = village; + if (village == null) { + setPage("empty"); + } else if (page.equals("waiting")) { + setPage("map"); + } + } + + public void setRank(Rank rank, int reputation, Set completedTasks, Map> tasks, Map buildingTypes) { + this.rank = rank; + this.reputation = reputation; + this.completedTasks = completedTasks; + this.tasks = tasks; + this.buildingTypes = buildingTypes; + } +} diff --git a/common/src/main/java/mca/client/gui/Button.java b/common/src/main/java/mca/client/gui/Button.java new file mode 100644 index 0000000000..8e5c69433f --- /dev/null +++ b/common/src/main/java/mca/client/gui/Button.java @@ -0,0 +1,100 @@ +package mca.client.gui; + +import java.util.Set; +import java.util.stream.Stream; + +/** + * Button is a button defined in assets/mca/api/gui/* + *

+ * These buttons are dynamically attached to a Screen and include additional instruction/constraints for building + * and processing interactions. + */ +public final class Button { + /** + * The text and action to perform for this button + */ + private final String identifier; + public String identifier() {return identifier;} + private final int x; + public int x() {return x;} + private final int y; + public int y() {return y;} + private final int width; + public int width() {return width;} + private final int height; + public int height() {return height;} + /** + * whether the button press is sent to the server for processing + */ + private final boolean notifyServer; + public boolean notifyServer() {return notifyServer;} + /** + * whether the button is processed by the villager or the server itself + */ + private final boolean targetServer; + public boolean targetServer() {return targetServer;} + /** + * list of EnumConstraints separated by the pipe character | + */ + private final String constraints; + public String constraints() {return constraints;} + /** + * Whether the button should be hidden completely when its constraints fail. The default is to simply disable it. + */ + private final boolean hideOnFail; + public boolean hideOnFail() {return hideOnFail;} + /** + * Whether the button is an interaction that generates a response and boosts/decreases hearts + */ + private final boolean isInteraction; + public boolean isInteraction() {return isInteraction;} + + public Button( /** + * The text and action to perform for this button + */ + String identifier, + int x, + int y, + int width, + int height, + /** + * whether the button press is sent to the server for processing + */ + boolean notifyServer, + /** + * whether the button is processed by the villager or the server itself + */ + boolean targetServer, + /** + * list of EnumConstraints separated by the pipe character | + */ + String constraints, + /** + * Whether the button should be hidden completely when its constraints fail. The default is to simply disable it. + */ + boolean hideOnFail, + /** + * Whether the button is an interaction that generates a response and boosts/decreases hearts + */ + boolean isInteraction) { + this.identifier = identifier; + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.notifyServer = notifyServer; + this.targetServer = targetServer; + this.constraints = constraints; + this.hideOnFail = hideOnFail; + this.isInteraction = isInteraction; + } + + public Stream getConstraints() { + return Constraint.fromStringList(constraints).stream(); + } + + //checks if a map of given evaluated constraints apply to this button + public boolean isValidForConstraint(Set constraints) { + return getConstraints().allMatch(constraints::contains); + } +} \ No newline at end of file diff --git a/common/src/main/java/mca/client/gui/Constraint.java b/common/src/main/java/mca/client/gui/Constraint.java new file mode 100644 index 0000000000..8224eee8fe --- /dev/null +++ b/common/src/main/java/mca/client/gui/Constraint.java @@ -0,0 +1,114 @@ +package mca.client.gui; + +import mca.entity.VillagerEntityMCA; +import mca.entity.VillagerLike; +import mca.entity.ai.MoveState; +import mca.entity.ai.ProfessionsMCA; +import mca.resources.Rank; +import mca.entity.ai.Relationship; +import mca.entity.ai.relationship.AgeState; +import mca.resources.Tasks; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.village.VillagerProfession; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum Constraint implements BiPredicate, Entity> { + FAMILY("family", Relationship.IS_FAMILY.asConstraint()), + NOT_FAMILY("!family", Relationship.IS_FAMILY.negate().asConstraint()), + + BABY("baby", (villager, player) -> villager.getAgeState() == AgeState.BABY), + NOT_BABY("!baby", (villager, player) -> villager.getAgeState() != AgeState.BABY), + + TEEN("teen", (villager, player) -> villager.getAgeState() == AgeState.TEEN), + NOT_TEEN("!teen", (villager, player) -> villager.getAgeState() != AgeState.TEEN), + + ADULT("adult", (villager, player) -> villager.getAgeState() == AgeState.ADULT), + NOT_ADULT("!adult", (villager, player) -> villager.getAgeState() != AgeState.ADULT), + + SPOUSE("spouse", Relationship.IS_MARRIED.asConstraint()), + NOT_SPOUSE("!spouse", Relationship.IS_MARRIED.negate().asConstraint()), + + KIDS("kids", Relationship.IS_PARENT.asConstraint()), + NOT_KIDS("!kids", Relationship.IS_PARENT.negate().asConstraint()), + + CLERIC("cleric", (villager, player) -> villager.getVillagerData().getProfession() == VillagerProfession.CLERIC), + NOT_CLERIC("!cleric", (villager, player) -> villager.getVillagerData().getProfession() != VillagerProfession.CLERIC), + + OUTLAWED("outlawed", (villager, player) -> villager.getVillagerData().getProfession() == ProfessionsMCA.OUTLAW), + NOT_OUTLAWED("!outlawed", (villager, player) -> villager.getVillagerData().getProfession() != ProfessionsMCA.OUTLAW), + + TRADER("trader", (villager, player) -> !ProfessionsMCA.canNotTrade.contains(villager.getVillagerData().getProfession())), + NOT_TRADER("!trader", (villager, player) -> ProfessionsMCA.canNotTrade.contains(villager.getVillagerData().getProfession())), + + PEASANT("peasant", (villager, player) -> isRankAtLeast(villager, player, Rank.PEASANT)), + NOT_PEASANT("!peasant", (villager, player) -> !isRankAtLeast(villager, player, Rank.PEASANT)), + + NOBLE("noble", (villager, player) -> isRankAtLeast(villager, player, Rank.NOBLE)), + NOT_NOBLE("!noble", (villager, player) -> !isRankAtLeast(villager, player, Rank.NOBLE)), + + MAYOR("mayor", (villager, player) -> isRankAtLeast(villager, player, Rank.MAYOR)), + NOT_MAYOR("!mayor", (villager, player) -> !isRankAtLeast(villager, player, Rank.MAYOR)), + + KING("king", (villager, player) -> isRankAtLeast(villager, player, Rank.KING)), + NOT_KING("!king", (villager, player) -> !isRankAtLeast(villager, player, Rank.KING)), + + ORPHAN("orphan", Relationship.IS_ORPHAN.asConstraint()), + NOT_ORPHAN("!orphan", Relationship.IS_ORPHAN.negate().asConstraint()), + + FOLLOWING("following", (villager, player) -> villager.getVillagerBrain().getMoveState() == MoveState.FOLLOW), + NOT_FOLLOWING("!following", (villager, player) -> villager.getVillagerBrain().getMoveState() != MoveState.FOLLOW), + + STAYING("staying", (villager, player) -> villager.getVillagerBrain().getMoveState() == MoveState.STAY), + NOT_STAYING("!staying", (villager, player) -> villager.getVillagerBrain().getMoveState() != MoveState.STAY); + + private static boolean isRankAtLeast(VillagerLike villager, Entity player, Rank rank) { + return player instanceof PlayerEntity && villager instanceof VillagerEntityMCA && ((VillagerEntityMCA)villager).getResidency().getHomeVillage() + .filter(village -> Tasks.getRank(village, (ServerPlayerEntity)player).isAtLeast(rank)).isPresent(); + } + + public static final Map REGISTRY = Stream.of(values()).collect(Collectors.toMap(a -> a.id, Function.identity())); + + private final String id; + private final BiPredicate, Entity> check; + + Constraint(String id, BiPredicate, Entity> check) { + this.id = id; + this.check = check; + } + + @Override + public boolean test(VillagerLike t, Entity u) { + return check.test(t, u); + } + + public static Set all() { + return new HashSet<>(REGISTRY.values()); + } + + public static Set allMatching(VillagerLike villager, Entity player) { + return Stream.of(values()).filter(c -> c.test(villager, player)).collect(Collectors.toSet()); + } + + public static List fromStringList(String constraints) { + if (constraints == null || constraints.isEmpty()) { + return new ArrayList<>(); + } + return Stream.of(constraints.split("\\,")) + .map(REGISTRY::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } +} + diff --git a/common/src/main/java/mca/client/gui/ExtendedBookScreen.java b/common/src/main/java/mca/client/gui/ExtendedBookScreen.java new file mode 100644 index 0000000000..172111f970 --- /dev/null +++ b/common/src/main/java/mca/client/gui/ExtendedBookScreen.java @@ -0,0 +1,172 @@ +package mca.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import mca.client.book.Book; +import mca.client.book.pages.Page; +import mca.client.gui.widget.ExtendedPageTurnWidget; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ScreenTexts; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.PageTurnWidget; +import net.minecraft.client.util.NarratorManager; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.text.ClickEvent; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +public class ExtendedBookScreen extends Screen { + private int pageIndex; + private PageTurnWidget nextPageButton; + private PageTurnWidget previousPageButton; + private final Book book; + + public ExtendedBookScreen(Book book) { + super(NarratorManager.EMPTY); + this.book = book; + book.open(); + book.setPage(0, false); + } + + public boolean setPage(int index) { + int i = MathHelper.clamp(index, 0, this.book.getPageCount() - 1); + if (i != this.pageIndex) { + book.setPage(i, false); + this.pageIndex = i; + this.updatePageButtons(); + return true; + } else { + return false; + } + } + + protected boolean jumpToPage(int page) { + return this.setPage(page); + } + + @Override + protected void init() { + this.addCloseButton(); + this.addPageButtons(); + } + + protected void addCloseButton() { + this.addButton(new ButtonWidget(this.width / 2 - 100, 196, 200, 20, ScreenTexts.DONE, (buttonWidget) -> this.client.openScreen(null))); + } + + protected void addPageButtons() { + int i = (this.width - 192) / 2; + this.nextPageButton = this.addButton(new ExtendedPageTurnWidget(i + 116, 159, true, (buttonWidget) -> goToNextPage(), book.hasPageTurnSound(), book.getBackground())); + this.previousPageButton = this.addButton(new ExtendedPageTurnWidget(i + 43, 159, false, (buttonWidget) -> goToPreviousPage(), book.hasPageTurnSound(), book.getBackground())); + this.updatePageButtons(); + } + + protected void goToPreviousPage() { + if (book.getPage(this.pageIndex).previousPage()) { + if (this.pageIndex > 0) { + --this.pageIndex; + book.setPage(this.pageIndex, true); + } + this.updatePageButtons(); + } + } + + protected void goToNextPage() { + if (book.getPage(this.pageIndex).nextPage()) { + if (this.pageIndex < book.getPageCount() - 1) { + ++this.pageIndex; + book.setPage(this.pageIndex, false); + } + this.updatePageButtons(); + } + } + + private void updatePageButtons() { + this.nextPageButton.visible = this.pageIndex < book.getPageCount() - 1; + this.previousPageButton.visible = this.pageIndex > 0; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (super.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } else { + switch (keyCode) { + case 266: + this.previousPageButton.onPress(); + return true; + case 267: + this.nextPageButton.onPress(); + return true; + default: + return false; + } + } + } + + public void bindTexture(Identifier tex) { + this.client.getTextureManager().bindTexture(tex); + } + + public TextRenderer getTextRenderer() { + return textRenderer; + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + this.renderBackground(matrices); + + // background + RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F); + bindTexture(book.getBackground()); + int i = (this.width - 192) / 2; + this.drawTexture(matrices, i, 2, 0, 0, 192, 192); + + // page number + if (book.getPageCount() > 1) { + Text pageIndexText = new TranslatableText("book.pageIndicator", this.pageIndex + 1, Math.max(book.getPageCount(), 1)).formatted(book.getTextFormatting()); + int k = textRenderer.getWidth(pageIndexText); + textRenderer.draw(matrices, pageIndexText, i - k + 192 - 44, 18.0f, 0); + } + + Page page = book.getPage(pageIndex); + if (page != null) { + page.render(this, matrices, mouseX, mouseY, delta); + } + + super.render(matrices, mouseX, mouseY, delta); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean handleTextClick(Style style) { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent == null) { + return false; + } else if (clickEvent.getAction() == ClickEvent.Action.CHANGE_PAGE) { + String string = clickEvent.getValue(); + + try { + int i = Integer.parseInt(string) - 1; + return this.jumpToPage(i); + } catch (Exception var5) { + return false; + } + } else { + boolean bl = super.handleTextClick(style); + if (bl && clickEvent.getAction() == ClickEvent.Action.RUN_COMMAND) { + this.client.openScreen(null); + } + + return bl; + } + } +} diff --git a/common/src/main/java/mca/client/gui/ExtendedScreen.java b/common/src/main/java/mca/client/gui/ExtendedScreen.java new file mode 100644 index 0000000000..f00bc45e71 --- /dev/null +++ b/common/src/main/java/mca/client/gui/ExtendedScreen.java @@ -0,0 +1,40 @@ +package mca.client.gui; + +import com.google.common.collect.Lists; +import java.util.List; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; + +public class ExtendedScreen extends Screen { + protected ExtendedScreen(Text title) { + super(title); + } + + public int getTooltipWidth(List lines_) { + List lines = Lists.transform(lines_, Text::asOrderedText); + + int w = 0; + if (!lines.isEmpty()) { + for (OrderedText orderedText : lines) { + int j = this.textRenderer.getWidth(orderedText); + if (j > w) { + w = j; + } + } + } + return w; + } + + public int getTooltipHeight(List lines_) { + List lines = Lists.transform(lines_, Text::asOrderedText); + + int h = 8; + if (!lines.isEmpty()) { + if (lines.size() > 1) { + h += 2 + (lines.size() - 1) * 10; + } + } + return h; + } +} diff --git a/common/src/main/java/mca/client/gui/FamilyTreeScreen.java b/common/src/main/java/mca/client/gui/FamilyTreeScreen.java new file mode 100644 index 0000000000..535d8465d8 --- /dev/null +++ b/common/src/main/java/mca/client/gui/FamilyTreeScreen.java @@ -0,0 +1,431 @@ +package mca.client.gui; + +import mca.cobalt.network.NetworkHandler; +import mca.entity.ai.relationship.MarriageState; +import mca.entity.ai.relationship.family.FamilyTreeNode; +import mca.network.GetFamilyTreeRequest; +import mca.util.compat.RenderSystemCompat; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.render.Tessellator; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.client.util.Window; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.sound.SoundEvents; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.math.Matrix4f; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.jetbrains.annotations.Nullable; +import org.lwjgl.opengl.GL11; + +public class FamilyTreeScreen extends Screen { + private static final int HORIZONTAL_SPACING = 20; + private static final int VERTICAL_SPACING = 60; + + private static final int SPOUSE_HORIZONTAL_SPACING = 50; + + private UUID focusedEntityId; + + private Map family = new HashMap<>(); + + private final Map nodes = new HashMap<>(); + + private final TreeNode emptyNode = new TreeNode(); + + private TreeNode tree = emptyNode; + + @Nullable + private TreeNode focused; + + private double scrollX; + private double scrollY; + + private final Screen parent; + + public FamilyTreeScreen(UUID entityId) { + super(new TranslatableText("gui.family_tree.title")); + this.focusedEntityId = entityId; + this.parent = MinecraftClient.getInstance().currentScreen; + } + + @Override + public boolean isPauseScreen() { + return false; + } + + public void setFamilyData(UUID uuid, Map family) { + this.focusedEntityId = uuid; + this.family.putAll(family); + rebuildTree(); + } + + private boolean focusEntity(UUID id) { + focusedEntityId = id; + + NetworkHandler.sendToServer(new GetFamilyTreeRequest(id)); + + return false; + } + + @Override + public void init() { + focusEntity(focusedEntityId); + + addButton(new ButtonWidget(width / 2 - 100, height - 25, 200, 20, new TranslatableText("gui.done"), sender -> { + onClose(); + })); + } + + @Override + public void onClose() { + client.openScreen(parent); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (button == 0) { + scrollX += deltaX; + scrollY += deltaY; + return true; + } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && focused != null) { + MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1)); + if (focusEntity(focused.id)) { + rebuildTree(); + } + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + renderBackground(matrices); + + fill(matrices, 0, 30, width, height - 30, 0x66000000); + + focused = null; + + Window window = MinecraftClient.getInstance().getWindow(); + double f = window.getScaleFactor(); + int windowHeight = (int)Math.round(window.getScaledHeight() * f); + + int x = 0; + int y = (int)(30 * f); + int w = (int)(width * f); + int h = (int)((height - 60) * f); + + GL11.glScissor(x, windowHeight - h - y, w, h); + GL11.glEnable(GL11.GL_SCISSOR_TEST); + + matrices.push(); + + int xx = (int)(scrollX + width / 2); + int yy = (int)(scrollY + height / 2); + matrices.translate(xx, yy, 0); + tree.render(matrices, mouseX - xx, mouseY - yy); + matrices.pop(); + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + FamilyTreeNode selected = family.get(focusedEntityId); + + Text label = selected == null ? title : new LiteralText(selected.getName()).append("'s ").append(title); + + drawCenteredText(matrices, textRenderer, label, width / 2, 10, 16777215); + + super.render(matrices, mouseX, mouseY, delta); + } + + private void rebuildTree() { + scrollX = 14; + scrollY = -69; + FamilyTreeNode focusedNode = family.get(focusedEntityId); + + // garbage collect + focused = null; + tree = emptyNode; + nodes.clear(); + + if (focusedNode != null) { + tree = insertParents(new TreeNode(focusedNode, true), focusedNode, 2); + } + } + + private TreeNode insertParents(TreeNode root, FamilyTreeNode focusedNode, int levels) { + @Nullable FamilyTreeNode father = family.get(focusedNode.father()); + @Nullable FamilyTreeNode mother = family.get(focusedNode.mother()); + + @Nullable FamilyTreeNode newRoot = father != null ? father : mother; + + TreeNode fNode = newRoot == null ? new TreeNode() : new TreeNode(newRoot, false); + fNode.children.add(root); + + @Nullable FamilyTreeNode spouse = newRoot == father ? mother : father; + + fNode.spouse = spouse == null ? new TreeNode() : new TreeNode(spouse, false); + + if (newRoot != null && levels > 0) { + return insertParents(fNode, newRoot, levels - 1); + } + + return fNode; + } + + private final class TreeNode { + private boolean widthComputed; + + private int width; + + private int labelWidth; + + private final List label = new ArrayList<>(); + + private final List children = new ArrayList<>(); + + private Bounds bounds; + + TreeNode spouse; + + final UUID id; + + final boolean deceased; + + @Nullable + private TreeNode previous; + + private TreeNode() { + this.id = null; + this.deceased = false; + this.label.add(new LiteralText("???")); + } + + public TreeNode(FamilyTreeNode node, boolean recurse) { + this(node, new HashSet<>(), recurse); + } + + public TreeNode(FamilyTreeNode node, Set parsed, boolean recurse) { + nodes.put(node.id(), this); + this.id = node.id(); + this.deceased = node.isDeceased(); + this.label.add(new LiteralText(node.getName().isEmpty() ? "???" : node.getName()).formatted(node.gender().getColor())); + this.label.add(new TranslatableText("entity.minecraft.villager." + node.getProfession()).formatted(Formatting.GRAY)); + + FamilyTreeNode father = family.get(node.father()); + FamilyTreeNode mother = family.get(node.mother()); + if ((father == null || father.isDeceased()) && (mother == null || mother.isDeceased())) { + this.label.add(new TranslatableText("gui.family_tree.label.orphan").formatted(Formatting.GRAY)); + } + + if (node.getMarriageState() != MarriageState.SINGLE) { + this.label.add(new TranslatableText("marriage." + node.getMarriageState().base().getIcon())); + } + + if (recurse) { + node.children().forEach(child -> { + FamilyTreeNode e = family.get(child); + if (e != null) { + children.add(new TreeNode(e, parsed, parsed.add(child))); + } + }); + + FamilyTreeNode spouse = family.get(node.spouse()); + + if (spouse != null) { + this.spouse = new TreeNode(spouse, parsed, false); + } else if (!children.isEmpty()) { + this.spouse = new TreeNode(); + } + } + } + + public void render(MatrixStack matrices, int mouseX, int mouseY) { + + Bounds bounds = getBounds(); + + boolean isFocused = id != null && bounds.contains(mouseX, mouseY); + + if (isFocused) { + focused = this; + } + + int childrenStartX = -getWidth() / 2; + + for (int i = 0; i < children.size(); i++) { + TreeNode node = children.get(i); + + childrenStartX += (node.getWidth() + HORIZONTAL_SPACING) / 2; + + int x = childrenStartX + HORIZONTAL_SPACING / 2; + int y = VERTICAL_SPACING; + + drawHook(matrices, x, y); + + matrices.push(); + matrices.translate(x, y, 0); + node.render(matrices, mouseX - x, mouseY - y); + matrices.pop(); + + childrenStartX += (node.getWidth() + HORIZONTAL_SPACING) / 2; + } + + matrices.push(); + matrices.translate(0, 0, 400); + + int fillColor = isFocused ? 0xF0100040 : 0xF0100010; + int borderColor = isFocused ? 0xFF28007F : 1347420415; + + fill(matrices, bounds.left, bounds.top + 1, bounds.left + 1, bounds.bottom - 1, fillColor); + fill(matrices, bounds.right - 1, bounds.top + 1, bounds.right, bounds.bottom - 1, fillColor); + fill(matrices, bounds.left + 1, bounds.top, bounds.right - 1, bounds.bottom, fillColor); + + fill(matrices, bounds.left + 1, bounds.top + 1, bounds.left + 2, bounds.bottom - 1, borderColor); + fill(matrices, bounds.right - 2, bounds.top + 1, bounds.right - 1, bounds.bottom - 1, borderColor); + + fill(matrices, bounds.left + 2, bounds.top + 1, bounds.right - 2, bounds.top + 2, borderColor); + fill(matrices, bounds.left + 2, bounds.bottom - 2, bounds.right - 2, bounds.bottom - 1, borderColor); + + VertexConsumerProvider.Immediate immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().getBuffer()); + + int l = bounds.top + 5; + int k = bounds.left + 6; + + if (deceased) { + k += 20; + } + + Matrix4f matrix4f = matrices.peek().getModel(); + + TextRenderer r = MinecraftClient.getInstance().textRenderer; + + for (int s = 0; s < label.size(); ++s) { + Text line = label.get(s); + if (line != null) { + r.draw(line, k, l, -1, true, matrix4f, immediate, false, 0, 15728880); + } + + if (s == 0) { + l += 2; + } + + l += 10; + } + + immediate.draw(); + matrices.pop(); + + RenderSystemCompat.setShaderTexture(0, InteractScreen.ICON_TEXTURES); + + if (deceased) { + drawTexture(matrices, bounds.left + 6, bounds.top + 6, 0, 16, 16, 16, 16, 256, 256); + + if (isFocused && mouseX <= bounds.left + 20) { + matrices.push(); + matrices.translate(0, 0, 20); + renderTooltip(matrices, new TranslatableText("gui.family_tree.label.deceased"), mouseX, mouseY); + matrices.pop(); + } + } + + if (spouse != null) { + int x = bounds.left - SPOUSE_HORIZONTAL_SPACING; + int y = bounds.top + bounds.bottom / 2; + + drawHorizontalLine(matrices, x, bounds.left - 1, y, 0xffffffff); + + drawTexture(matrices, bounds.left - SPOUSE_HORIZONTAL_SPACING / 2 - 8, y - 8, 0, 0, 0, 16, 16, 256, 256); + + y -= spouse.label.size() * textRenderer.fontHeight / 2; + x -= spouse.getWidth() / 2 - 6; + + matrices.push(); + matrices.translate(x, y, 0); + + spouse.render(matrices, mouseX - x, mouseY - y); + matrices.pop(); + } + } + + private void drawHook(MatrixStack matrices, int endX, int endY) { + int midY = endY / 2; + + drawVerticalLine(matrices, 0, 0, midY, 0xffffffff); + drawHorizontalLine(matrices, 0, endX, midY, 0xffffffff); + drawVerticalLine(matrices, endX, midY, endY, 0xffffffff); + } + + public int getWidth() { + if (!widthComputed) { + widthComputed = true; + labelWidth = label.stream().mapToInt(textRenderer::getWidth).max().orElse(0); + if (deceased) { + labelWidth += 20; + } + width = Math.max(labelWidth + 10, children.stream().mapToInt(TreeNode::getWidth).sum()) + (HORIZONTAL_SPACING / 2); + if (spouse != null) { + width += spouse.getWidth() + SPOUSE_HORIZONTAL_SPACING; + } + } + return width; + } + + public Bounds getBounds() { + if (bounds == null) { + getWidth(); + + int padding = 4; + bounds = new Bounds( + (-labelWidth / 2) - padding, + (labelWidth / 2) + padding * 2, + -padding, + textRenderer.fontHeight * label.size() + padding * 2 + ); + } + return bounds; + } + } + + static final class Bounds { + final int left; + final int right; + final int top; + final int bottom; + + public Bounds(int left, int right, int top, int bottom) { + this.left = left; + this.right = right; + this.top = top; + this.bottom = bottom; + } + + public Bounds add(int x, int y) { + return new Bounds(left + x, right + x, top + y, bottom + y); + } + + public boolean contains(int mouseX, int mouseY) { + return mouseX >= left + && mouseY >= top + && mouseX <= right + && mouseY <= bottom; + } + } +} diff --git a/common/src/main/java/mca/client/gui/FamilyTreeSearchScreen.java b/common/src/main/java/mca/client/gui/FamilyTreeSearchScreen.java new file mode 100644 index 0000000000..b62208b16f --- /dev/null +++ b/common/src/main/java/mca/client/gui/FamilyTreeSearchScreen.java @@ -0,0 +1,139 @@ +package mca.client.gui; + +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import mca.cobalt.network.NetworkHandler; +import mca.network.FamilyTreeUUIDLookup; +import mca.resources.data.SerializablePair; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; + +public class FamilyTreeSearchScreen extends Screen { + static int DATA_WIDTH = 120; + + private List>> list = new LinkedList<>(); + private ButtonWidget buttonPage; + private int pageNumber; + + private UUID selectedVillager; + + private int mouseX; + private int mouseY; + + public FamilyTreeSearchScreen() { + super(new TranslatableText("gui.family_tree.title")); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void init() { + TextFieldWidget field = addButton(new TextFieldWidget(this.textRenderer, width / 2 - DATA_WIDTH / 2, height / 2 - 80, DATA_WIDTH, 18, new TranslatableText("structure_block.structure_name"))); + field.setMaxLength(32); + field.setChangedListener(this::searchVillager); + field.setTextFieldFocused(true); + setFocused(field); + + addButton(new ButtonWidget(width / 2 - 44, height / 2 + 82, 88, 20, new TranslatableText("gui.done"), sender -> { + onClose(); + })); + + addButton(new ButtonWidget(width / 2 - 24 - 20, height / 2 + 60, 20, 20, new LiteralText("<"), (b) -> { + if (pageNumber > 0) { + pageNumber--; + } + })); + addButton(new ButtonWidget(width / 2 + 24, height / 2 + 60, 20, 20, new LiteralText(">"), (b) -> { + if (pageNumber < Math.ceil(list.size() / 9.0) - 1) { + pageNumber++; + } + })); + buttonPage = addButton(new ButtonWidget(width / 2 - 24, height / 2 + 60, 48, 20, new LiteralText("0/0)"), (b) -> { + })); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + assert client != null; + this.mouseX = (int)(client.mouse.getX() * width / client.getWindow().getFramebufferWidth()); + this.mouseY = (int)(client.mouse.getY() * height / client.getWindow().getFramebufferHeight()); + + fill(matrices, width / 2 - DATA_WIDTH / 2 - 10, height / 2 - 110, width / 2 + DATA_WIDTH / 2 + 10, height / 2 + 110, 0x66000000); + + renderBackground(matrices); + + renderVillagers(matrices); + + drawCenteredText(matrices, textRenderer, new TranslatableText("gui.title.family_tree"), width / 2, height / 2 - 100, 16777215); + + super.render(matrices, mouseX, mouseY, delta); + } + + private void renderVillagers(MatrixStack transform) { + int maxPages = (int)Math.ceil(list.size() / 9.0); + buttonPage.setMessage(new LiteralText((pageNumber + 1) + "/" + maxPages)); + + selectedVillager = null; + for (int i = 0; i < 9; i++) { + int index = i + pageNumber * 9; + if (index < list.size()) { + int y = height / 2 - 52 + i * 12; + boolean hover = isMouseWithin(width / 2 - 50, y - 1, 100, 12); + SerializablePair> pair = list.get(index); + String left = pair.getRight().getLeft(); + String right = pair.getRight().getRight(); + + Text text; + if (left.isEmpty() && right.isEmpty()) { + text = new TranslatableText("gui.family_tree.child_of_0"); + } else if (left.isEmpty()) { + text = new TranslatableText("gui.family_tree.child_of_1", right); + } else if (right.isEmpty()) { + text = new TranslatableText("gui.family_tree.child_of_1", left); + } else { + text = new TranslatableText("gui.family_tree.child_of_2", left, right); + } + + drawCenteredText(transform, textRenderer, text, width / 2, y, hover ? 0xFFD7D784 : 0xFFFFFFFF); + if (hover) { + selectedVillager = pair.getLeft(); + } + } else { + break; + } + } + } + + private void searchVillager(String v) { + if (!v.isEmpty()) { + NetworkHandler.sendToServer(new FamilyTreeUUIDLookup(v)); + } + } + + public void setList(List>> list) { + this.list = list; + } + + protected boolean isMouseWithin(int x, int y, int w, int h) { + return mouseX >= x && mouseX < x + w && mouseY >= y && mouseY < y + h; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (selectedVillager != null) { + MinecraftClient.getInstance().openScreen(new FamilyTreeScreen(selectedVillager)); + } + + return super.mouseClicked(mouseX, mouseY, button); + } +} diff --git a/common/src/main/java/mca/client/gui/InteractScreen.java b/common/src/main/java/mca/client/gui/InteractScreen.java new file mode 100644 index 0000000000..84f1685e3a --- /dev/null +++ b/common/src/main/java/mca/client/gui/InteractScreen.java @@ -0,0 +1,405 @@ +package mca.client.gui; + +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import mca.cobalt.network.NetworkHandler; +import mca.entity.VillagerLike; +import mca.entity.ai.Genetics; +import mca.entity.ai.Memories; +import mca.entity.ai.Traits; +import mca.entity.ai.brain.VillagerBrain; +import mca.entity.ai.relationship.CompassionateEntity; +import mca.entity.ai.relationship.MarriageState; +import mca.network.GetInteractDataRequest; +import mca.network.InteractionCloseRequest; +import mca.network.InteractionDialogueInitMessage; +import mca.network.InteractionDialogueMessage; +import mca.network.InteractionServerMessage; +import mca.network.InteractionVillagerMessage; +import mca.resources.data.Analysis; +import mca.resources.data.Question; +import mca.util.compat.RenderSystemCompat; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.text.BaseText; +import net.minecraft.text.LiteralText; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.text.TranslatableText; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.village.VillagerProfession; +import org.lwjgl.glfw.GLFW; + +public class InteractScreen extends AbstractDynamicScreen { + public static final Identifier ICON_TEXTURES = new Identifier("mca:textures/gui.png"); + + private final VillagerLike villager; + private final PlayerEntity player = MinecraftClient.getInstance().player; + + private boolean inGiftMode; + private int timeSinceLastClick; + + private String father; + private String mother; + + private MarriageState marriageState; + private Text spouse; + + private List dialogAnswers; + private String dialogAnswerHover; + private List dialogQuestionText; + private String dialogQuestionId; + + private static Analysis analysis; + + public InteractScreen(VillagerLike villager) { + super(new LiteralText("Interact")); + this.villager = villager; + } + + public void setParents(String father, String mother) { + this.father = father; + this.mother = mother; + } + + public void setSpouse(MarriageState marriageState, String spouse) { + this.marriageState = marriageState; + this.spouse = spouse == null ? new TranslatableText("gui.interact.label.parentUnknown") : new LiteralText(spouse); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + Objects.requireNonNull(this.client).openScreen(null); + NetworkHandler.sendToServer(new InteractionCloseRequest(villager.asEntity().getUuid())); + } + + @Override + public void init() { + NetworkHandler.sendToServer(new GetInteractDataRequest(villager.asEntity().getUuid())); + } + + @Override + public void tick() { + if (timeSinceLastClick < 100) { + timeSinceLastClick++; + } + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float tickDelta) { + super.render(matrices, mouseX, mouseY, tickDelta); + + drawIcons(matrices); + drawTextPopups(matrices); + } + + @Override + public boolean mouseScrolled(double x, double y, double d) { + if (d < 0) { + player.inventory.selectedSlot = player.inventory.selectedSlot == 8 ? 0 : player.inventory.selectedSlot + 1; + } else if (d > 0) { + player.inventory.selectedSlot = player.inventory.selectedSlot == 0 ? 8 : player.inventory.selectedSlot - 1; + } + + return super.mouseScrolled(x, y, d); + } + + @Override + public boolean mouseClicked(double posX, double posY, int button) { + super.mouseClicked(posX, posY, button); + + // Dialog + if (button == 0 && dialogAnswerHover != null && dialogQuestionText != null) { + NetworkHandler.sendToServer(new InteractionDialogueMessage(villager.asEntity().getUuid(), dialogQuestionId, dialogAnswerHover)); + } + + // Right mouse button + if (inGiftMode && button == 1) { + NetworkHandler.sendToServer(new InteractionVillagerMessage("gui.button.gift", villager.asEntity().getUuid())); + return true; + } else { + return false; + } + } + + @Override + public boolean keyPressed(int keyChar, int keyCode, int unknown) { + // Hotkey to leave gift mode + if (keyChar == GLFW.GLFW_KEY_ESCAPE) { + if (inGiftMode) { + inGiftMode = false; + setLayout("interact"); + } else { + onClose(); + } + return true; + } + return false; + } + + private void drawIcons(MatrixStack transform) { + Memories memory = villager.getVillagerBrain().getMemoriesForPlayer(player); + + transform.push(); + transform.scale(iconScale, iconScale, iconScale); + + RenderSystemCompat.setShaderTexture(0, ICON_TEXTURES); + + if (marriageState != null) { + drawIcon(transform, marriageState.getIcon()); + } + + drawIcon(transform, memory.getHearts() < 0 ? "blackHeart" : memory.getHearts() >= 100 ? "goldHeart" : "redHeart"); + // drawIcon(transform, "neutralEmerald"); + drawIcon(transform, "genes"); + + if (canDrawParentsIcon()) { + drawIcon(transform, "parents"); + } + if (canDrawGiftIcon()) { + drawIcon(transform, "gift"); + } + + if (analysis != null) { + drawIcon(transform, "analysis"); + } + + transform.pop(); + } + + private void drawTextPopups(MatrixStack transform) { + //general information + VillagerProfession profession = villager.getVillagerData().getProfession(); + + //name or state tip (gifting, ...) + int h = 17; + if (inGiftMode) { + renderTooltip(transform, new TranslatableText("gui.interact.label.giveGift"), 10, 28); + } else { + renderTooltip(transform, villager.asEntity().getName(), 10, 28); + } + + //age or profession + String prof = profession.toString(); + if (prof.equals("none")) { + prof = "mca.none"; + } + renderTooltip(transform, villager.asEntity().isBaby() ? villager.getAgeState().getName() : new TranslatableText("entity.minecraft.villager." + prof), 10, 30 + h); + + VillagerBrain brain = villager.getVillagerBrain(); + + //mood + renderTooltip(transform, + new TranslatableText("gui.interact.label.mood", brain.getMood().getText()) + .formatted(brain.getMood().getColor()), 10, 30 + h * 2); + + //personality + if (hoveringOverText(10, 30 + h * 3, 128)) { + renderTooltip(transform, brain.getPersonality().getDescription(), 10, 30 + h * 3); + } else { + //White as we don't know if a personality is negative + renderTooltip(transform, new TranslatableText("gui.interact.label.personality", brain.getPersonality().getName()).formatted(Formatting.WHITE), 10, 30 + h * 3); + } + + //traits + Set traits = villager.getTraits().getTraits(); + if (traits.size() > 0) { + if (hoveringOverText(10, 30 + h * 4, 128)) { + //details + List traitText = traits.stream().map(Traits.Trait::getDescription).collect(Collectors.toList()); + traitText.add(0, new TranslatableText("traits.title")); + renderTooltip(transform, traitText, 10, 30 + h * 4); + } else { + //list + TranslatableText traitText = new TranslatableText("traits.title"); + traits.stream().map(Traits.Trait::getName).forEach(t -> { + if (traitText.getSiblings().size() > 0) { + traitText.append(new LiteralText(", ")); + } + traitText.append(t); + }); + renderTooltip(transform, traitText, 10, 30 + h * 4); + } + } + + //hearts + if (hoveringOverIcon("redHeart")) { + int hearts = brain.getMemoriesForPlayer(player).getHearts(); + drawHoveringIconText(transform, new LiteralText(hearts + " hearts"), "redHeart"); + } + + //marriage status + if (marriageState != null && hoveringOverIcon("married") && villager instanceof CompassionateEntity) { + String ms = marriageState.base().getIcon().toLowerCase(Locale.ENGLISH); + drawHoveringIconText(transform, new TranslatableText("gui.interact.label." + ms, spouse), "married"); + } + + //parents + if (canDrawParentsIcon() && hoveringOverIcon("parents")) { + drawHoveringIconText(transform, new TranslatableText("gui.interact.label.parents", + father == null ? new TranslatableText("gui.interact.label.parentUnknown") : father, + mother == null ? new TranslatableText("gui.interact.label.parentUnknown") : mother + ), "parents"); + } + + //gift + if (canDrawGiftIcon() && hoveringOverIcon("gift")) { + drawHoveringIconText(transform, new TranslatableText("gui.interact.label.gift"), "gift"); + } + + //genes + if (hoveringOverIcon("genes")) { + List lines = new LinkedList<>(); + lines.add(new LiteralText("Genes")); + + for (Genetics.Gene gene : villager.getGenetics()) { + String key = gene.getType().getTranslationKey(); + int value = (int)(gene.get() * 100); + lines.add(new TranslatableText("gene.tooltip", new TranslatableText(key), value)); + } + + drawHoveringIconText(transform, lines, "genes"); + } + + //analysis + if (hoveringOverIcon("analysis") && analysis != null) { + List lines = new LinkedList<>(); + lines.add(new TranslatableText("analysis.title").formatted(Formatting.GRAY)); + + //summands + for (Analysis.AnalysisElement d : analysis) { + lines.add(new TranslatableText("analysis." + d.getKey()) + .append(new LiteralText(": " + (d.isPositive() ? "+" : "") + d.getValue())) + .formatted(d.isPositive() ? Formatting.GREEN : Formatting.RED)); + } + + //total + String chance = analysis.getTotalAsString(); + lines.add(new TranslatableText("analysis.total").append(": " + chance)); + + drawHoveringIconText(transform, lines, "analysis"); + } + + //dialogue + if (dialogQuestionText != null) { + //background + fill(transform, width / 2 - 85, height / 2 - 50 - 10 * dialogQuestionText.size(), width / 2 + 85, + height / 2 - 30 + 10 * dialogAnswers.size(), 0x77000000); + + //question + int i = -dialogQuestionText.size(); + for (OrderedText t : dialogQuestionText) { + i++; + textRenderer.drawWithShadow(transform, t, width / 2.0f - textRenderer.getWidth(t) / 2.0f, (float)height / 2 - 50 + i * 10, 0xFFFFFFFF); + } + dialogAnswerHover = null; + + //separator + drawHorizontalLine(transform, width / 2 - 75, width / 2 + 75, height / 2 - 40, 0xAAFFFFFF); + + //answers + int y = height / 2 - 35; + for (String a : dialogAnswers) { + boolean hover = hoveringOver(width / 2 - 100, y - 3, 200, 10); + drawCenteredText(transform, textRenderer, new TranslatableText(Question.getTranslationKey(dialogQuestionId, a)), width / 2, y, hover ? 0xFFD7D784 : 0xAAFFFFFF); + if (hover) { + dialogAnswerHover = a; + } + y += 10; + } + } + } + + //checks if the mouse hovers over a tooltip + //tooltips are not rendered on the given coordinates, so we need an offset + private boolean hoveringOverText(int x, int y, int w) { + return hoveringOver(x + 8, y - 16, w, 16); + } + + private boolean canDrawParentsIcon() { + return father != null || mother != null; + } + + private boolean canDrawGiftIcon() { + return false;//villager.getVillagerBrain().getMemoriesForPlayer(player).isGiftPresent(); + } + + public void setDialogue(String dialogue, List answers, boolean silent) { + dialogQuestionId = dialogue; + dialogAnswers = answers; + BaseText translatable = villager.getTranslatable(player, Question.getTranslationKey(dialogQuestionId)); + dialogQuestionText = textRenderer.wrapLines(translatable, 160); + + if (!silent) { + villager.sendChatMessage(translatable, player); + } + } + + @Override + protected void buttonPressed(Button button) { + String id = button.identifier(); + + if (timeSinceLastClick <= 2) { + return; /* Prevents click-throughs on Mojang's button system */ + } + timeSinceLastClick = 0; + + /* Progression to different GUIs */ + if (id.equals("gui.button.interact")) { + setLayout("interact"); + } else if (id.equals("gui.button.command")) { + setLayout("command"); + disableButton("gui.button." + villager.getVillagerBrain().getMoveState().name().toLowerCase(Locale.ENGLISH)); + } else if (id.equals("gui.button.clothing")) { + setLayout("clothing"); + } else if (id.equals("gui.button.familyTree")) { + MinecraftClient.getInstance().openScreen(new FamilyTreeScreen(villager.asEntity().getUuid())); + } else if (id.equals("gui.button.talk")) { + children.clear(); + buttons.clear(); + NetworkHandler.sendToServer(new InteractionDialogueInitMessage(villager.asEntity().getUuid())); + } else if (id.equals("gui.button.work")) { + setLayout("work"); + disableButton("gui.button." + villager.getVillagerBrain().getCurrentJob().name().toLowerCase(Locale.ENGLISH)); + } else if (id.equals("gui.button.professions")) { + setLayout("professions"); + } else if (id.equals("gui.button.backarrow")) { + if (inGiftMode) { + inGiftMode = false; + setLayout("interact"); + } else if (getActiveScreen().equals("locations")) { + setLayout("interact"); + } else { + setLayout("main"); + } + } else if (id.equals("gui.button.locations")) { + setLayout("locations"); + } else if (button.notifyServer()) { + /* Anything that should notify the server is handled here */ + + if (button.targetServer()) { + NetworkHandler.sendToServer(new InteractionServerMessage(id)); + } else { + NetworkHandler.sendToServer(new InteractionVillagerMessage(id, villager.asEntity().getUuid())); + } + } else if (id.equals("gui.button.gift")) { + this.inGiftMode = true; + disableAllButtons(); + } + } + + public static void setAnalysis(Analysis analysis) { + InteractScreen.analysis = analysis; + } +} diff --git a/common/src/main/java/mca/client/gui/MCAScreens.java b/common/src/main/java/mca/client/gui/MCAScreens.java new file mode 100644 index 0000000000..ab60493e92 --- /dev/null +++ b/common/src/main/java/mca/client/gui/MCAScreens.java @@ -0,0 +1,81 @@ +package mca.client.gui; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +import mca.MCA; +import mca.client.resources.Icon; +import mca.resources.Resources; +import net.minecraft.resource.JsonDataLoader; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +public class MCAScreens extends JsonDataLoader { + protected static final Identifier ID = new Identifier(MCA.MOD_ID, "screens"); + private static final Type ICONS_TYPE = new TypeToken>() {}.getType(); + + private static MCAScreens INSTANCE; + + public static final MCAScreens getInstance() { + return INSTANCE; + } + + private final Map buttons = new HashMap<>(); + private final Map icons = new HashMap<>(); + + public MCAScreens() { + super(Resources.GSON, "api/gui"); + INSTANCE = this; + } + + @Override + protected void apply(Map data, ResourceManager manager, Profiler profiler) { + buttons.clear(); + icons.clear(); + data.forEach(this::loadScreen); + } + + private void loadScreen(Identifier id, JsonElement element) { + if (element.isJsonObject()) { + icons.putAll(Resources.GSON.fromJson(element, ICONS_TYPE)); + } else { + buttons.put(id, Resources.GSON.fromJson(element, Button[].class)); + } + } + + /** + * Returns an API icon based on its key + * + * @param key String key of icon + * @return Instance of APIIcon matching the ID provided + */ + public Icon getIcon(String key) { + return icons.getOrDefault(key, Icon.EMPTY); + } + + /** + * Gets all of the buttons for a particular screen. + * + * @param guiKey String key for the GUI's buttons + */ + public Optional getScreen(String guiKey) { + return Optional.ofNullable(buttons.get(new Identifier("mca", guiKey))); + } + + /** + * Returns an API button based on its ID + * + * @param id String id matching the targeted button + * @return Instance of APIButton matching the ID provided + */ + public Optional